refactor(core): [CORE-20] type entry and converge initialization

This commit is contained in:
Harvey Zhao committed 2026-09-11 16:07:06 +08:00
1 parent ccf77c4ed2
commit ee895a558f
100 files changed
+33854 -839

No files matched your search

+1 -1
View File
@@ -130,7 +130,7 @@ For proxy/plugin work, prefer validating on the local demo page rather than reas
### Core package
- `packages/artplayer/src/index.js`: player entry
- `packages/artplayer/src/index.ts`: player entry
- `packages/artplayer/src/player/`: playback mixins and player-facing behavior
- `packages/artplayer/src/control/`: bottom controls
- `packages/artplayer/src/setting/`: settings panel
+1 -1
View File
@@ -67,4 +67,4 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!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 n=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(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(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1",a={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 n=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(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(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1";function a(t,e){let r=document.getElementById(t);r||(r=document.createElement("style"),r.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(r)}):(document.head||document.documentElement).appendChild(r)),r.textContent=e}const s=globalThis.CUSTOM_USER_AGENT??("undefined"===typeof navigator?"":navigator.userAgent),l=/^(?:(?!chrome|android).)*safari/i.test(s),c=/iPad|iPhone|iPod/i.test(s)&&("undefined"===typeof window||!window.MSStream),d=c||s.includes("Macintosh")&&"undefined"!==typeof navigator&&navigator.maxTouchPoints>=1,p=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(s)||d,u="undefined"!==typeof window&&"undefined"!==typeof document;const h={properties:["audioTracks","autoplay","buffered","controller","controlLine truncated
+1 -1
View File
@@ -67,4 +67,4 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!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,n=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,i=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,s=(t,r,n)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[r]=n,l=(t,e)=>{for(var r in e||(e={}))i.call(e,r)&&s(t,r,e[r]);if(o)for(var r of o(e))a.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,n(e)),d=(t,e,r)=>new Promise((n,o)=>{var i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function u(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var p,h={exports:{}};var f=(p||(p=1,h.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(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(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),h.exports);const m=u(f),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=Object.defineProperty,e=Object.defineProperties,r=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,a=(e,r,n)=>r in e?t(e,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[r]=n,s=(t,e)=>{for(var r in e||(e={}))o.call(e,r)&&a(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&a(t,r,e[r]);return t},l=(t,n)=>e(t,r(n)),c=(t,e,r)=>new Promise((n,o)=>{var i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function d(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var u,p={exports:{}};var h=(u||(u=1,p.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(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(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),p.exports);const f=d(h),m="5.4.1";function g(t,e){let r=document.getElementById(t);r||(r=document.createElement("style"),r.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.apLine truncated
+345 -210
View File
@@ -221,6 +221,44 @@ function requireOptionValidator() {
var optionValidatorExports = requireOptionValidator();
const validator = /* @__PURE__ */ getDefaultExportFromCjs(optionValidatorExports);
const version$1 = "5.4.1";
function setStyleText(id2, style) {
let $style = document.getElementById(id2);
if (!$style) {
$style = document.createElement("style");
$style.id = id2;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
document.head.appendChild($style);
});
} else {
(document.head || document.documentElement).appendChild($style);
}
}
$style.textContent = style;
}
const customAgent = globalThis.CUSTOM_USER_AGENT;
const userAgent = customAgent ?? (typeof navigator === "undefined" ? "" : navigator.userAgent);
const isSafari = /^(?:(?!chrome|android).)*safari/i.test(userAgent);
const isIOS = /iPad|iPhone|iPod/i.test(userAgent) && (typeof window === "undefined" || !window.MSStream);
const isIOS13 = isIOS || userAgent.includes("Macintosh") && typeof navigator !== "undefined" && navigator.maxTouchPoints >= 1;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || isIOS13;
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
function publishBrowserEntry(Artplayer2, style) {
if (!isBrowser)
return;
window.Artplayer = Artplayer2;
setStyleText("artplayer-style", style);
setTimeout(() => {
if (Artplayer2.LOG_VERSION) {
console.log(
`%c ArtPlayer %c ${Artplayer2.version} %c https://artplayer.org`,
"color: #fff; background: #5f5f5f",
"color: #fff; background: #4bc729",
""
);
}
}, 100);
}
const config$1 = {
properties: [
"audioTracks",
@@ -387,6 +425,16 @@ function isClosing(owner) {
const state2 = stateOf$1(owner);
return state2.destroying || state2.scope.closed;
}
function duringTemplateMount(owner, template, initialize) {
const state2 = stateOf$1(owner);
const previous = state2.mountingTemplate;
state2.mountingTemplate = template;
try {
initialize();
} finally {
state2.mountingTemplate = previous;
}
}
function ownContainer(owner, container, rollback) {
const current2 = containers.get(container);
if (current2 && current2 !== owner)
@@ -420,7 +468,7 @@ function destroyInstance(owner, instances2, removeHtml, removeSource, failed = f
if (removeSource && owner.template?.$video)
attempt(() => owner.reset());
attempt(() => state2.scope.dispose());
attempt(() => owner.template?.destroy(removeHtml));
attempt(() => (owner.template || state2.mountingTemplate)?.destroy(removeHtml));
const index = instances2.indexOf(owner);
if (index !== -1)
instances2.splice(index, 1);
@@ -490,12 +538,122 @@ function controlEvents(art, element) {
proxy: (target, name, callback) => proxyEntry(art, element, target, name, callback)
};
}
const userAgent = globalThis?.CUSTOM_USER_AGENT ?? (typeof navigator !== "undefined" ? navigator.userAgent : "");
const isSafari = /^(?:(?!chrome|android).)*safari/i.test(userAgent);
const isIOS = /iPad|iPhone|iPod/i.test(userAgent) && !window.MSStream;
const isIOS13 = isIOS || userAgent.includes("Macintosh") && navigator.maxTouchPoints >= 1;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || isIOS13;
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
function getComposedPath(event) {
if (event.composedPath)
return event.composedPath();
const path = [];
let node = event.target;
while (node) {
path.push(node);
node = node.parentNode ?? null;
}
if (typeof window !== "undefined" && !path.includes(window))
path.push(window);
return path;
}
function includeFromEvent(event, target) {
return getComposedPath(event).includes(target);
}
function supportsFlex() {
const div = document.createElement("div");
div.style.display = "flex";
return div.style.display === "flex";
}
function getRect(el) {
return el.getBoundingClientRect();
}
function isInViewport(el, offset = 0) {
const rect = el.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
const vertInView = rect.top - offset <= windowHeight && rect.top + rect.height + offset >= 0;
const horInView = rect.left - offset <= windowWidth + offset && rect.left + rect.width + offset >= 0;
return vertInView && horInView;
}
function getSafeAreaInsets() {
const div = document.createElement("div");
div.style.cssText = "position:fixed;top:env(safe-area-inset-top,0px);right:env(safe-area-inset-right,0px);bottom:env(safe-area-inset-bottom,0px);left:env(safe-area-inset-left,0px);pointer-events:none;visibility:hidden;";
try {
document.body.appendChild(div);
const style = getComputedStyle(div);
return {
top: Number.parseFloat(style.top) || 0,
right: Number.parseFloat(style.right) || 0,
bottom: Number.parseFloat(style.bottom) || 0,
left: Number.parseFloat(style.left) || 0
};
} finally {
div.remove();
}
}
function query(selector, parent = document) {
return parent.querySelector(selector);
}
function queryAll(selector, parent = document) {
return Array.from(parent.querySelectorAll(selector));
}
function addClass(target, className2) {
return target.classList.add(className2);
}
function removeClass(target, className2) {
return target.classList.remove(className2);
}
function hasClass(target, className2) {
return target.classList.contains(className2);
}
function append(parent, child) {
if (child instanceof Element) {
parent.appendChild(child);
} else {
parent.insertAdjacentHTML("beforeend", String(child));
}
return parent.lastElementChild || parent.lastChild;
}
function remove(child) {
return child.parentNode.removeChild(child);
}
function siblings(target) {
return Array.from(target.parentElement.children).filter((item) => item !== target);
}
function inverseClass(target, className2) {
siblings(target).forEach((item) => removeClass(item, className2));
addClass(target, className2);
}
function replaceElement(newChild, oldChild) {
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
}
function createElement(tag) {
return document.createElement(tag);
}
function tooltip(target, msg, pos = "top") {
if (isMobile)
return;
target.setAttribute("aria-label", msg);
addClass(target, "hint--rounded");
addClass(target, `hint--${pos}`);
}
function getIcon(key = "", html2 = "") {
const icon = createElement("i");
addClass(icon, "art-icon");
addClass(icon, `art-icon-${key}`);
append(icon, html2);
return icon;
}
function setStyle(element, key, value) {
const style = element.style;
style[key] = value;
return element;
}
function setStyles(element, styles) {
for (const key in styles)
setStyle(element, key, styles[key]);
return element;
}
function getStyle(element, key, numberType = true) {
const value = window.getComputedStyle(element, null).getPropertyValue(key);
return numberType ? Number.parseFloat(value) : value;
}
function requestImage(url, scale, owner) {
return new Promise((resolve, reject) => {
const request = owner ? owner.child() : new ResourceScope();
@@ -622,136 +780,6 @@ function loadImg(url, scale) {
function loadThumbnailImage(url, scale, owner) {
return requestImage(url, scale, owner);
}
function setStyleText(id2, style) {
let $style = document.getElementById(id2);
if (!$style) {
$style = document.createElement("style");
$style.id = id2;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
document.head.appendChild($style);
});
} else {
(document.head || document.documentElement).appendChild($style);
}
}
$style.textContent = style;
}
function query(selector, parent = document) {
return parent.querySelector(selector);
}
function queryAll(selector, parent = document) {
return Array.from(parent.querySelectorAll(selector));
}
function addClass(target, className2) {
return target.classList.add(className2);
}
function removeClass(target, className2) {
return target.classList.remove(className2);
}
function hasClass(target, className2) {
return target.classList.contains(className2);
}
function append(parent, child) {
if (child instanceof Element) {
parent.appendChild(child);
} else {
parent.insertAdjacentHTML("beforeend", String(child));
}
return parent.lastElementChild || parent.lastChild;
}
function remove(child) {
return child.parentNode.removeChild(child);
}
function setStyle(element, key, value) {
element.style[key] = value;
return element;
}
function setStyles(element, styles) {
for (const key in styles) {
setStyle(element, key, styles[key]);
}
return element;
}
function getStyle(element, key, numberType = true) {
const value = window.getComputedStyle(element, null).getPropertyValue(key);
return numberType ? Number.parseFloat(value) : value;
}
function siblings(target) {
return Array.from(target.parentElement.children).filter((item) => item !== target);
}
function inverseClass(target, className2) {
siblings(target).forEach((item) => removeClass(item, className2));
addClass(target, className2);
}
function tooltip(target, msg, pos = "top") {
if (isMobile)
return;
target.setAttribute("aria-label", msg);
addClass(target, "hint--rounded");
addClass(target, `hint--${pos}`);
}
function isInViewport(el, offset = 0) {
const rect = el.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
const vertInView = rect.top - offset <= windowHeight && rect.top + rect.height + offset >= 0;
const horInView = rect.left - offset <= windowWidth + offset && rect.left + rect.width + offset >= 0;
return vertInView && horInView;
}
function includeFromEvent(event, target) {
return getComposedPath(event).includes(target);
}
function replaceElement(newChild, oldChild) {
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
}
function createElement(tag) {
return document.createElement(tag);
}
function getIcon(key = "", html2 = "") {
const icon = createElement("i");
addClass(icon, "art-icon");
addClass(icon, `art-icon-${key}`);
append(icon, html2);
return icon;
}
function supportsFlex() {
const div = document.createElement("div");
div.style.display = "flex";
return div.style.display === "flex";
}
function getRect(el) {
return el.getBoundingClientRect();
}
function getComposedPath(event) {
if (event.composedPath)
return event.composedPath();
const path = [];
let node = event.target;
while (node) {
path.push(node);
node = node.parentNode;
}
if (!path.includes(window) && window !== void 0) {
path.push(window);
}
return path;
}
function getSafeAreaInsets() {
const div = document.createElement("div");
div.style.cssText = "position:fixed;top:env(safe-area-inset-top,0px);right:env(safe-area-inset-right,0px);bottom:env(safe-area-inset-bottom,0px);left:env(safe-area-inset-left,0px);pointer-events:none;visibility:hidden;";
document.body.appendChild(div);
const style = getComputedStyle(div);
const insets = {
top: Number.parseFloat(style.top) || 0,
right: Number.parseFloat(style.right) || 0,
bottom: Number.parseFloat(style.bottom) || 0,
left: Number.parseFloat(style.left) || 0
};
div.remove();
return insets;
}
class ArtPlayerError extends Error {
constructor(message, context) {
super(message);
@@ -3444,7 +3472,12 @@ function resolveOption(input, defaults2) {
merged.container = input.container;
return validator(merged, scheme);
}
function resolveRuntimeOption(input, defaults2) {
return resolveOption(input, defaults2);
}
function airplayMix(art) {
if (isClosing(art))
return;
const {
i18n,
notice,
@@ -3452,8 +3485,16 @@ function airplayMix(art) {
template: { $video }
} = art;
let available = true;
if (window.WebKitPlaybackTargetAvailabilityEvent && $video.webkitShowPlaybackTargetPicker) {
const supported = window.WebKitPlaybackTargetAvailabilityEvent;
if (isClosing(art))
return;
const picker = supported && $video.webkitShowPlaybackTargetPicker;
if (isClosing(art))
return;
if (picker) {
proxy($video, "webkitplaybacktargetavailabilitychanged", (event) => {
if (isClosing(art))
return;
switch (event.availability) {
case "available":
available = true;
@@ -3466,13 +3507,23 @@ function airplayMix(art) {
} else {
available = false;
}
if (isClosing(art))
return;
def(art, "airplay", {
value() {
if (isClosing(art))
return;
if (available) {
$video.webkitShowPlaybackTargetPicker();
art.emit("airplay");
const picker2 = $video.webkitShowPlaybackTargetPicker;
if (isClosing(art))
return;
Reflect.apply(picker2, $video, []);
if (!isClosing(art))
art.emit("airplay");
} else {
notice.show = i18n.get("AirPlay Not Available");
const message = i18n.get("AirPlay Not Available");
if (!isClosing(art))
notice.show = message;
}
}
});
@@ -3534,9 +3585,10 @@ function attrMix(art) {
} = art;
def(art, "attr", {
value(key, value) {
const target = $video;
if (value === void 0)
return $video[key];
$video[key] = value;
return target[key];
target[key] = value;
}
});
}
@@ -4678,41 +4730,93 @@ function miniMix(art) {
def(art, "mini", mini(art));
}
function optionInit(art) {
if (isClosing(art))
return;
const {
option,
storage,
template: { $video, $poster }
} = art;
for (const key in option.moreVideoAttr) {
art.attr(key, option.moreVideoAttr[key]);
if (isClosing(art))
return;
const value = option.moreVideoAttr[key];
if (isClosing(art))
return;
art.attr(key, value);
}
if (isClosing(art))
return;
if (option.muted) {
art.muted = option.muted;
const value = option.muted;
if (isClosing(art))
return;
art.muted = value;
}
if (isClosing(art))
return;
if (option.volume) {
$video.volume = clamp(option.volume, 0, 1);
const value = clamp(option.volume, 0, 1);
if (isClosing(art))
return;
$video.volume = value;
}
if (isClosing(art))
return;
const volumeStorage = storage.get("volume");
if (isClosing(art))
return;
if (typeof volumeStorage === "number") {
$video.volume = clamp(volumeStorage, 0, 1);
}
if (isClosing(art))
return;
if (option.poster) {
setStyle($poster, "backgroundImage", `url(${option.poster})`);
const value = `url(${option.poster})`;
if (isClosing(art))
return;
setStyle($poster, "backgroundImage", value);
}
if (isClosing(art))
return;
if (option.autoplay) {
$video.autoplay = option.autoplay;
const value = option.autoplay;
if (isClosing(art))
return;
$video.autoplay = value;
}
if (isClosing(art))
return;
if (option.playsInline) {
if (isClosing(art))
return;
$video.playsInline = true;
if (isClosing(art))
return;
$video["webkit-playsinline"] = true;
}
if (isClosing(art))
return;
if (option.theme) {
option.cssVar["--art-theme"] = option.theme;
const styles = option.cssVar;
const theme = option.theme;
if (isClosing(art))
return;
styles["--art-theme"] = theme;
}
for (const key in option.cssVar) {
art.cssVar(key, option.cssVar[key]);
if (isClosing(art))
return;
const value = option.cssVar[key];
if (isClosing(art))
return;
art.cssVar(key, value);
}
art.url = option.url;
if (isClosing(art))
return;
const url = option.url;
if (!isClosing(art))
art.url = url;
}
function pauseMix(art) {
const {
@@ -5614,43 +5718,52 @@ function volumeMix(art) {
}
});
}
const installers = [
attrMix,
playMix,
pauseMix,
toggleMix,
seekMix,
volumeMix,
currentTimeMix,
durationMix,
switchMix,
playbackRateMix,
aspectRatioMix,
screenshotMix,
fullscreenMix,
fullscreenWebMix,
pipMix,
loadedMix,
playedMix,
playingMix,
autoSizeMix,
rectMix,
flipMix,
miniMix,
posterMix,
autoHeightMix,
cssVarMix,
themeMix,
typeMix,
stateMix,
subtitleOffsetMix,
airplayMix,
qualityMix,
thumbnailsMix,
eventInit,
optionInit
];
class Player {
constructor(art) {
if (isClosing(art))
return;
urlMix(art);
attrMix(art);
playMix(art);
pauseMix(art);
toggleMix(art);
seekMix(art);
volumeMix(art);
currentTimeMix(art);
durationMix(art);
switchMix(art);
playbackRateMix(art);
aspectRatioMix(art);
screenshotMix(art);
fullscreenMix(art);
fullscreenWebMix(art);
pipMix(art);
loadedMix(art);
playedMix(art);
playingMix(art);
autoSizeMix(art);
rectMix(art);
flipMix(art);
miniMix(art);
posterMix(art);
autoHeightMix(art);
cssVarMix(art);
themeMix(art);
typeMix(art);
stateMix(art);
subtitleOffsetMix(art);
airplayMix(art);
qualityMix(art);
thumbnailsMix(art);
eventInit(art);
optionInit(art);
for (const install of installers) {
if (isClosing(art))
return;
install(art);
}
}
}
const owners$2 = /* @__PURE__ */ new WeakMap();
@@ -6136,9 +6249,8 @@ function installBuiltins(registry, option) {
registry.add(lock);
if (!isClosing(registry.art) && option.autoPlayback && !option.isLive)
registry.add(autoPlayback);
if (!isClosing(registry.art) && option.autoOrientation && isMobile) {
if (!isClosing(registry.art) && option.autoOrientation && isMobile)
registry.add(autoOrientation);
}
if (!isClosing(registry.art) && option.fastForward && isMobile && !option.isLive)
registry.add(fastForward);
}
@@ -7112,7 +7224,7 @@ function createSettingHeader(setting2, item) {
setStyle($item, "height", `${SETTING_ITEM_HEIGHT}px`);
addClass($item, "art-setting-item");
addClass($item, "art-setting-item-back");
const $left = append($item, '<div class="art-setting-item-left"></div>');
const $left = appendElement($item, '<div class="art-setting-item-left"></div>');
const $icon = document.createElement("div");
addClass($icon, "art-setting-item-left-icon");
append($icon, arrowLeft2);
@@ -7153,8 +7265,8 @@ function createSettingItem(setting2, item, isUpdate = false) {
$item.dataset.value = String(item.value || "");
if (!current2())
return;
const $left = append($item, '<div class="art-setting-item-left"></div>');
const $right = append($item, '<div class="art-setting-item-right"></div>');
const $left = appendElement($item, '<div class="art-setting-item-left"></div>');
const $right = appendElement($item, '<div class="art-setting-item-right"></div>');
const $icon = document.createElement("div");
addClass($icon, "art-setting-item-left-icon");
switch (type) {
@@ -7204,8 +7316,8 @@ function createSettingItem(setting2, item, isUpdate = false) {
case "switch": {
const $switch = document.createElement("div");
addClass($switch, "art-setting-item-right-icon");
const $switchOn = append($switch, icons.switchOn);
const $switchOff = append($switch, icons.switchOff);
const $switchOn = appendElement($switch, icons.switchOn);
const $switchOff = appendElement($switch, icons.switchOff);
const initialSwitch = item.switch;
if (!current2())
return;
@@ -7992,8 +8104,10 @@ class Template {
);
this.query = this.query.bind(this);
ownContainer(art, this.$container, captureTemplate(this.$container));
this.$container.dataset.artId = String(art.id);
this.init();
duringTemplateMount(art, this, () => {
this.$container.dataset.artId = String(art.id);
this.init();
});
}
static get html() {
return html;
@@ -8009,6 +8123,8 @@ class Template {
bindNodes(this);
if (option.proxy) {
const video = option.proxy.call(this.art, this.art);
if (isClosing(this.art))
return;
assertProxy(video);
replaceElement(video, this.$video);
video.className = "art-video";
@@ -8097,7 +8213,7 @@ class Artplayer extends Emitter {
throw new Error("Artplayer can only be used in the browser environment");
}
this.id = ++id;
this.option = resolveOption(option, Artplayer.option);
this.option = resolveRuntimeOption(option, Artplayer.option);
this.isLock = false;
this.isReady = false;
this.isFocus = false;
@@ -8107,21 +8223,53 @@ class Artplayer extends Emitter {
beginLifecycle(this);
try {
this.template = new Template(this);
if (getScope(this).closed)
return;
this.events = new Events(this);
this.storage = new Storage(this);
if (getScope(this).closed)
return;
this.storage = new Storage();
if (getScope(this).closed)
return;
this.icons = new Icons(this);
if (getScope(this).closed)
return;
this.i18n = new I18n(this);
if (getScope(this).closed)
return;
this.notice = new Notice(this);
if (getScope(this).closed)
return;
this.player = new Player(this);
if (getScope(this).closed)
return;
this.layers = new Layer(this);
if (getScope(this).closed)
return;
this.controls = new Control(this);
if (getScope(this).closed)
return;
this.contextmenu = new Contextmenu(this);
if (getScope(this).closed)
return;
this.subtitle = new Subtitle(this);
if (getScope(this).closed)
return;
this.info = new Info(this);
if (getScope(this).closed)
return;
this.loading = new Loading(this);
if (getScope(this).closed)
return;
this.hotkey = new Hotkey(this);
if (getScope(this).closed)
return;
this.mask = new Mask(this);
if (getScope(this).closed)
return;
this.setting = new Setting(this);
if (getScope(this).closed)
return;
this.plugins = new Plugins(this);
if (getScope(this).closed)
return;
@@ -8227,20 +8375,7 @@ Artplayer.FULLSCREEN_WEB_IN_BODY = true;
Artplayer.LOG_VERSION = true;
Artplayer.USE_RAF = false;
Artplayer.REMOVE_SRC_WHEN_DESTROY = true;
if (isBrowser) {
window.Artplayer = Artplayer;
setStyleText("artplayer-style", css);
setTimeout(() => {
if (Artplayer.LOG_VERSION) {
console.log(
`%c ArtPlayer %c ${Artplayer.version} %c https://artplayer.org`,
"color: #fff; background: #5f5f5f",
"color: #fff; background: #4bc729",
""
);
}
}, 100);
}
publishBrowserEntry(Artplayer, css);
export {
Artplayer as default
};
+1 -1
View File
@@ -49,7 +49,7 @@
"ci:build": "yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
"typecheck": "node scripts/typecheck.mjs",
"test:unit": "node --test test/playback.test.js test/dash-control.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/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/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.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/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js",
"test": "yarn test:node && yarn test:baseline",
"test:browser": "playwright test",
"test:browser:install": "playwright install chromium firefox webkit",
+108 -11
View File
@@ -1,8 +1,106 @@
# Core implementation and migration map
ArtPlayer keeps its existing constructor, player mixins, plugins and DOM/CSS hooks.
The production entry is still `src/index.js`; this document marks actual migrated
boundaries rather than describing the entire core as TypeScript.
The production entry is `src/index.ts`. All owned core production modules use
TypeScript; libs/screenfull.js remains an audited third-party source file.
## Entry and bootstrap (CORE-20)
The typed entry checks subsystem composition. Seven leaf
modules now have explicit source types: storage, environment flags and the attr,
cssVar, type, theme and poster mixins. Attribute access remains a deliberately
dynamic native/proxy property boundary; cssVar retains its truthy write condition
and passes values directly to the native style setter. The poster getter keeps
its historical quoted-value extraction and empty fallback. These small property
adapters remain single files rather than acquiring redundant forwarding layers.
storage.ts preserves the artplayer_settings JSON envelope and mutable name/settings
fields. Raw reads return unknown, including historically accepted JSON primitives;
this is not schema validation. Read errors use settings by reference, write errors
update that fallback, and successful native reads do not merge it. Changing that
failure policy requires an explicit compatibility decision rather than a type cast.
utils/dom.ts is a compatibility export surface. dom/tree.ts owns node/query/class
operations; dom/styles.ts owns native style conversion; dom/event-path.ts owns
composed/fallback paths; dom/measure.ts owns geometry, flex and safe-area probes;
dom/presentation.ts composes tree operations with environment flags for icons and
tooltips. None imports the utils barrel. The public export names remain the same.
Safe-area probes are removed even when append or style access fails. The frozen
CORE-19 helper is test-only own-source provenance; npm 5.4.0 has no such helper.
Typed callers distinguish optional nodes from known builtin markup. Setting
renderers reuse component/dom.ts appendElement only where they append known element
markup or icon wrappers. Cache assertions follow existing has checks; template
assertions describe mounted required nodes and preserve historical failures if
consumers remove those nodes. Public append still returns the actual final element,
text node or null; detached remove is not silently changed to a no-op.
Environment flags retain custom UA priority and lazy navigator reads. A custom
iOS/Macintosh UA can now be evaluated without window/navigator during SSR; fallback
event paths also work without window. Use environment, storage, facade-properties
and dom-boundaries Node tests and the browser dom-boundaries spec. entry.test.js and
the isolated package consumer also exercise these profiles through complete bundles.
config/index.ts retains mutable public media-property/method/event/prototype arrays
in their original order. player/optionInit.ts has a minimal initialization host and
preserves attribute -> muted -> configured volume -> stored volume -> poster ->
autoplay -> inline flags -> theme/CSS -> source order. External getter, storage and
setter boundaries cannot continue writing once lifecycle teardown closes the owner.
player/airplayMix.ts retains native picker receiver, availability and event order;
closed instances do not invoke the picker or update a notice. Native AirPlay errors
still propagate. Its controlled tests do not certify an actual Apple receiver.
test/initialization.test.js compares frozen CORE-19 functions with the new code;
test/types/core-initialization.ts checks raw storage, mutable config and nullable DOM
results. Internal PiP consumers accept the native element/null/boolean getter and
boolean setter; the public declaration compatibility work remains in CORE-21.
The auto-playback consumer owns the legacy unvalidated record assumption locally;
Storage does not promise that arbitrary persisted values are PlaybackTimes.
player/index.ts installs URL handling first with the actual generic host, then runs
the remaining 34 steps in one ordered table. Its PlayerHost is derived from the
owning modules' required capabilities, including deferred property/callback needs;
it is not proof that those properties already exist before Player construction.
The constructor keeps its empty instance shape and stops before each later installer
when the owner closes. src/index.ts similarly stops between subsystem constructors.
Do not reorder the table or initialize public fields eagerly: descriptor insertion,
callback access and the absence of not-yet-created subsystem properties are visible.
lifecycle/instance.ts temporarily owns the mounting Template while its constructor
runs. A proxy callback can destroy the instance before art.template is assigned;
teardown still clears/marks the DOM before the destroy event, with the first
removeHtml choice. The temporary reference is released in finally. Template.init
does not adopt or restyle a returned proxy after that callback destroys its owner.
The caller still owns a proxy it creates. test/browser/initialization.spec.js covers
these boundaries plus normal descriptor/static order and early native setters.
player/properties.ts describes the actual installed descriptors, separately from
installer inputs. The entry and Icons merge declaration-only interfaces with their
classes because emitted fields would shadow installed getters or change property
order. All subsystem assignments and mutable static assignments keep their original
order. bootstrap/browser.ts owns global publication, style injection and the deferred
version log; server imports do none of those browser operations.
option/runtime.ts is the compatibility boundary between old external callback
declarations and internal module views. It keeps validated merge behavior, callback
identity and runtime receivers; it does not wrap callbacks or assert the whole player
as the legacy declaration class. template/types.ts names the PlayerTemplate contract:
canonical template nodes plus a native/media-like proxy. The entry makes one scoped
assertion for caller-provided SSR/proxy inputs. This is a compatibility precondition,
not runtime validation; no eager missing-node rejection or synthesized nodes are added.
Template itself and query helpers keep nullable results. Complete public declaration
generation and consumer views remain CORE-21 work.
events/core-types.ts names known core/native payloads while leaving extension event
names open. CoreEmission narrows a module's emitted names against that shared map.
NoticeSink describes writing arbitrary notice values without pretending its getter
returns a message. Actual Notice visibility is boolean. Internal views also retain
undefined reads for setter-only properties and pre-metadata fullscreen, optional
subtitle icons, HTMLElement quality labels, and raw storage values.
plugins/builtins.ts now checks all five builtin hosts against the real assembled
Artplayer and no longer casts entire factories through unknown. The source entry
fixture checks this composition, native event payloads and property reads; ordinary
consumer fixtures still check the separate published declarations in five modes.
## Screenshot capture (CORE-19)
@@ -115,8 +213,8 @@ including its timer and previous playback rate. Touch move/end/cancel, lock, pau
source disposal and player destruction release the press. The native media element
still performs its own defaultPlaybackRate reset on load/source replacement.
Resource cleanup must not create a second press or overwrite a newer reentrant one.
The typed builtin factory cast is confined to the existing JS constructor boundary;
CORE-20 must revisit it when the full constructor host becomes typed.
The typed constructor checks the complete builtin host in CORE-20; the former
factory casts through unknown have been removed.
Run `test/notice.test.js` and `test/fast-forward.test.js` through the Node runner;
their matching `test/types` fixtures check source inference and retained consumers.
@@ -588,7 +686,7 @@ queries; component/types.ts defines the minimal generic host and callback shapes
component/resources.ts owns each entry's child ResourceScope, DOM proxies and guarded
Emitter subscriptions. control/resources.ts specializes the shared subscriptions with
UIEvents; it does not create a second event bus. Builtins depend on these capabilities,
not the full player class. CORE-20 still has to integrate the constructor facade.
not the full player class. The typed entry checks that it supplies these capabilities.
Control index.ts preserves routing and visibility behavior. builtins.ts preserves
installation order and option/platform conditions. selector.ts owns item binding,
@@ -704,8 +802,8 @@ minimal option hosts and internal sync/Promise return types. builtins.ts install
five existing builtins in their original order and reads each condition at its turn.
It keeps the mobile/live exclusions, including no fastForward for live media. The
constructor captures option once before builtin installation, then traverses the
same live user-plugin array. All five builtin implementations are now TypeScript;
their minimum-host factory casts remain confined to this constructor boundary until CORE-20.
same live user-plugin array. All five builtin implementations are TypeScript, and
BuiltinHost checks their combined requirements against the actual constructor host.
registration.ts owns result naming and non-enumerable/non-writable/non-configurable
registry properties. Names still prefer result.name, then factory.name, then the
@@ -983,9 +1081,8 @@ descriptors, boolean shim playing precedence, duration normalization and live la
reads are unchanged. play/pause/playing capture the original media object; duration
reads template.$video on each access, as before.
The JS construction facade still assembles these hosts dynamically. Its complete
static integration belongs to CORE-20; capability consumers migrate in their own
CORE tasks. Do not assume these types already validate every third-party proxy.
The typed construction facade checks these host requirements while retaining the
original staged initialization. Do not assume these types validate every third-party proxy.
Public art.video remains the original object and retains its existing declaration
for consumer compatibility. A public proxy typing extension requires separate
consumer checks; internal code must not use that old declaration to hide a canvas.
@@ -1117,5 +1214,5 @@ core files in docs/compiled when committing a shippable core change.
For new utility behavior, extend the same old/new contract tests. Clearly separate
intentional defect corrections from preserved behavior. For timers or Blob URLs,
also identify the owner and verify cleanup in the consuming module. Remaining
constructor, playback, setting, input and capability migrations are recorded
public declaration convergence, accessibility and release validation are recorded
in refactor/tasks.json and should extend this map as they land.
+1 -1
View File
@@ -67,4 +67,4 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!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 n=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(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(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1",a={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 n=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(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(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1";function a(t,e){let r=document.getElementById(t);r||(r=document.createElement("style"),r.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(r)}):(document.head||document.documentElement).appendChild(r)),r.textContent=e}const s=globalThis.CUSTOM_USER_AGENT??("undefined"===typeof navigator?"":navigator.userAgent),l=/^(?:(?!chrome|android).)*safari/i.test(s),c=/iPad|iPhone|iPod/i.test(s)&&("undefined"===typeof window||!window.MSStream),d=c||s.includes("Macintosh")&&"undefined"!==typeof navigator&&navigator.maxTouchPoints>=1,p=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(s)||d,u="undefined"!==typeof window&&"undefined"!==typeof document;const h={properties:["audioTracks","autoplay","buffered","controller","controlLine truncated
+1 -1
View File
@@ -67,4 +67,4 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!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,n=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,i=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,s=(t,r,n)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[r]=n,l=(t,e)=>{for(var r in e||(e={}))i.call(e,r)&&s(t,r,e[r]);if(o)for(var r of o(e))a.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,n(e)),d=(t,e,r)=>new Promise((n,o)=>{var i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function u(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var p,h={exports:{}};var f=(p||(p=1,h.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(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(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),h.exports);const m=u(f),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=Object.defineProperty,e=Object.defineProperties,r=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,a=(e,r,n)=>r in e?t(e,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[r]=n,s=(t,e)=>{for(var r in e||(e={}))o.call(e,r)&&a(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&a(t,r,e[r]);return t},l=(t,n)=>e(t,r(n)),c=(t,e,r)=>new Promise((n,o)=>{var i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function d(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var u,p={exports:{}};var h=(u||(u=1,p.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(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(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),p.exports);const f=d(h),m="5.4.1";function g(t,e){let r=document.getElementById(t);r||(r=document.createElement("style"),r.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.apLine truncated
+345 -210
View File
@@ -221,6 +221,44 @@ function requireOptionValidator() {
var optionValidatorExports = requireOptionValidator();
const validator = /* @__PURE__ */ getDefaultExportFromCjs(optionValidatorExports);
const version$1 = "5.4.1";
function setStyleText(id2, style) {
let $style = document.getElementById(id2);
if (!$style) {
$style = document.createElement("style");
$style.id = id2;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
document.head.appendChild($style);
});
} else {
(document.head || document.documentElement).appendChild($style);
}
}
$style.textContent = style;
}
const customAgent = globalThis.CUSTOM_USER_AGENT;
const userAgent = customAgent ?? (typeof navigator === "undefined" ? "" : navigator.userAgent);
const isSafari = /^(?:(?!chrome|android).)*safari/i.test(userAgent);
const isIOS = /iPad|iPhone|iPod/i.test(userAgent) && (typeof window === "undefined" || !window.MSStream);
const isIOS13 = isIOS || userAgent.includes("Macintosh") && typeof navigator !== "undefined" && navigator.maxTouchPoints >= 1;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || isIOS13;
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
function publishBrowserEntry(Artplayer2, style) {
if (!isBrowser)
return;
window.Artplayer = Artplayer2;
setStyleText("artplayer-style", style);
setTimeout(() => {
if (Artplayer2.LOG_VERSION) {
console.log(
`%c ArtPlayer %c ${Artplayer2.version} %c https://artplayer.org`,
"color: #fff; background: #5f5f5f",
"color: #fff; background: #4bc729",
""
);
}
}, 100);
}
const config$1 = {
properties: [
"audioTracks",
@@ -387,6 +425,16 @@ function isClosing(owner) {
const state2 = stateOf$1(owner);
return state2.destroying || state2.scope.closed;
}
function duringTemplateMount(owner, template, initialize) {
const state2 = stateOf$1(owner);
const previous = state2.mountingTemplate;
state2.mountingTemplate = template;
try {
initialize();
} finally {
state2.mountingTemplate = previous;
}
}
function ownContainer(owner, container, rollback) {
const current2 = containers.get(container);
if (current2 && current2 !== owner)
@@ -420,7 +468,7 @@ function destroyInstance(owner, instances2, removeHtml, removeSource, failed = f
if (removeSource && owner.template?.$video)
attempt(() => owner.reset());
attempt(() => state2.scope.dispose());
attempt(() => owner.template?.destroy(removeHtml));
attempt(() => (owner.template || state2.mountingTemplate)?.destroy(removeHtml));
const index = instances2.indexOf(owner);
if (index !== -1)
instances2.splice(index, 1);
@@ -490,12 +538,122 @@ function controlEvents(art, element) {
proxy: (target, name, callback) => proxyEntry(art, element, target, name, callback)
};
}
const userAgent = globalThis?.CUSTOM_USER_AGENT ?? (typeof navigator !== "undefined" ? navigator.userAgent : "");
const isSafari = /^(?:(?!chrome|android).)*safari/i.test(userAgent);
const isIOS = /iPad|iPhone|iPod/i.test(userAgent) && !window.MSStream;
const isIOS13 = isIOS || userAgent.includes("Macintosh") && navigator.maxTouchPoints >= 1;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || isIOS13;
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
function getComposedPath(event) {
if (event.composedPath)
return event.composedPath();
const path = [];
let node = event.target;
while (node) {
path.push(node);
node = node.parentNode ?? null;
}
if (typeof window !== "undefined" && !path.includes(window))
path.push(window);
return path;
}
function includeFromEvent(event, target) {
return getComposedPath(event).includes(target);
}
function supportsFlex() {
const div = document.createElement("div");
div.style.display = "flex";
return div.style.display === "flex";
}
function getRect(el) {
return el.getBoundingClientRect();
}
function isInViewport(el, offset = 0) {
const rect = el.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
const vertInView = rect.top - offset <= windowHeight && rect.top + rect.height + offset >= 0;
const horInView = rect.left - offset <= windowWidth + offset && rect.left + rect.width + offset >= 0;
return vertInView && horInView;
}
function getSafeAreaInsets() {
const div = document.createElement("div");
div.style.cssText = "position:fixed;top:env(safe-area-inset-top,0px);right:env(safe-area-inset-right,0px);bottom:env(safe-area-inset-bottom,0px);left:env(safe-area-inset-left,0px);pointer-events:none;visibility:hidden;";
try {
document.body.appendChild(div);
const style = getComputedStyle(div);
return {
top: Number.parseFloat(style.top) || 0,
right: Number.parseFloat(style.right) || 0,
bottom: Number.parseFloat(style.bottom) || 0,
left: Number.parseFloat(style.left) || 0
};
} finally {
div.remove();
}
}
function query(selector, parent = document) {
return parent.querySelector(selector);
}
function queryAll(selector, parent = document) {
return Array.from(parent.querySelectorAll(selector));
}
function addClass(target, className2) {
return target.classList.add(className2);
}
function removeClass(target, className2) {
return target.classList.remove(className2);
}
function hasClass(target, className2) {
return target.classList.contains(className2);
}
function append(parent, child) {
if (child instanceof Element) {
parent.appendChild(child);
} else {
parent.insertAdjacentHTML("beforeend", String(child));
}
return parent.lastElementChild || parent.lastChild;
}
function remove(child) {
return child.parentNode.removeChild(child);
}
function siblings(target) {
return Array.from(target.parentElement.children).filter((item) => item !== target);
}
function inverseClass(target, className2) {
siblings(target).forEach((item) => removeClass(item, className2));
addClass(target, className2);
}
function replaceElement(newChild, oldChild) {
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
}
function createElement(tag) {
return document.createElement(tag);
}
function tooltip(target, msg, pos = "top") {
if (isMobile)
return;
target.setAttribute("aria-label", msg);
addClass(target, "hint--rounded");
addClass(target, `hint--${pos}`);
}
function getIcon(key = "", html2 = "") {
const icon = createElement("i");
addClass(icon, "art-icon");
addClass(icon, `art-icon-${key}`);
append(icon, html2);
return icon;
}
function setStyle(element, key, value) {
const style = element.style;
style[key] = value;
return element;
}
function setStyles(element, styles) {
for (const key in styles)
setStyle(element, key, styles[key]);
return element;
}
function getStyle(element, key, numberType = true) {
const value = window.getComputedStyle(element, null).getPropertyValue(key);
return numberType ? Number.parseFloat(value) : value;
}
function requestImage(url, scale, owner) {
return new Promise((resolve, reject) => {
const request = owner ? owner.child() : new ResourceScope();
@@ -622,136 +780,6 @@ function loadImg(url, scale) {
function loadThumbnailImage(url, scale, owner) {
return requestImage(url, scale, owner);
}
function setStyleText(id2, style) {
let $style = document.getElementById(id2);
if (!$style) {
$style = document.createElement("style");
$style.id = id2;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
document.head.appendChild($style);
});
} else {
(document.head || document.documentElement).appendChild($style);
}
}
$style.textContent = style;
}
function query(selector, parent = document) {
return parent.querySelector(selector);
}
function queryAll(selector, parent = document) {
return Array.from(parent.querySelectorAll(selector));
}
function addClass(target, className2) {
return target.classList.add(className2);
}
function removeClass(target, className2) {
return target.classList.remove(className2);
}
function hasClass(target, className2) {
return target.classList.contains(className2);
}
function append(parent, child) {
if (child instanceof Element) {
parent.appendChild(child);
} else {
parent.insertAdjacentHTML("beforeend", String(child));
}
return parent.lastElementChild || parent.lastChild;
}
function remove(child) {
return child.parentNode.removeChild(child);
}
function setStyle(element, key, value) {
element.style[key] = value;
return element;
}
function setStyles(element, styles) {
for (const key in styles) {
setStyle(element, key, styles[key]);
}
return element;
}
function getStyle(element, key, numberType = true) {
const value = window.getComputedStyle(element, null).getPropertyValue(key);
return numberType ? Number.parseFloat(value) : value;
}
function siblings(target) {
return Array.from(target.parentElement.children).filter((item) => item !== target);
}
function inverseClass(target, className2) {
siblings(target).forEach((item) => removeClass(item, className2));
addClass(target, className2);
}
function tooltip(target, msg, pos = "top") {
if (isMobile)
return;
target.setAttribute("aria-label", msg);
addClass(target, "hint--rounded");
addClass(target, `hint--${pos}`);
}
function isInViewport(el, offset = 0) {
const rect = el.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
const vertInView = rect.top - offset <= windowHeight && rect.top + rect.height + offset >= 0;
const horInView = rect.left - offset <= windowWidth + offset && rect.left + rect.width + offset >= 0;
return vertInView && horInView;
}
function includeFromEvent(event, target) {
return getComposedPath(event).includes(target);
}
function replaceElement(newChild, oldChild) {
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
}
function createElement(tag) {
return document.createElement(tag);
}
function getIcon(key = "", html2 = "") {
const icon = createElement("i");
addClass(icon, "art-icon");
addClass(icon, `art-icon-${key}`);
append(icon, html2);
return icon;
}
function supportsFlex() {
const div = document.createElement("div");
div.style.display = "flex";
return div.style.display === "flex";
}
function getRect(el) {
return el.getBoundingClientRect();
}
function getComposedPath(event) {
if (event.composedPath)
return event.composedPath();
const path = [];
let node = event.target;
while (node) {
path.push(node);
node = node.parentNode;
}
if (!path.includes(window) && window !== void 0) {
path.push(window);
}
return path;
}
function getSafeAreaInsets() {
const div = document.createElement("div");
div.style.cssText = "position:fixed;top:env(safe-area-inset-top,0px);right:env(safe-area-inset-right,0px);bottom:env(safe-area-inset-bottom,0px);left:env(safe-area-inset-left,0px);pointer-events:none;visibility:hidden;";
document.body.appendChild(div);
const style = getComputedStyle(div);
const insets = {
top: Number.parseFloat(style.top) || 0,
right: Number.parseFloat(style.right) || 0,
bottom: Number.parseFloat(style.bottom) || 0,
left: Number.parseFloat(style.left) || 0
};
div.remove();
return insets;
}
class ArtPlayerError extends Error {
constructor(message, context) {
super(message);
@@ -3444,7 +3472,12 @@ function resolveOption(input, defaults2) {
merged.container = input.container;
return validator(merged, scheme);
}
function resolveRuntimeOption(input, defaults2) {
return resolveOption(input, defaults2);
}
function airplayMix(art) {
if (isClosing(art))
return;
const {
i18n,
notice,
@@ -3452,8 +3485,16 @@ function airplayMix(art) {
template: { $video }
} = art;
let available = true;
if (window.WebKitPlaybackTargetAvailabilityEvent && $video.webkitShowPlaybackTargetPicker) {
const supported = window.WebKitPlaybackTargetAvailabilityEvent;
if (isClosing(art))
return;
const picker = supported && $video.webkitShowPlaybackTargetPicker;
if (isClosing(art))
return;
if (picker) {
proxy($video, "webkitplaybacktargetavailabilitychanged", (event) => {
if (isClosing(art))
return;
switch (event.availability) {
case "available":
available = true;
@@ -3466,13 +3507,23 @@ function airplayMix(art) {
} else {
available = false;
}
if (isClosing(art))
return;
def(art, "airplay", {
value() {
if (isClosing(art))
return;
if (available) {
$video.webkitShowPlaybackTargetPicker();
art.emit("airplay");
const picker2 = $video.webkitShowPlaybackTargetPicker;
if (isClosing(art))
return;
Reflect.apply(picker2, $video, []);
if (!isClosing(art))
art.emit("airplay");
} else {
notice.show = i18n.get("AirPlay Not Available");
const message = i18n.get("AirPlay Not Available");
if (!isClosing(art))
notice.show = message;
}
}
});
@@ -3534,9 +3585,10 @@ function attrMix(art) {
} = art;
def(art, "attr", {
value(key, value) {
const target = $video;
if (value === void 0)
return $video[key];
$video[key] = value;
return target[key];
target[key] = value;
}
});
}
@@ -4678,41 +4730,93 @@ function miniMix(art) {
def(art, "mini", mini(art));
}
function optionInit(art) {
if (isClosing(art))
return;
const {
option,
storage,
template: { $video, $poster }
} = art;
for (const key in option.moreVideoAttr) {
art.attr(key, option.moreVideoAttr[key]);
if (isClosing(art))
return;
const value = option.moreVideoAttr[key];
if (isClosing(art))
return;
art.attr(key, value);
}
if (isClosing(art))
return;
if (option.muted) {
art.muted = option.muted;
const value = option.muted;
if (isClosing(art))
return;
art.muted = value;
}
if (isClosing(art))
return;
if (option.volume) {
$video.volume = clamp(option.volume, 0, 1);
const value = clamp(option.volume, 0, 1);
if (isClosing(art))
return;
$video.volume = value;
}
if (isClosing(art))
return;
const volumeStorage = storage.get("volume");
if (isClosing(art))
return;
if (typeof volumeStorage === "number") {
$video.volume = clamp(volumeStorage, 0, 1);
}
if (isClosing(art))
return;
if (option.poster) {
setStyle($poster, "backgroundImage", `url(${option.poster})`);
const value = `url(${option.poster})`;
if (isClosing(art))
return;
setStyle($poster, "backgroundImage", value);
}
if (isClosing(art))
return;
if (option.autoplay) {
$video.autoplay = option.autoplay;
const value = option.autoplay;
if (isClosing(art))
return;
$video.autoplay = value;
}
if (isClosing(art))
return;
if (option.playsInline) {
if (isClosing(art))
return;
$video.playsInline = true;
if (isClosing(art))
return;
$video["webkit-playsinline"] = true;
}
if (isClosing(art))
return;
if (option.theme) {
option.cssVar["--art-theme"] = option.theme;
const styles = option.cssVar;
const theme = option.theme;
if (isClosing(art))
return;
styles["--art-theme"] = theme;
}
for (const key in option.cssVar) {
art.cssVar(key, option.cssVar[key]);
if (isClosing(art))
return;
const value = option.cssVar[key];
if (isClosing(art))
return;
art.cssVar(key, value);
}
art.url = option.url;
if (isClosing(art))
return;
const url = option.url;
if (!isClosing(art))
art.url = url;
}
function pauseMix(art) {
const {
@@ -5614,43 +5718,52 @@ function volumeMix(art) {
}
});
}
const installers = [
attrMix,
playMix,
pauseMix,
toggleMix,
seekMix,
volumeMix,
currentTimeMix,
durationMix,
switchMix,
playbackRateMix,
aspectRatioMix,
screenshotMix,
fullscreenMix,
fullscreenWebMix,
pipMix,
loadedMix,
playedMix,
playingMix,
autoSizeMix,
rectMix,
flipMix,
miniMix,
posterMix,
autoHeightMix,
cssVarMix,
themeMix,
typeMix,
stateMix,
subtitleOffsetMix,
airplayMix,
qualityMix,
thumbnailsMix,
eventInit,
optionInit
];
class Player {
constructor(art) {
if (isClosing(art))
return;
urlMix(art);
attrMix(art);
playMix(art);
pauseMix(art);
toggleMix(art);
seekMix(art);
volumeMix(art);
currentTimeMix(art);
durationMix(art);
switchMix(art);
playbackRateMix(art);
aspectRatioMix(art);
screenshotMix(art);
fullscreenMix(art);
fullscreenWebMix(art);
pipMix(art);
loadedMix(art);
playedMix(art);
playingMix(art);
autoSizeMix(art);
rectMix(art);
flipMix(art);
miniMix(art);
posterMix(art);
autoHeightMix(art);
cssVarMix(art);
themeMix(art);
typeMix(art);
stateMix(art);
subtitleOffsetMix(art);
airplayMix(art);
qualityMix(art);
thumbnailsMix(art);
eventInit(art);
optionInit(art);
for (const install of installers) {
if (isClosing(art))
return;
install(art);
}
}
}
const owners$2 = /* @__PURE__ */ new WeakMap();
@@ -6136,9 +6249,8 @@ function installBuiltins(registry, option) {
registry.add(lock);
if (!isClosing(registry.art) && option.autoPlayback && !option.isLive)
registry.add(autoPlayback);
if (!isClosing(registry.art) && option.autoOrientation && isMobile) {
if (!isClosing(registry.art) && option.autoOrientation && isMobile)
registry.add(autoOrientation);
}
if (!isClosing(registry.art) && option.fastForward && isMobile && !option.isLive)
registry.add(fastForward);
}
@@ -7112,7 +7224,7 @@ function createSettingHeader(setting2, item) {
setStyle($item, "height", `${SETTING_ITEM_HEIGHT}px`);
addClass($item, "art-setting-item");
addClass($item, "art-setting-item-back");
const $left = append($item, '<div class="art-setting-item-left"></div>');
const $left = appendElement($item, '<div class="art-setting-item-left"></div>');
const $icon = document.createElement("div");
addClass($icon, "art-setting-item-left-icon");
append($icon, arrowLeft2);
@@ -7153,8 +7265,8 @@ function createSettingItem(setting2, item, isUpdate = false) {
$item.dataset.value = String(item.value || "");
if (!current2())
return;
const $left = append($item, '<div class="art-setting-item-left"></div>');
const $right = append($item, '<div class="art-setting-item-right"></div>');
const $left = appendElement($item, '<div class="art-setting-item-left"></div>');
const $right = appendElement($item, '<div class="art-setting-item-right"></div>');
const $icon = document.createElement("div");
addClass($icon, "art-setting-item-left-icon");
switch (type) {
@@ -7204,8 +7316,8 @@ function createSettingItem(setting2, item, isUpdate = false) {
case "switch": {
const $switch = document.createElement("div");
addClass($switch, "art-setting-item-right-icon");
const $switchOn = append($switch, icons.switchOn);
const $switchOff = append($switch, icons.switchOff);
const $switchOn = appendElement($switch, icons.switchOn);
const $switchOff = appendElement($switch, icons.switchOff);
const initialSwitch = item.switch;
if (!current2())
return;
@@ -7992,8 +8104,10 @@ class Template {
);
this.query = this.query.bind(this);
ownContainer(art, this.$container, captureTemplate(this.$container));
this.$container.dataset.artId = String(art.id);
this.init();
duringTemplateMount(art, this, () => {
this.$container.dataset.artId = String(art.id);
this.init();
});
}
static get html() {
return html;
@@ -8009,6 +8123,8 @@ class Template {
bindNodes(this);
if (option.proxy) {
const video = option.proxy.call(this.art, this.art);
if (isClosing(this.art))
return;
assertProxy(video);
replaceElement(video, this.$video);
video.className = "art-video";
@@ -8097,7 +8213,7 @@ class Artplayer extends Emitter {
throw new Error("Artplayer can only be used in the browser environment");
}
this.id = ++id;
this.option = resolveOption(option, Artplayer.option);
this.option = resolveRuntimeOption(option, Artplayer.option);
this.isLock = false;
this.isReady = false;
this.isFocus = false;
@@ -8107,21 +8223,53 @@ class Artplayer extends Emitter {
beginLifecycle(this);
try {
this.template = new Template(this);
if (getScope(this).closed)
return;
this.events = new Events(this);
this.storage = new Storage(this);
if (getScope(this).closed)
return;
this.storage = new Storage();
if (getScope(this).closed)
return;
this.icons = new Icons(this);
if (getScope(this).closed)
return;
this.i18n = new I18n(this);
if (getScope(this).closed)
return;
this.notice = new Notice(this);
if (getScope(this).closed)
return;
this.player = new Player(this);
if (getScope(this).closed)
return;
this.layers = new Layer(this);
if (getScope(this).closed)
return;
this.controls = new Control(this);
if (getScope(this).closed)
return;
this.contextmenu = new Contextmenu(this);
if (getScope(this).closed)
return;
this.subtitle = new Subtitle(this);
if (getScope(this).closed)
return;
this.info = new Info(this);
if (getScope(this).closed)
return;
this.loading = new Loading(this);
if (getScope(this).closed)
return;
this.hotkey = new Hotkey(this);
if (getScope(this).closed)
return;
this.mask = new Mask(this);
if (getScope(this).closed)
return;
this.setting = new Setting(this);
if (getScope(this).closed)
return;
this.plugins = new Plugins(this);
if (getScope(this).closed)
return;
@@ -8227,20 +8375,7 @@ Artplayer.FULLSCREEN_WEB_IN_BODY = true;
Artplayer.LOG_VERSION = true;
Artplayer.USE_RAF = false;
Artplayer.REMOVE_SRC_WHEN_DESTROY = true;
if (isBrowser) {
window.Artplayer = Artplayer;
setStyleText("artplayer-style", css);
setTimeout(() => {
if (Artplayer.LOG_VERSION) {
console.log(
`%c ArtPlayer %c ${Artplayer.version} %c https://artplayer.org`,
"color: #fff; background: #5f5f5f",
"color: #fff; background: #4bc729",
""
);
}
}, 100);
}
publishBrowserEntry(Artplayer, css);
export {
Artplayer as default
};
@@ -0,0 +1,21 @@
import { setStyleText } from '../style/inject'
import { isBrowser } from '../utils/compatibility'
export default function publishBrowserEntry<Constructor extends { version: string, LOG_VERSION: boolean }>(Artplayer: Constructor, style: string): void {
if (!isBrowser)
return
(window as Window & { Artplayer?: Constructor }).Artplayer = Artplayer
setStyleText('artplayer-style', style)
setTimeout(() => {
if (Artplayer.LOG_VERSION) {
// eslint-disable-next-line no-console
console.log(
`%c ArtPlayer %c ${Artplayer.version} %c https://artplayer.org`,
'color: #fff; background: #5f5f5f',
'color: #fff; background: #4bc729',
'',
)
}
}, 100)
}
@@ -1,4 +1,12 @@
export default {
export interface MediaConfig {
properties: string[]
methods: string[]
events: string[]
prototypes: string[]
}
// Public arrays intentionally remain mutable and retain their historical order.
const config: MediaConfig = {
properties: [
'audioTracks',
'autoplay',
@@ -79,3 +87,5 @@ export default {
'webkitExitFullscreen',
],
}
export default config
+2
View File
@@ -18,6 +18,8 @@ import { checkSelector, renderSelector } from './selector'
export default class Control extends Component<ControlHost> {
declare isHover: boolean
declare timer: number
declare setting?: HTMLDivElement
declare thumbnails?: HTMLDivElement
constructor(art: ControlHost) {
super(art)
@@ -7,7 +7,8 @@ export interface ProgressPositionHost {
isRotate: boolean
top: number
height: number
seek: number | string
get seek(): undefined
set seek(value: number | string)
emit: (name: 'setBar', kind: 'played', percentage: number, event: ProgressEvent) => unknown
}
+2 -2
View File
@@ -37,7 +37,7 @@ export function checkSelector(target?: SelectorItem): void {
const item = target.$control_option![index]!
item.default = item === target
if (item.default)
inverseClass(item.$control_item, 'art-current')
inverseClass(item.$control_item!, 'art-current')
}
}
@@ -69,7 +69,7 @@ export function renderSelector(art: ControlHost, check: (target?: SelectorItem)
if (scope.closed)
return
const path = getComposedPath(event)
const item = option.selector!.find(item => path.includes(item.$control_item))
const item = option.selector!.find(item => path.includes(item.$control_item!))
if (!item)
return
const current = ++generation
+6 -4
View File
@@ -1,5 +1,6 @@
import type { SubscriptionHost } from '../component/resources'
import type { ComponentHost, EntryInput, EntryOption } from '../component/types'
import type { PipProperty } from '../display/types'
export interface UIEvents {
'mousemove': [MouseEvent]
@@ -27,11 +28,11 @@ export interface UIEvents {
'resize': []
}
export interface QualityItem { html: string, url: string, default?: boolean }
export interface QualityItem { html: string | HTMLElement, url: string, default?: boolean }
type IconName = 'play' | 'pause' | 'volume' | 'volumeClose' | 'fullscreenOn' | 'fullscreenOff' | 'fullscreenWebOn' | 'fullscreenWebOff' | 'pip' | 'setting' | 'screenshot' | 'airplay'
export interface ControlHost extends ComponentHost, SubscriptionHost<UIEvents> {
export interface ControlHost extends ComponentHost, SubscriptionHost<UIEvents>, PipProperty {
template: {
$player: HTMLDivElement
$bottom: HTMLDivElement
@@ -67,10 +68,11 @@ export interface ControlHost extends ComponentHost, SubscriptionHost<UIEvents> {
loaded: number
top: number
height: number
fullscreen: boolean
fullscreen?: boolean
fullscreenWeb: boolean
pip: boolean
get seek(): undefined
set seek(second: number)
get quality(): undefined
set quality(value: QualityItem[])
play: () => unknown
pause: () => unknown
@@ -1,11 +1,12 @@
import type { SubscriptionHost } from '../component/resources'
import type { NoticeSink } from '../notice'
export interface OrientationHost extends SubscriptionHost<{ fullscreenWeb: [boolean], fullscreen: [boolean] }> {
constructor: { AUTO_ORIENTATION_TIME?: number }
template: { $player: HTMLElement, $video: { readonly videoWidth: number, readonly videoHeight: number } }
fullscreenWeb: boolean
isRotate: boolean
notice: { set show(value: unknown) }
notice: NoticeSink
emit: (name: 'resize') => unknown
}
+9 -9
View File
@@ -1,23 +1,23 @@
import type { SubscriptionHost } from '../component/resources'
import type { CoreEmission } from '../events/core-types'
import type { OptionalMediaCapabilities } from '../media/types'
import type { NoticeSink } from '../notice'
export interface WebFullscreenHost {
export interface WebFullscreenHost extends CoreEmission<'fullscreenWeb' | 'resize'> {
constructor: { FULLSCREEN_WEB_IN_BODY: boolean }
template: { $container: HTMLElement, $player: HTMLElement }
state: string
emit: (name: 'fullscreenWeb' | 'resize', ...args: unknown[]) => unknown
}
export interface WebFullscreenProperty {
fullscreenWeb: boolean
}
export interface FullscreenHost {
export interface FullscreenHost extends CoreEmission<'fullscreen' | 'fullscreenError' | 'resize'> {
template: { $player: HTMLElement, $video: HTMLElement & OptionalMediaCapabilities }
state: string
notice: { set show(value: unknown) }
notice: NoticeSink
i18n: { get: (key: string) => string }
emit: (name: 'fullscreen' | 'fullscreenError' | 'resize', ...args: unknown[]) => unknown
once: (name: 'video:loadedmetadata', listener: () => void) => unknown
on: (name: 'document:webkitfullscreenchange', listener: () => void) => unknown
off: (name: 'video:loadedmetadata' | 'document:webkitfullscreenchange', listener: () => void) => unknown
@@ -37,7 +37,7 @@ export interface NativeFullscreenAdapter {
export interface PipHost {
template: { $video: HTMLElement & OptionalMediaCapabilities & { disablePictureInPicture?: boolean } }
state: string
notice: { set show(value: unknown) }
notice: NoticeSink
i18n: { get: (key: string) => string }
emit: (name: 'pip', value: boolean) => unknown
}
@@ -48,9 +48,9 @@ export interface PipProperty {
}
export interface MiniEvents {
'video:playing': []
'video:pause': []
'video:timeupdate': []
'video:playing': [Event]
'video:pause': [Event]
'video:timeupdate': [Event]
'document:mousemove': [MouseEvent]
'document:mouseup': [MouseEvent]
}
+22
View File
@@ -0,0 +1,22 @@
interface EventPathSource {
target: EventTarget | null
composedPath?: () => EventTarget[]
}
export function getComposedPath(event: EventPathSource): EventTarget[] {
if (event.composedPath)
return event.composedPath()
const path: EventTarget[] = []
let node = event.target
while (node) {
path.push(node)
node = (node as EventTarget & { parentNode?: EventTarget | null }).parentNode ?? null
}
if (typeof window !== 'undefined' && !path.includes(window))
path.push(window)
return path
}
export function includeFromEvent(event: EventPathSource, target: EventTarget | null | undefined): boolean {
return getComposedPath(event).includes(target as EventTarget)
}
+37
View File
@@ -0,0 +1,37 @@
export function supportsFlex(): boolean {
const div = document.createElement('div')
div.style.display = 'flex'
return div.style.display === 'flex'
}
export function getRect(el: Pick<Element, 'getBoundingClientRect'>): DOMRect {
return el.getBoundingClientRect()
}
export function isInViewport(el: Pick<Element, 'getBoundingClientRect'>, offset = 0): boolean {
const rect = el.getBoundingClientRect()
const windowHeight = window.innerHeight || document.documentElement.clientHeight
const windowWidth = window.innerWidth || document.documentElement.clientWidth
const vertInView = rect.top - offset <= windowHeight && rect.top + rect.height + offset >= 0
const horInView = rect.left - offset <= windowWidth + offset && rect.left + rect.width + offset >= 0
return vertInView && horInView
}
export function getSafeAreaInsets(): { top: number, right: number, bottom: number, left: number } {
const div = document.createElement('div')
div.style.cssText
= 'position:fixed;top:env(safe-area-inset-top,0px);right:env(safe-area-inset-right,0px);bottom:env(safe-area-inset-bottom,0px);left:env(safe-area-inset-left,0px);pointer-events:none;visibility:hidden;'
try {
document.body.appendChild(div)
const style = getComputedStyle(div)
return {
top: Number.parseFloat(style.top) || 0,
right: Number.parseFloat(style.right) || 0,
bottom: Number.parseFloat(style.bottom) || 0,
left: Number.parseFloat(style.left) || 0,
}
}
finally {
div.remove()
}
}
@@ -0,0 +1,18 @@
import { isMobile } from '../utils/compatibility'
import { addClass, append, createElement } from './tree'
export function tooltip(target: Element, msg: string | number, pos = 'top'): void {
if (isMobile)
return
target.setAttribute('aria-label', msg as string)
addClass(target, 'hint--rounded')
addClass(target, `hint--${pos}`)
}
export function getIcon(key = '', html: string | HTMLElement = ''): HTMLElement {
const icon = createElement('i')
addClass(icon, 'art-icon')
addClass(icon, `art-icon-${key}`)
append(icon, html)
return icon
}
+22
View File
@@ -0,0 +1,22 @@
interface StyledElement { style: CSSStyleDeclaration }
export function setStyle<T extends StyledElement>(element: T, key: PropertyKey, value: unknown): T {
// Preserve bracket assignment, including native conversion and thrown setters.
const style = element.style as unknown as Record<PropertyKey, unknown>
style[key] = value
return element
}
export function setStyles<T extends StyledElement>(element: T, styles: object): T {
for (const key in styles)
setStyle(element, key, (styles as Record<string, unknown>)[key])
return element
}
export function getStyle(element: Element, key: string, numberType?: true): number
export function getStyle(element: Element, key: string, numberType: false): string
export function getStyle(element: Element, key: string, numberType: boolean): number | string
export function getStyle(element: Element, key: string, numberType = true): number | string {
const value = window.getComputedStyle(element, null).getPropertyValue(key)
return numberType ? Number.parseFloat(value) : value
}
+54
View File
@@ -0,0 +1,54 @@
export function query<T extends Element = Element>(selector: string, parent: ParentNode = document): T | null {
return parent.querySelector<T>(selector)
}
export function queryAll<T extends Element = Element>(selector: string, parent: ParentNode = document): T[] {
return Array.from(parent.querySelectorAll<T>(selector))
}
export function addClass(target: Pick<Element, 'classList'>, className: string): void {
return target.classList.add(className)
}
export function removeClass(target: Pick<Element, 'classList'>, className: string): void {
return target.classList.remove(className)
}
export function hasClass(target: Pick<Element, 'classList'>, className: string): boolean {
return target.classList.contains(className)
}
export function append(parent: Element, child: unknown): Element | ChildNode | null {
if (child instanceof Element) {
parent.appendChild(child)
}
else {
parent.insertAdjacentHTML('beforeend', String(child))
}
return parent.lastElementChild || parent.lastChild
}
export function remove<T extends Node>(child: T): T {
// Detached nodes retain their historical failure rather than becoming no-ops.
return child.parentNode!.removeChild(child)
}
export function siblings(target: Element): Element[] {
return Array.from(target.parentElement!.children).filter(item => item !== target)
}
export function inverseClass(target: Element, className: string): void {
siblings(target).forEach(item => removeClass(item, className))
addClass(target, className)
}
export function replaceElement<T extends Node>(newChild: T, oldChild: Node): T {
oldChild.parentNode!.replaceChild(newChild, oldChild)
return newChild
}
export function createElement<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElementTagNameMap[K]
export function createElement(tag: string): HTMLElement
export function createElement(tag: string): HTMLElement {
return document.createElement(tag)
}
@@ -0,0 +1,30 @@
import type { UIEvents } from '../control/types'
// The emitter stays open to extension names. Known native events retain their
// payloads so subsystem subscriptions are checked against the assembled player.
export interface CoreEvents extends UIEvents, Record<PropertyKey, unknown[]> {
[name: `video:${string}`]: [event: Event]
'ready': []
'error': [error: unknown, attempt: number]
'fullscreenError': [error: unknown]
'destroy': []
'restart': [string]
'focus': [Event]
'lock': [boolean]
'view': [boolean]
'subtitleOffset': [number]
'seek': [time: number, requestedTime: number | string]
'document:click': [Event]
'document:contextmenu': [Event]
'document:keydown': [KeyboardEvent]
'document:touchmove': [Event]
'document:touchend': [Event]
'document:touchcancel': [Event]
'window:resize': [Event]
'window:orientationchange': [Event]
'window:scroll': [Event]
}
export interface CoreEmission<Names extends keyof CoreEvents = keyof CoreEvents> {
emit: <Name extends Names>(name: Name, ...args: [...CoreEvents[Name]]) => unknown
}
+1 -1
View File
@@ -5,6 +5,6 @@ export default function moveInit(art: MoveHost, events: Pick<PointerRegistry, 'p
const { $player } = art.template
events.proxy($player, 'mousemove', (event) => {
if (!isClosing(art))
art.emit('mousemove', event)
art.emit('mousemove', event as MouseEvent)
})
}
@@ -1,4 +1,5 @@
import type { SubscriptionHost } from '../component/resources'
import type { NoticeSink } from '../notice'
export interface ResizeEvents {
'resize': []
@@ -13,7 +14,7 @@ export interface ResizeHost extends SubscriptionHost<ResizeEvents> {
aspectRatio: string
state: string
autoSize: () => unknown
notice: { show: string }
notice: NoticeSink
emit: (name: 'resize') => unknown
}
+8 -2
View File
@@ -1,10 +1,14 @@
import { def, getIcon } from '../utils'
import defaults from './defaults'
export default class Icons {
// eslint-disable-next-line ts/no-unsafe-declaration-merging -- The constructor defines a getter for every default key without class fields.
interface Icons extends Record<keyof typeof defaults, HTMLElement> {}
// eslint-disable-next-line ts/no-unsafe-declaration-merging -- The merged mapped interface describes the getters installed below.
class Icons {
readonly [key: string]: HTMLElement
constructor(art: { option: { icons: Record<string, string | HTMLElement> } }) {
constructor(art: { option: { icons: Readonly<Record<string, string | HTMLElement | undefined>> } }) {
const icons: Record<string, string | HTMLElement | undefined> = { ...defaults, ...art.option.icons }
for (const key in icons) {
def(this, key, {
@@ -13,3 +17,5 @@ export default class Icons {
}
}
}
export default Icons
@@ -1,5 +1,11 @@
import type { CoreEvents } from './events/core-types'
import type { RuntimeOption } from './option/runtime'
import type { OptionInput } from './option/types'
import type { PlayerProperties } from './player/properties'
import type { PlayerTemplate } from './template/types'
import validator from 'option-validator'
import { version } from '../package.json'
import publishBrowserEntry from './bootstrap/browser'
import config from './config'
import Contextmenu from './contextmenu'
import Control from './control'
@@ -14,7 +20,7 @@ import Loading from './loading'
import Mask from './mask'
import Notice from './notice'
import createDefaults from './option/defaults'
import resolveOption from './option/resolve'
import resolveRuntimeOption from './option/runtime'
import Player from './player'
import Plugins from './plugins'
import scheme from './scheme'
@@ -27,9 +33,76 @@ import * as utils from './utils'
import Emitter from './utils/emitter'
let id = 0
const instances = []
export default class Artplayer extends Emitter {
constructor(option, readyCallback) {
const instances: Artplayer[] = []
// eslint-disable-next-line ts/no-unsafe-declaration-merging -- Player installs these descriptors; emitting fields would shadow them.
interface Artplayer extends PlayerProperties {
constructor: typeof Artplayer
}
// eslint-disable-next-line ts/no-unsafe-declaration-merging -- The merged interface describes installed descriptors and the inherited constructor.
class Artplayer extends Emitter<CoreEvents> {
declare static STYLE: string
declare static DEBUG: boolean
declare static CONTEXTMENU: boolean
declare static NOTICE_TIME: number
declare static SETTING_WIDTH: number
declare static SETTING_ITEM_WIDTH: number
declare static SETTING_ITEM_HEIGHT: number
declare static RESIZE_TIME: number
declare static SCROLL_TIME: number
declare static SCROLL_GAP: number
declare static AUTO_PLAYBACK_MAX: number
declare static AUTO_PLAYBACK_MIN: number
declare static AUTO_PLAYBACK_TIMEOUT: number
declare static RECONNECT_TIME_MAX: number
declare static RECONNECT_SLEEP_TIME: number
declare static CONTROL_HIDE_TIME: number
declare static DBCLICK_TIME: number
declare static DBCLICK_FULLSCREEN: boolean
declare static MOBILE_DBCLICK_PLAY: boolean
declare static MOBILE_CLICK_PLAY: boolean
declare static AUTO_ORIENTATION_TIME: number
declare static INFO_LOOP_TIME: number
declare static FAST_FORWARD_VALUE: number
declare static FAST_FORWARD_TIME: number
declare static TOUCH_MOVE_RATIO: number
declare static VOLUME_STEP: number
declare static SEEK_STEP: number
declare static PLAYBACK_RATE: number[]
declare static ASPECT_RATIO: string[]
declare static FLIP: string[]
declare static FULLSCREEN_WEB_IN_BODY: boolean
declare static LOG_VERSION: boolean
declare static USE_RAF: boolean
declare static REMOVE_SRC_WHEN_DESTROY: boolean
declare id: number
declare option: RuntimeOption<Artplayer>
declare isLock: boolean
declare isReady: boolean
declare isFocus: boolean
declare isInput: boolean
declare isRotate: boolean
declare isDestroy: boolean
declare template: PlayerTemplate<Artplayer>
declare events: Events
declare storage: Storage
declare icons: Icons
declare i18n: I18n
declare notice: Notice
declare player: Player<Artplayer>
declare layers: Layer
declare controls: Control
declare contextmenu: Contextmenu
declare subtitle: Subtitle
declare info: Info
declare loading: Loading
declare hotkey: Hotkey<Artplayer>
declare mask: Mask
declare setting: Setting
declare plugins: Plugins<Artplayer>
constructor(option: OptionInput, readyCallback?: (this: Artplayer, art: Artplayer) => unknown) {
super()
if (!utils.isBrowser) {
@@ -38,7 +111,7 @@ export default class Artplayer extends Emitter {
this.id = ++id
this.option = resolveOption(option, Artplayer.option)
this.option = resolveRuntimeOption<Artplayer>(option, Artplayer.option)
this.isLock = false
this.isReady = false
@@ -49,22 +122,54 @@ export default class Artplayer extends Emitter {
beginLifecycle(this)
try {
this.template = new Template(this)
this.template = new Template<Artplayer>(this) as PlayerTemplate<Artplayer>
if (getScope(this).closed)
return
this.events = new Events(this)
this.storage = new Storage(this)
if (getScope(this).closed)
return
this.storage = new Storage()
if (getScope(this).closed)
return
this.icons = new Icons(this)
if (getScope(this).closed)
return
this.i18n = new I18n(this)
if (getScope(this).closed)
return
this.notice = new Notice(this)
this.player = new Player(this)
if (getScope(this).closed)
return
this.player = new Player<Artplayer>(this)
if (getScope(this).closed)
return
this.layers = new Layer(this)
if (getScope(this).closed)
return
this.controls = new Control(this)
if (getScope(this).closed)
return
this.contextmenu = new Contextmenu(this)
if (getScope(this).closed)
return
this.subtitle = new Subtitle(this)
if (getScope(this).closed)
return
this.info = new Info(this)
if (getScope(this).closed)
return
this.loading = new Loading(this)
this.hotkey = new Hotkey(this)
if (getScope(this).closed)
return
this.hotkey = new Hotkey<Artplayer>(this)
if (getScope(this).closed)
return
this.mask = new Mask(this)
if (getScope(this).closed)
return
this.setting = new Setting(this)
if (getScope(this).closed)
return
this.plugins = new Plugins(this)
if (getScope(this).closed)
@@ -76,7 +181,7 @@ export default class Artplayer extends Emitter {
if (Artplayer.DEBUG) {
// eslint-disable-next-line no-console
const log = msg => console.log(`[ART.${this.id}] -> ${msg}`)
const log = (msg: string) => console.log(`[ART.${this.id}] -> ${msg}`)
log(`Version@${Artplayer.version}`)
for (let index = 0; index < config.events.length; index++) {
this.on(`video:${config.events[index]}`, event => log(`Event@${event.type}`))
@@ -194,19 +299,6 @@ Artplayer.LOG_VERSION = true
Artplayer.USE_RAF = false
Artplayer.REMOVE_SRC_WHEN_DESTROY = true
if (utils.isBrowser) {
window.Artplayer = Artplayer
utils.setStyleText('artplayer-style', style)
publishBrowserEntry(Artplayer, style)
setTimeout(() => {
if (Artplayer.LOG_VERSION) {
// eslint-disable-next-line no-console
console.log(
`%c ArtPlayer %c ${Artplayer.version} %c https://artplayer.org`,
'color: #fff; background: #5f5f5f',
'color: #fff; background: #4bc729',
'',
)
}
}, 100)
}
export default Artplayer
@@ -1,5 +1,6 @@
import type { SubscriptionHost } from '../component/resources'
import type { ProgressPositionHost } from '../control/progress/position'
import type { NoticeSink } from '../notice'
export interface GestureHost extends ProgressPositionHost, SubscriptionHost<{
'document:touchend': [Event]
@@ -12,7 +13,7 @@ export interface GestureHost extends ProgressPositionHost, SubscriptionHost<{
isLock: boolean
width: number
currentTime: number
notice: { show: string }
notice: NoticeSink
}
export interface Drag {
+4 -2
View File
@@ -6,8 +6,10 @@ export interface HotkeyHost extends SubscriptionHost<{ 'document:keydown': [Keyb
template: { $player: HTMLElement }
isFocus: boolean
fullscreenWeb: boolean
backward: number
forward: number
get backward(): undefined
set backward(value: number)
get forward(): undefined
set forward(value: number)
volume: number
toggle: () => unknown
emit: (name: 'hotkey' | 'keydown', event: KeyboardEvent) => unknown
@@ -23,7 +23,7 @@ export interface ClickHost extends FocusHost {
template: { $player: HTMLElement, $video: EventTarget }
isLock: boolean
fullscreen: boolean
fullscreen?: boolean
toggle: () => unknown
emit: (name: 'focus' | 'blur' | 'click' | 'dblclick', event: Event) => unknown
}
@@ -35,5 +35,5 @@ export interface HoverHost {
export interface MoveHost {
template: { $player: HTMLElement }
emit: (name: 'mousemove', event: Event) => unknown
emit: (name: 'mousemove', event: MouseEvent) => unknown
}
+22 -2
View File
@@ -1,10 +1,15 @@
import ResourceScope, { ResourceCleanupError } from './scope'
interface LifecycleTemplate {
$video?: unknown
destroy: (removeHtml: boolean) => void
}
interface LifecycleOwner {
isDestroy: boolean
reset: () => void
emit: (name: 'destroy') => unknown
template?: { $video?: unknown, destroy: (removeHtml: boolean) => void }
template?: LifecycleTemplate
}
interface State {
@@ -13,6 +18,7 @@ interface State {
destroying: boolean
rollback?: () => void
releaseContainer?: () => void
mountingTemplate?: LifecycleTemplate
}
const states = new WeakMap<object, State>()
@@ -48,6 +54,20 @@ export function isClosing(owner: object): boolean {
return state.destroying || state.scope.closed
}
// The proxy callback runs before the public art.template assignment. Keep its
// in-progress template reachable by teardown without changing that public order.
export function duringTemplateMount(owner: object, template: LifecycleTemplate, initialize: () => void): void {
const state = stateOf(owner)
const previous = state.mountingTemplate
state.mountingTemplate = template
try {
initialize()
}
finally {
state.mountingTemplate = previous
}
}
export function ownContainer(owner: object, container: Element, rollback: () => void): void {
const current = containers.get(container)
if (current && current !== owner)
@@ -84,7 +104,7 @@ export function destroyInstance(owner: LifecycleOwner, instances: LifecycleOwner
if (removeSource && owner.template?.$video)
attempt(() => owner.reset())
attempt(() => state.scope.dispose())
attempt(() => owner.template?.destroy(removeHtml))
attempt(() => (owner.template || state.mountingTemplate)?.destroy(removeHtml))
const index = instances.indexOf(owner)
if (index !== -1)
instances.splice(index, 1)
+6 -7
View File
@@ -1,15 +1,13 @@
import type { CoreEmission } from '../../events/core-types'
import type { NoticeSink } from '../../notice'
export type MediaEventName = `video:${string}`
export type MediaListener = (event?: unknown) => void
export interface MediaEvents {
export interface MediaEvents extends CoreEmission<MediaEventName | 'ready' | 'resize' | 'error'> {
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 {
@@ -28,11 +26,12 @@ export interface MediaEventHost extends MediaEvents, MediaUI {
isReady: boolean
readonly playing: boolean
url: string | null
get seek(): undefined
set seek(time: number)
play: () => unknown
option: { url: string, loop: boolean }
i18n: { get: (key: string) => string }
notice: { set show(message: string) }
notice: NoticeSink
constructor: { RECONNECT_TIME_MAX: number, RECONNECT_SLEEP_TIME: number }
}
+2 -1
View File
@@ -1,3 +1,4 @@
import type { NoticeSink } from '../notice'
import type { ResolvedOption } from '../option/types'
import type { PlaybackMethods } from './types'
@@ -7,7 +8,7 @@ export interface MediaHost<Media> {
interface NoticeHost {
i18n: { get: (key: string) => string }
notice: { get show(): unknown, set show(value: string | Error) }
notice: NoticeSink
emit: (name: 'play' | 'pause') => unknown
}
+6 -3
View File
@@ -1,3 +1,6 @@
import type { PipProperty } from '../display/types'
import type { NoticeSink } from '../notice'
export interface Position {
readonly duration: number
get currentTime(): number
@@ -14,13 +17,13 @@ export interface SeekMethods {
}
export interface SeekHost extends Position {
notice: { set show(message: string) }
notice: NoticeSink
emit: (name: 'seek', currentTime: number, requestedTime: number | string) => unknown
}
export interface PlaybackNotice {
i18n: { get: (key: string) => string }
notice: { set show(message: string) }
notice: NoticeSink
}
export interface VolumeHost extends PlaybackNotice {
@@ -30,7 +33,7 @@ export interface VolumeHost extends PlaybackNotice {
}
export type DisplayState = 'mini' | 'pip' | 'fullscreen' | 'fullscreenWeb'
export type DisplayStates = Record<DisplayState, boolean>
export type DisplayStates = Record<'mini' | 'fullscreenWeb', boolean> & PipProperty & { fullscreen?: boolean }
export interface StateProperty {
get state(): DisplayState | 'standard'
+6 -1
View File
@@ -2,6 +2,11 @@ import type { SubscriptionHost } from './component/resources'
import { getScope, isClosing } from './lifecycle/instance'
import { addClass, removeClass } from './utils'
export interface NoticeSink {
get show(): unknown
set show(value: unknown)
}
interface NoticeHost extends SubscriptionHost<{ destroy: [] }> {
constructor: { NOTICE_TIME: number }
template: { $player: HTMLElement, $noticeInner: HTMLElement }
@@ -24,7 +29,7 @@ function cancelNotice(notice: Notice): void {
}
}
export default class Notice {
export default class Notice implements NoticeSink {
declare art: NoticeHost
declare timer: ReturnType<typeof setTimeout> | null
+28
View File
@@ -0,0 +1,28 @@
import type { ContextmenuHost } from '../contextmenu/types'
import type { ControlHost } from '../control/types'
import type { LayerHost } from '../layer'
import type { MediaSurface } from '../media/types'
import type { PluginFactory } from '../plugins/types'
import type { SettingItem } from '../setting/types'
import type { UrlHost } from '../source/types'
import type { OptionInput, ResolvedOption } from './types'
import resolveOption from './resolve'
interface RuntimeCallbacks<Host> {
proxy?: (this: Host, art: Host) => unknown
plugins: PluginFactory<Host>[]
customType: UrlHost<MediaSurface, Host>['option']['customType']
layers: LayerHost['option']['layers']
controls: ControlHost['option']['controls']
contextmenu: ContextmenuHost['option']['contextmenu']
settings: SettingItem[]
}
export type RuntimeOption<Host> = Omit<ResolvedOption, keyof RuntimeCallbacks<Host>> & RuntimeCallbacks<Host>
export default function resolveRuntimeOption<Host>(input: OptionInput, defaults: ResolvedOption): RuntimeOption<Host> {
// The external declaration facade and internal module views describe the same
// caller-owned callbacks. Do not wrap, clone or rebind their runtime identity.
// All non-callback options still derive directly from the validated merge type.
return resolveOption(input, defaults) as unknown as RuntimeOption<Host>
}
@@ -0,0 +1,70 @@
import type { NoticeSink } from '../notice'
import { isClosing } from '../lifecycle/instance'
import { def } from '../utils'
export interface AirplayHost {
template: { $video: EventTarget & { webkitShowPlaybackTargetPicker?: () => void } }
i18n: { get: (key: string) => string }
notice: NoticeSink
proxy: (target: EventTarget, name: string, listener: (event: Event & { availability?: string }) => void) => unknown
emit: (name: 'airplay') => unknown
}
export default function airplayMix(art: AirplayHost): void {
if (isClosing(art))
return
const {
i18n,
notice,
proxy,
template: { $video },
} = art
let available = true
const supported = (window as Window & { WebKitPlaybackTargetAvailabilityEvent?: unknown }).WebKitPlaybackTargetAvailabilityEvent
if (isClosing(art))
return
const picker = supported && $video.webkitShowPlaybackTargetPicker
if (isClosing(art))
return
if (picker) {
proxy($video, 'webkitplaybacktargetavailabilitychanged', (event) => {
if (isClosing(art))
return
switch (event.availability) {
case 'available':
available = true
break
case 'not-available':
available = false
break
}
})
}
else {
available = false
}
if (isClosing(art))
return
def(art, 'airplay', {
value() {
if (isClosing(art))
return
if (available) {
const picker = $video.webkitShowPlaybackTargetPicker
if (isClosing(art))
return
Reflect.apply(picker!, $video, [])
if (!isClosing(art))
art.emit('airplay')
}
else {
const message = i18n.get('AirPlay Not Available')
if (!isClosing(art))
notice.show = message
}
},
})
}
-15
View File
@@ -1,15 +0,0 @@
import { def } from '../utils'
export default function attrMix(art) {
const {
template: { $video },
} = art
def(art, 'attr', {
value(key, value) {
if (value === undefined)
return $video[key]
$video[key] = value
},
})
}
+24
View File
@@ -0,0 +1,24 @@
import { def } from '../utils'
export interface AttributeMethods {
attr: {
(key: PropertyKey): unknown
(key: PropertyKey, value: unknown): unknown
}
}
export default function attrMix(art: { template: { $video: object } }): void {
const {
template: { $video },
} = art
def(art, 'attr', {
value(key: PropertyKey, value?: unknown): unknown {
// attr intentionally exposes arbitrary native/proxy properties, including symbols.
const target = $video as Record<PropertyKey, unknown>
if (value === undefined)
return target[key]
target[key] = value
},
})
}
@@ -1,16 +0,0 @@
import { def } from '../utils'
export default function cssVarMix(art) {
const { $player } = art.template
def(art, 'cssVar', {
value(key, value) {
if (value) {
return $player.style.setProperty(key, value)
}
else {
return getComputedStyle($player).getPropertyValue(key)
}
},
})
}
@@ -0,0 +1,24 @@
import { def } from '../utils'
export interface CssVariableMethods {
cssVar: {
(key: string): string
(key: string, value: unknown): string | void
}
}
export default function cssVarMix(art: { template: { $player: HTMLElement } }): void {
const { $player } = art.template
def(art, 'cssVar', {
value(key: string, value?: unknown): string | void {
if (value) {
// Retain Web IDL conversion and the historical truthy write condition.
return $player.style.setProperty(key, value as string)
}
else {
return getComputedStyle($player).getPropertyValue(key)
}
},
})
}
-75
View File
@@ -1,75 +0,0 @@
import airplayMix from './airplayMix'
import aspectRatioMix from './aspectRatioMix'
import attrMix from './attrMix'
import autoHeightMix from './autoHeightMix'
import autoSizeMix from './autoSizeMix'
import cssVarMix from './cssVarMix'
import currentTimeMix from './currentTimeMix'
import durationMix from './durationMix'
import eventInit from './eventInit'
import flipMix from './flipMix'
import fullscreenMix from './fullscreenMix'
import fullscreenWebMix from './fullscreenWebMix'
import loadedMix from './loadedMix'
import miniMix from './miniMix'
import optionInit from './optionInit'
import pauseMix from './pauseMix'
import pipMix from './pipMix'
import playbackRateMix from './playbackRateMix'
import playedMix from './playedMix'
import playingMix from './playingMix'
import playMix from './playMix'
import posterMix from './posterMix'
import qualityMix from './qualityMix'
import rectMix from './rectMix'
import screenshotMix from './screenshotMix'
import seekMix from './seekMix'
import stateMix from './stateMix'
import subtitleOffsetMix from './subtitleOffsetMix'
import switchMix from './switchMix'
import themeMix from './themeMix'
import thumbnailsMix from './thumbnailsMix'
import toggleMix from './toggleMix'
import typeMix from './typeMix'
import urlMix from './urlMix'
import volumeMix from './volumeMix'
export default class Player {
constructor(art) {
urlMix(art)
attrMix(art)
playMix(art)
pauseMix(art)
toggleMix(art)
seekMix(art)
volumeMix(art)
currentTimeMix(art)
durationMix(art)
switchMix(art)
playbackRateMix(art)
aspectRatioMix(art)
screenshotMix(art)
fullscreenMix(art)
fullscreenWebMix(art)
pipMix(art)
loadedMix(art)
playedMix(art)
playingMix(art)
autoSizeMix(art)
rectMix(art)
flipMix(art)
miniMix(art)
posterMix(art)
autoHeightMix(art)
cssVarMix(art)
themeMix(art)
typeMix(art)
stateMix(art)
subtitleOffsetMix(art)
airplayMix(art)
qualityMix(art)
thumbnailsMix(art)
eventInit(art)
optionInit(art)
}
}
+128
View File
@@ -0,0 +1,128 @@
import type { MediaSurface } from '../media/types'
import type { UrlHost } from '../source/types'
import { isClosing } from '../lifecycle/instance'
import airplayMix from './airplayMix'
import aspectRatioMix from './aspectRatioMix'
import attrMix from './attrMix'
import autoHeightMix from './autoHeightMix'
import autoSizeMix from './autoSizeMix'
import cssVarMix from './cssVarMix'
import currentTimeMix from './currentTimeMix'
import durationMix from './durationMix'
import eventInit from './eventInit'
import flipMix from './flipMix'
import fullscreenMix from './fullscreenMix'
import fullscreenWebMix from './fullscreenWebMix'
import loadedMix from './loadedMix'
import miniMix from './miniMix'
import optionInit from './optionInit'
import pauseMix from './pauseMix'
import pipMix from './pipMix'
import playbackRateMix from './playbackRateMix'
import playedMix from './playedMix'
import playingMix from './playingMix'
import playMix from './playMix'
import posterMix from './posterMix'
import qualityMix from './qualityMix'
import rectMix from './rectMix'
import screenshotMix from './screenshotMix'
import seekMix from './seekMix'
import stateMix from './stateMix'
import subtitleOffsetMix from './subtitleOffsetMix'
import switchMix from './switchMix'
import themeMix from './themeMix'
import thumbnailsMix from './thumbnailsMix'
import toggleMix from './toggleMix'
import typeMix from './typeMix'
import urlMix from './urlMix'
import volumeMix from './volumeMix'
// Installation requirements come from the owning modules. Some properties are
// installed here and consumed only by later callbacks, after construction.
type PlayerDependencies = Parameters<typeof airplayMix>[0]
& Parameters<typeof aspectRatioMix>[0]
& Parameters<typeof attrMix>[0]
& Parameters<typeof autoHeightMix>[0]
& Parameters<typeof autoSizeMix>[0]
& Parameters<typeof cssVarMix>[0]
& Parameters<typeof currentTimeMix>[0]
& Parameters<typeof durationMix>[0]
& Parameters<typeof eventInit>[0]
& Parameters<typeof flipMix>[0]
& Parameters<typeof fullscreenMix>[0]
& Parameters<typeof fullscreenWebMix>[0]
& Parameters<typeof loadedMix>[0]
& Parameters<typeof miniMix>[0]
& Parameters<typeof optionInit>[0]
& Parameters<typeof pauseMix>[0]
& Parameters<typeof pipMix>[0]
& Parameters<typeof playbackRateMix>[0]
& Parameters<typeof playedMix>[0]
& Parameters<typeof playingMix>[0]
& Parameters<typeof playMix>[0]
& Parameters<typeof posterMix>[0]
& Parameters<typeof qualityMix>[0]
& Parameters<typeof rectMix>[0]
& Parameters<typeof screenshotMix>[0]
& Parameters<typeof seekMix>[0]
& Parameters<typeof stateMix>[0]
& Parameters<typeof subtitleOffsetMix>[0]
& Parameters<typeof switchMix>[0]
& Parameters<typeof themeMix>[0]
& Parameters<typeof thumbnailsMix>[0]
& Parameters<typeof toggleMix>[0]
& Parameters<typeof typeMix>[0]
& Parameters<typeof volumeMix>[0]
// The order is observable through descriptors, subscriptions and option writes.
const installers: readonly ((art: PlayerDependencies) => void)[] = [
attrMix,
playMix,
pauseMix,
toggleMix,
seekMix,
volumeMix,
currentTimeMix,
durationMix,
switchMix,
playbackRateMix,
aspectRatioMix,
screenshotMix,
fullscreenMix,
fullscreenWebMix,
pipMix,
loadedMix,
playedMix,
playingMix,
autoSizeMix,
rectMix,
flipMix,
miniMix,
posterMix,
autoHeightMix,
cssVarMix,
themeMix,
typeMix,
stateMix,
subtitleOffsetMix,
airplayMix,
qualityMix,
thumbnailsMix,
eventInit,
optionInit,
]
export type PlayerHost<Host> = UrlHost<MediaSurface, Host> & PlayerDependencies
export default class Player<Host extends PlayerHost<Host>> {
constructor(art: Host) {
if (isClosing(art))
return
urlMix<MediaSurface, Host>(art)
for (const install of installers) {
if (isClosing(art))
return
install(art)
}
}
}
+117
View File
@@ -0,0 +1,117 @@
import type { ResolvedOption } from '../option/types'
import type { AttributeMethods } from './attrMix'
import type { CssVariableMethods } from './cssVarMix'
import { isClosing } from '../lifecycle/instance'
import { clamp, setStyle } from '../utils'
export interface InitialOptionHost extends AttributeMethods, CssVariableMethods {
option: Pick<ResolvedOption, 'moreVideoAttr' | 'muted' | 'volume' | 'poster' | 'autoplay' | 'playsInline' | 'theme' | 'cssVar' | 'url'>
storage: { get: (key: string) => unknown }
template: {
$video: Pick<HTMLMediaElement, 'volume'> & { 'autoplay'?: boolean, 'playsInline'?: boolean, 'webkit-playsinline'?: boolean }
$poster: HTMLElement
}
muted: boolean
get url(): string | null
set url(value: string)
}
export default function optionInit(art: InitialOptionHost): void {
if (isClosing(art))
return
const {
option,
storage,
template: { $video, $poster },
} = art
for (const key in option.moreVideoAttr) {
if (isClosing(art))
return
const value = (option.moreVideoAttr as Record<string, unknown>)[key]
if (isClosing(art))
return
art.attr(key, value)
}
if (isClosing(art))
return
if (option.muted) {
const value = option.muted
if (isClosing(art))
return
art.muted = value
}
if (isClosing(art))
return
if (option.volume) {
const value = clamp(option.volume, 0, 1)
if (isClosing(art))
return
$video.volume = value
}
if (isClosing(art))
return
const volumeStorage = storage.get('volume')
if (isClosing(art))
return
if (typeof volumeStorage === 'number') {
$video.volume = clamp(volumeStorage, 0, 1)
}
if (isClosing(art))
return
if (option.poster) {
const value = `url(${option.poster})`
if (isClosing(art))
return
setStyle($poster, 'backgroundImage', value)
}
if (isClosing(art))
return
if (option.autoplay) {
const value = option.autoplay
if (isClosing(art))
return
$video.autoplay = value
}
if (isClosing(art))
return
if (option.playsInline) {
if (isClosing(art))
return
$video.playsInline = true
if (isClosing(art))
return
$video['webkit-playsinline'] = true
}
if (isClosing(art))
return
if (option.theme) {
const styles = option.cssVar
const theme = option.theme
if (isClosing(art))
return
styles['--art-theme'] = theme
}
for (const key in option.cssVar) {
if (isClosing(art))
return
const value = (option.cssVar as Record<string, unknown>)[key]
if (isClosing(art))
return
art.cssVar(key, value)
}
if (isClosing(art))
return
const url = option.url
if (!isClosing(art))
art.url = url
}
@@ -1,6 +1,6 @@
import { def, setStyle } from '../utils'
export default function posterMix(art) {
export default function posterMix(art: { template: { $poster: HTMLElement } }): void {
const {
template: { $poster },
} = art
@@ -8,13 +8,13 @@ export default function posterMix(art) {
def(art, 'poster', {
get: () => {
try {
return $poster.style.backgroundImage.match(/"(.*)"/)[1]
return $poster.style.backgroundImage.match(/"(.*)"/)![1]!
}
catch {
return ''
}
},
set(url) {
set(url: string) {
setStyle($poster, 'backgroundImage', `url(${url})`)
},
})
@@ -0,0 +1,43 @@
import type { Thumbnails } from '../../types/option'
import type { QualityItem } from '../control/types'
import type { PipProperty } from '../display/types'
import type { RectState } from '../media/hosts'
import type { Position, SeekMethods, StateProperty } from '../media/playback'
import type { SwitchMethods } from '../source/types'
import type { AttributeMethods } from './attrMix'
import type { CssVariableMethods } from './cssVarMix'
// Runtime descriptors installed by Player, distinct from each installer's inputs.
// Declaring these on the entry must never emit class fields before installation.
export interface PlayerProperties extends AttributeMethods, CssVariableMethods, Position, SeekMethods, SwitchMethods, RectState, PipProperty, StateProperty {
get url(): string | null
set url(value: string)
play: () => Promise<unknown>
pause: () => unknown
toggle: () => unknown
volume: number
muted: boolean
playbackRate: number
aspectRatio: string
flip: string
fullscreen?: boolean
fullscreenWeb: boolean
mini: boolean
readonly loaded: number
readonly loadedTime: number
readonly played: number
readonly playing: boolean
autoSize: () => void
autoHeight: () => void
poster: string
theme: string
type: string
subtitleOffset: number
airplay: () => void
get quality(): undefined
set quality(value: QualityItem[])
thumbnails: Thumbnails
getDataURL: () => Promise<string>
getBlobUrl: () => Promise<string>
screenshot: (name?: string) => Promise<string>
}
+5 -4
View File
@@ -1,12 +1,13 @@
import type { ComponentHost, EntryOption } from '../component/types'
import type { QualityItem } from '../control/types'
import type { ComponentHost } from '../component/types'
import type { ControlOption, QualityItem } from '../control/types'
import type { NoticeSink } from '../notice'
import { captureSelection } from '../component/selection'
import { isClosing } from '../lifecycle/instance'
import { def } from '../utils'
export interface QualityHost extends ComponentHost {
controls: { update: (option: EntryOption<QualityHost>) => unknown }
notice: { set show(message: string) }
controls: { update: (option: ControlOption) => unknown }
notice: NoticeSink
i18n: { get: (key: string) => string }
switchQuality: (url: string) => Promise<unknown>
}
@@ -1,4 +1,5 @@
import type { CaptureMedia } from '../capture/frame'
import type { NoticeSink } from '../notice'
import { captureBlobUrl, drawFrame } from '../capture/frame'
import { isClosing } from '../lifecycle/instance'
import { captureSource } from '../source/operation'
@@ -6,7 +7,7 @@ import { def, download, secondToTime } from '../utils'
export interface ScreenshotHost {
template: { $video: CaptureMedia }
notice: { set show(value: unknown) }
notice: NoticeSink
getDataURL: () => Promise<string>
emit: (name: 'screenshot', dataUri: string) => unknown
}
@@ -1,11 +1,12 @@
import type { CssVariableMethods } from './cssVarMix'
import { def } from '../utils'
export default function themeMix(art) {
export default function themeMix(art: CssVariableMethods): void {
def(art, 'theme', {
get() {
return art.cssVar('--art-theme')
},
set(theme) {
set(theme: string) {
art.cssVar('--art-theme', theme)
},
})
@@ -1,11 +1,11 @@
import { def } from '../utils'
export default function typeMix(art) {
export default function typeMix(art: { option: { type: string } }): void {
def(art, 'type', {
get() {
return art.option.type
},
set(type) {
set(type: string) {
art.option.type = type
},
})
@@ -24,7 +24,8 @@ export interface ResumePromptHost extends ComponentHost, SubscriptionHost<Prompt
i18n: { get: (key: string) => string }
storage: PlaybackStorage
option: { id?: string, url: string }
seek: number
get seek(): undefined
set seek(value: number)
play: () => unknown
}
@@ -5,7 +5,7 @@ import { isClosing } from '../../lifecycle/instance'
export type PlaybackTimes = Record<string, number>
export interface PlaybackStorage {
get: (key: 'times') => PlaybackTimes | null | undefined
get: (key: 'times') => unknown
set: (key: 'times', value: PlaybackTimes) => void
del: (key: 'times') => void
}
@@ -19,7 +19,9 @@ export interface PlaybackRecordHost extends SubscriptionHost<{ 'video:timeupdate
}
export function readTimes(storage: PlaybackStorage): PlaybackTimes {
return storage.get('times') || {}
// Historical storage is unvalidated JSON. Keep its truthy fallback and localize
// the legacy record assumption to this consumer, rather than all Storage reads.
return (storage.get('times') || {}) as PlaybackTimes
}
export function installPlaybackRecords(art: PlaybackRecordHost): void {
+14 -10
View File
@@ -1,4 +1,4 @@
import type { BuiltinRegistry, PluginFactory, PluginHost } from './types'
import type { BuiltinRegistry, PluginHost } from './types'
import { isClosing } from '../lifecycle/instance'
import { isMobile } from '../utils/compatibility'
import autoOrientation from './autoOrientation'
@@ -7,18 +7,22 @@ import fastForward from './fastForward'
import lock from './lock'
import miniProgressBar from './miniProgressBar'
export default function installBuiltins<Host extends PluginHost<Host>>(registry: BuiltinRegistry<Host>, option: PluginHost<Host>['option']): void {
export type BuiltinHost = Parameters<typeof miniProgressBar>[0]
& Parameters<typeof lock>[0]
& Parameters<typeof autoPlayback>[0]
& Parameters<typeof autoOrientation>[0]
& Parameters<typeof fastForward>[0]
export default function installBuiltins<Host extends PluginHost<Host> & BuiltinHost>(registry: BuiltinRegistry<Host>, option: PluginHost<Host>['option']): void {
// Read each condition at its original turn; earlier plugins may change options.
if (!isClosing(registry.art) && option.miniProgressBar && !option.isLive)
registry.add(miniProgressBar as unknown as PluginFactory<Host>)
registry.add(miniProgressBar)
if (!isClosing(registry.art) && option.lock && isMobile)
registry.add(lock as unknown as PluginFactory<Host>)
registry.add(lock)
if (!isClosing(registry.art) && option.autoPlayback && !option.isLive)
registry.add(autoPlayback as unknown as PluginFactory<Host>)
if (!isClosing(registry.art) && option.autoOrientation && isMobile) {
// The JS constructor supplies the complete host before builtin installation.
registry.add(autoOrientation as unknown as PluginFactory<Host>)
}
registry.add(autoPlayback)
if (!isClosing(registry.art) && option.autoOrientation && isMobile)
registry.add(autoOrientation)
if (!isClosing(registry.art) && option.fastForward && isMobile && !option.isLive)
registry.add(fastForward as unknown as PluginFactory<Host>)
registry.add(fastForward)
}
+2 -1
View File
@@ -1,10 +1,11 @@
import type { BuiltinHost } from './builtins'
import type { PluginFactory, PluginHost, PluginRegistration } from './types'
import { isClosing } from '../lifecycle/instance'
import { errorHandle } from '../utils/error'
import installBuiltins from './builtins'
import registerPlugin from './registration'
export default class Plugins<Host extends PluginHost<Host>> {
export default class Plugins<Host extends PluginHost<Host> & BuiltinHost> {
declare art: Host
declare id: number
+7 -6
View File
@@ -1,5 +1,6 @@
import type { SettingKind } from './selection'
import type { SettingItem, SettingManager, SettingRange } from './types'
import { appendElement } from '../component/dom'
import { isClosing } from '../lifecycle/instance'
import { timeout } from '../lifecycle/resources'
import { addClass, append, def, has, inverseClass, setStyle } from '../utils'
@@ -38,7 +39,7 @@ export function createSettingHeader(setting: SettingManager, item: SettingItem):
setStyle($item, 'height', `${SETTING_ITEM_HEIGHT}px`)
addClass($item, 'art-setting-item')
addClass($item, 'art-setting-item-back')
const $left = append($item, '<div class="art-setting-item-left"></div>')
const $left = appendElement($item, '<div class="art-setting-item-left"></div>')
const $icon = document.createElement('div')
addClass($icon, 'art-setting-item-left-icon')
append($icon, arrowLeft)
@@ -88,8 +89,8 @@ export function createSettingItem(setting: SettingManager, item: SettingItem, is
if (!current())
return
const $left = append($item, '<div class="art-setting-item-left"></div>')
const $right = append($item, '<div class="art-setting-item-right"></div>')
const $left = appendElement($item, '<div class="art-setting-item-left"></div>')
const $right = appendElement($item, '<div class="art-setting-item-right"></div>')
const $icon = document.createElement('div')
addClass($icon, 'art-setting-item-left-icon')
@@ -149,8 +150,8 @@ export function createSettingItem(setting: SettingManager, item: SettingItem, is
case 'switch': {
const $switch = document.createElement('div')
addClass($switch, 'art-setting-item-right-icon')
const $switchOn = append($switch, icons.switchOn)
const $switchOff = append($switch, icons.switchOff)
const $switchOn = appendElement($switch, icons.switchOn)
const $switchOff = appendElement($switch, icons.switchOff)
const initialSwitch = item.switch
if (!current())
return
@@ -308,7 +309,7 @@ export function renderSetting(setting: SettingManager, option = setting.option):
const restoreNavigation = () => {
setting.active = previous
if (previous && setting.cache.has(previous))
inverseClass(setting.cache.get(previous), 'art-current')
inverseClass(setting.cache.get(previous)!, 'art-current')
for (const [key, value, priority] of previousLayout)
setting.$parent.style.setProperty(key, value, priority)
}
+2 -2
View File
@@ -37,7 +37,7 @@ export interface SettingEvents {
'blur': []
'focus': [event: Event]
'resize': []
'video:ratechange': []
'video:ratechange': [Event]
'aspectRatio': [value: string]
'flip': [value: string]
'subtitleOffset': [value: number]
@@ -48,7 +48,7 @@ export interface SettingHost extends ComponentHost, SubscriptionHost<SettingEven
controls: { setting?: HTMLElement }
option: { setting: boolean, settings: SettingItem[], playbackRate: boolean, aspectRatio: boolean, flip: boolean, subtitleOffset: boolean }
constructor: { SETTING_WIDTH: number, SETTING_ITEM_HEIGHT: number, SETTING_ITEM_WIDTH: number, PLAYBACK_RATE: number[], ASPECT_RATIO: string[], FLIP: string[] }
icons: Record<'arrowLeft' | 'arrowRight' | 'config' | 'check' | 'switchOn' | 'switchOff' | 'playbackRate' | 'aspectRatio' | 'flip' | 'subtitle', HTMLElement>
icons: Record<'arrowLeft' | 'arrowRight' | 'config' | 'check' | 'switchOn' | 'switchOff' | 'playbackRate' | 'aspectRatio' | 'flip', HTMLElement> & { subtitle?: HTMLElement }
proxy: ComponentHost['events']['proxy']
i18n: { get: (key: string) => string }
setting: SettingManager
+1 -1
View File
@@ -155,7 +155,7 @@ export function updateSetting(setting: SettingManager, target: SettingItem): Set
if (rendered) {
setting.active = previous
if (previous && setting.cache.has(previous))
inverseClass(setting.cache.get(previous), 'art-current')
inverseClass(setting.cache.get(previous)!, 'art-current')
for (const [key, value, priority] of layout)
setting.$parent.style.setProperty(key, value, priority)
}
+4 -2
View File
@@ -1,5 +1,7 @@
import type { NoticeSink } from '../notice'
export type SourceEvent = 'video:error' | 'video:loadedmetadata' | 'video:canplay' | 'video:seeked' | 'seek'
export type SourceListener = (event?: unknown) => void
export type SourceListener = (event?: unknown, ...extra: unknown[]) => void
export interface SourceEvents {
on: (name: SourceEvent, callback: SourceListener) => unknown
@@ -35,7 +37,7 @@ export interface SwitchHost extends UrlTarget, SourceEvents {
playbackRate: number
pause: () => unknown
play: () => unknown
notice: { set show(value: string) }
notice: NoticeSink
}
export interface SwitchMethods {
@@ -1,12 +1,16 @@
export default class Storage {
declare name: string
declare settings: Record<PropertyKey, unknown>
constructor() {
this.name = 'artplayer_settings'
this.settings = {}
}
get(key) {
get(key?: PropertyKey): unknown {
try {
const storage = JSON.parse(window.localStorage.getItem(this.name)) || {}
// Preserve JSON.parse(null), primitive payloads and truthy-key selection.
const storage = (JSON.parse(window.localStorage.getItem(this.name) as string) || {}) as Record<PropertyKey, unknown>
return key ? storage[key] : storage
}
catch {
@@ -14,7 +18,7 @@ export default class Storage {
}
}
set(key, value) {
set(key: PropertyKey, value: unknown): void {
try {
const storage = Object.assign({}, this.get(), {
[key]: value,
@@ -26,9 +30,9 @@ export default class Storage {
}
}
del(key) {
del(key: PropertyKey): void {
try {
const storage = this.get()
const storage = this.get() as Record<PropertyKey, unknown>
delete storage[key]
window.localStorage.setItem(this.name, JSON.stringify(storage))
}
@@ -37,7 +41,7 @@ export default class Storage {
}
}
clear() {
clear(): void {
try {
window.localStorage.removeItem(this.name)
}
+3 -2
View File
@@ -1,6 +1,7 @@
import type { Subtitle as SubtitleInput } from '../../types/subtitle'
import type { ComponentHost } from '../component/types'
import type { OptionalMediaCapabilities } from '../media/types'
import type { NoticeSink } from '../notice'
export type SubtitleOption = Required<SubtitleInput>
@@ -20,7 +21,7 @@ export interface SubtitleHost extends ComponentHost {
proxy: ComponentHost['events']['proxy']
on: (name: 'video:timeupdate', callback: () => void) => unknown
off: (name: 'video:timeupdate', callback: () => void) => unknown
notice: { set show(value: unknown) }
notice: NoticeSink
i18n: { get: (key: string) => string }
}
@@ -38,7 +39,7 @@ export interface SubtitleOffsetHost {
template: { $track?: SubtitleTrack | null }
subtitle: { readonly cues: SubtitleCue[], update: () => void }
readonly duration: number
notice: { set show(value: string) }
notice: NoticeSink
i18n: { get: (key: string) => string }
emit: (name: 'subtitleOffset', value: number) => unknown
}
+11 -7
View File
@@ -1,5 +1,5 @@
import type { TemplateHost, TemplateNodes } from './template/types'
import { ownContainer } from './lifecycle/instance'
import { duringTemplateMount, isClosing, ownContainer } from './lifecycle/instance'
import captureTemplate from './lifecycle/template-rollback'
import html from './template/html'
import bindNodes from './template/nodes'
@@ -56,8 +56,10 @@ export default class Template<Host extends TemplateHost<Host>> implements Templa
this.query = this.query.bind(this)
ownContainer(art, this.$container, captureTemplate(this.$container))
this.$container.dataset.artId = String(art.id)
this.init()
duringTemplateMount(art, this, () => {
this.$container.dataset.artId = String(art.id)
this.init()
})
}
static get html(): string {
@@ -79,18 +81,20 @@ export default class Template<Host extends TemplateHost<Host>> implements Templa
if (option.proxy) {
const video = option.proxy.call(this.art, this.art)
if (isClosing(this.art))
return
assertProxy(video)
replaceElement(video, this.$video)
replaceElement(video, this.$video!)
video.className = 'art-video'
this.$video = video
}
if (option.backdrop) {
addClass(this.$player, 'art-backdrop')
addClass(this.$player!, 'art-backdrop')
}
if (isMobile) {
addClass(this.$player, 'art-mobile')
addClass(this.$player!, 'art-mobile')
}
}
@@ -99,7 +103,7 @@ export default class Template<Host extends TemplateHost<Host>> implements Templa
this.$container.innerHTML = ''
}
else {
addClass(this.$player, 'art-destroy')
addClass(this.$player!, 'art-destroy')
}
}
}
+13
View File
@@ -1,3 +1,16 @@
import type { MediaSurface } from '../media/types'
import type Template from '../template'
// Consumers of the canonical template require these nodes. SSR callers supply
// the same structure and proxy callers supply a media-like element; neither
// contract is proven by a DOM query. Keep this assertion at the entry boundary.
// Template itself and all public query helpers retain nullable query results.
type RequiredPlayerNode = Exclude<keyof TemplateNodes, '$danmuku' | '$controlsCenter' | '$notice' | '$mask' | '$info'>
export type PlayerTemplate<Host extends TemplateHost<Host>> = Template<Host> & {
[Name in RequiredPlayerNode]: NonNullable<TemplateNodes[Name]>
} & { $video: MediaSurface, $mini?: HTMLElement }
export interface TemplateHost<Host> {
id: number
option: {
@@ -1,6 +0,0 @@
export const userAgent = globalThis?.CUSTOM_USER_AGENT ?? (typeof navigator !== 'undefined' ? navigator.userAgent : '')
export const isSafari = /^(?:(?!chrome|android).)*safari/i.test(userAgent)
export const isIOS = /iPad|iPhone|iPod/i.test(userAgent) && !window.MSStream
export const isIOS13 = isIOS || (userAgent.includes('Macintosh') && navigator.maxTouchPoints >= 1)
export const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || isIOS13
export const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'
@@ -0,0 +1,7 @@
const customAgent = (globalThis as { CUSTOM_USER_AGENT?: string }).CUSTOM_USER_AGENT
export const userAgent = customAgent ?? (typeof navigator === 'undefined' ? '' : navigator.userAgent)
export const isSafari = /^(?:(?!chrome|android).)*safari/i.test(userAgent)
export const isIOS = /iPad|iPhone|iPod/i.test(userAgent) && (typeof window === 'undefined' || !(window as Window & { MSStream?: unknown }).MSStream)
export const isIOS13 = isIOS || (userAgent.includes('Macintosh') && typeof navigator !== 'undefined' && navigator.maxTouchPoints >= 1)
export const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || isIOS13
export const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'
-145
View File
@@ -1,145 +0,0 @@
import { isMobile } from './compatibility'
export { loadImg } from '../image/load'
export { setStyleText } from '../style/inject'
/** @param {string} selector @param {ParentNode} parent */
export function query(selector, parent = document) {
return parent.querySelector(selector)
}
export function queryAll(selector, parent = document) {
return Array.from(parent.querySelectorAll(selector))
}
export function addClass(target, className) {
return target.classList.add(className)
}
export function removeClass(target, className) {
return target.classList.remove(className)
}
export function hasClass(target, className) {
return target.classList.contains(className)
}
export function append(parent, child) {
if (child instanceof Element) {
parent.appendChild(child)
}
else {
parent.insertAdjacentHTML('beforeend', String(child))
}
return parent.lastElementChild || parent.lastChild
}
export function remove(child) {
return child.parentNode.removeChild(child)
}
export function setStyle(element, key, value) {
element.style[key] = value
return element
}
export function setStyles(element, styles) {
for (const key in styles) {
setStyle(element, key, styles[key])
}
return element
}
export function getStyle(element, key, numberType = true) {
const value = window.getComputedStyle(element, null).getPropertyValue(key)
return numberType ? Number.parseFloat(value) : value
}
export function siblings(target) {
return Array.from(target.parentElement.children).filter(item => item !== target)
}
export function inverseClass(target, className) {
siblings(target).forEach(item => removeClass(item, className))
addClass(target, className)
}
export function tooltip(target, msg, pos = 'top') {
if (isMobile)
return
target.setAttribute('aria-label', msg)
addClass(target, 'hint--rounded')
addClass(target, `hint--${pos}`)
}
export function isInViewport(el, offset = 0) {
const rect = el.getBoundingClientRect()
const windowHeight = window.innerHeight || document.documentElement.clientHeight
const windowWidth = window.innerWidth || document.documentElement.clientWidth
const vertInView = rect.top - offset <= windowHeight && rect.top + rect.height + offset >= 0
const horInView = rect.left - offset <= windowWidth + offset && rect.left + rect.width + offset >= 0
return vertInView && horInView
}
export function includeFromEvent(event, target) {
return getComposedPath(event).includes(target)
}
export function replaceElement(newChild, oldChild) {
oldChild.parentNode.replaceChild(newChild, oldChild)
return newChild
}
export function createElement(tag) {
return document.createElement(tag)
}
/** @param {string} key @param {string | HTMLElement} html */
export function getIcon(key = '', html = '') {
const icon = createElement('i')
addClass(icon, 'art-icon')
addClass(icon, `art-icon-${key}`)
append(icon, html)
return icon
}
export function supportsFlex() {
const div = document.createElement('div')
div.style.display = 'flex'
return div.style.display === 'flex'
}
export function getRect(el) {
return el.getBoundingClientRect()
}
export function getComposedPath(event) {
if (event.composedPath)
return event.composedPath()
const path = []
let node = event.target
while (node) {
path.push(node)
node = node.parentNode
}
if (!path.includes(window) && window !== undefined) {
path.push(window)
}
return path
}
export function getSafeAreaInsets() {
const div = document.createElement('div')
div.style.cssText
= 'position:fixed;top:env(safe-area-inset-top,0px);right:env(safe-area-inset-right,0px);bottom:env(safe-area-inset-bottom,0px);left:env(safe-area-inset-left,0px);pointer-events:none;visibility:hidden;'
document.body.appendChild(div)
const style = getComputedStyle(div)
const insets = {
top: Number.parseFloat(style.top) || 0,
right: Number.parseFloat(style.right) || 0,
bottom: Number.parseFloat(style.bottom) || 0,
left: Number.parseFloat(style.left) || 0,
}
div.remove()
return insets
}
+7
View File
@@ -0,0 +1,7 @@
export { getComposedPath, includeFromEvent } from '../dom/event-path'
export { getRect, getSafeAreaInsets, isInViewport, supportsFlex } from '../dom/measure'
export { getIcon, tooltip } from '../dom/presentation'
export { getStyle, setStyle, setStyles } from '../dom/styles'
export { addClass, append, createElement, hasClass, inverseClass, query, queryAll, remove, removeClass, replaceElement, siblings } from '../dom/tree'
export { loadImg } from '../image/load'
export { setStyleText } from '../style/inject'
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,91 @@
# CORE-20 核心入口与依赖收敛
## 范围与状态
从 ccf77c4e 开始,CORE-20 源码及任务验收完成;下文保留阶段实施过程。上一任务已独立提交并验证,工作区开始时干净。覆盖剩余 13 个自有 JS:公共入口、Player 安装与 option 初始化、config/storage、DOM/环境工具和六个属性 mixin。screenfull 为登记的第三方文件,遵循来源与许可规则。完整迁移、严格类型、旧接口/描述符/静态/事件差分、构建和安装产物验证完成前不标 done,也不提前提交任务完成 commit。
API-01/02/03/04/05/07/08/09/10/11/12 均相关。保留构造顺序、属性安装顺序、静态字段可写性与求值时机、实例收集、就绪回调 this、导入样式/global/日志副作用。公共类型精确视图由 CORE-21 收敛,不能用全局 any 索引绕过入口检查。
## 实施顺序与边界
先迁移叶层属性、storage 和环境判断;按职责拆分 DOM 查询/树操作、样式、事件路径与布局测量,utils 保留兼容导出。然后类型化 config 与 optionInit,明确初始化依赖及失效边界;最后将 Player 和 Artplayer 门面接入已迁移子系统,复核断言是否确实只存在于动态属性安装和旧媒体扩展边界。
Storage 必须先保留 artplayer_settings 信封、name/settings 可观察字段、同步返回及错误 fallback。attr 的动态属性访问、cssVar 的 truthy 判定和 poster 解析也属于历史事实,不能在补类型时顺手规范化。发现缺陷先复现登记,再记录兼容修正。
## 验收与交接
复用既有 Node/类型/浏览器/真实 tarball 入口,新增测试只覆盖未覆盖的行为、失效与重入。普通显示与媒体流程继续使用真实浏览器,AirPlay 等实体设备能力不能以受控对象证明。各阶段结果如实记录,不将上个候选的完整矩阵当作当前代码证据。
本任务可使用已有工具链;新增依赖或脚本时记录用途和固定版本。完成后独立本地提交 CORE-20,不推送或发布。回退只撤销本任务并重建对应产物,保留 CORE-19 已交付功能。
## 叶层与 DOM 阶段
已迁移 storage、compatibility 及 attr/cssVar/type/theme/poster 五个 mixin;保留动态属性访问、undefined getter、truthy CSS 写入、原媒体/样式目标捕获、poster 引号解析和同步描述符。Storage 使用 unknown 表示未校验的历史 JSON 读取;没有把既有错误 fallback 改成无记录的新持久化策略。
环境测试从发布包复现自定义 iPhone/Macintosh UA 的 SSR ReferenceError。候选增加缺失全局保护,同时保留自定义 UA 的优先级与 navigator 惰性访问。该异常修复的完整候选入口/安装产物验证仍待完成,BASE-ENV-02 保持 open。
utils/dom 迁移并拆为 tree/styles/event-path/measure/presentation,公开 utils 仍是原导出集合。初次严格检查暴露 23 个隐含节点类型假设;设置内已知 DOM 复用 appendElement,Map.has 后的缓存和已挂载模板节点使用局部断言,没有把所有返回扩大为 any 或强行宣称 public append 永不为空。
事件路径无 window 时的 ReferenceError 已由发布包复现。安全区测量失败后泄漏探测节点则来自 CORE-19 工作区;最初误以 npm 基线也有该 helper,Node 检查指出 getSafeAreaInsets 不存在,已更正来源。test/helpers/legacy-safe-area.js 是从 ccf77c4e 原函数逐字提取的自有代码夹具,注明函数 SHA-256;浏览器服务也登记其来源与哈希。不依赖浅 clone 中的 Git 历史来运行回归,不将该夹具当作生产实现。
新增 Node 边界检查覆盖 Storage 信封/JSON/异常、属性转发、环境 flags/SSR、DOM 字符串与节点、原始样式值、事件路径和安全区 finally 清理。浏览器专项复验原生树/样式/事件与探测节点清理,同时复用设置、模板和方向场景。此阶段不生成或验收正式 tarball,最终仍需完成剩余初始化与入口源码。
叶层/DOM 历史阶段已完成 CI 436 项(407 单元 + 4 工程 + 25 基线)、220 个生产 TS 检查与 180 项源码 UMD 三浏览器专项,见 baselines/entry-leaf-dom-partial.json。该证据固定当时源码;后续初始化改动不能沿用它作为全量候选验收。
## 初始化与配置阶段
媒体 config 迁移 TS,公共数组仍可修改,保持顺序。optionInit 保持属性、静音、配置音量、持久音量、poster、autoplay、inline、theme/CSS、url 的顺序;在 getter、原生 setter、storage 和样式回调后检查销毁,阻止后续写入。原函数与 AirPlay 函数从 ccf77c4e 冻结成自有测试夹具,仅调整 utils 相对导入,注明原文件 SHA。
六项 Node 测试比较原初始化顺序和八个销毁重入边界,并验证属性 getter、AirPlay 可用性/接收者/事件/错误和销毁后的 picker/notice。候选均通过;AirPlay 的真实连接设备仍需环境矩阵验证。新增内部类型夹具明确 raw storage unknown、append 可空以及 mutable config;公开声明未改。BASE-LIFE-46/47 保持 open,等待两个入口及完整出口验证。
初始化阶段 `yarn ci:check` 已通过:442 项(413 单元、4 工程、25 基线),223 个生产 TS 文件严格检查(核心 218 + chapter 5)。日志为 `refactor/.cache/core20-initialization-ci.log`。本任务已迁移 11 个原有 JS,自有 JS 尚余 Artplayer 与 Player 两个入口;当前改动尚未作为完成任务提交,后续仍需最终构建、安装产物与全量浏览器验收。
## Player 安装与构造取消阶段
player/index.ts 将原 35 个步骤保留在一个有类型的安装表中,Player 实例仍无自有字段、构造参数个数为 1。PlayerHost 从各拥有模块的参数推导安装依赖,包含安装期间提供、稍后回调才读取的能力;不将其当成所有构造阶段已初始化的证明。Artplayer 入口暂仍为 JS,现已在子系统之间检查关闭状态;公共字段仍在各自构造返回后赋值,未提前填充 undefined 字段。Storage 原本不接收参数,入口移除未使用实参。
内部接线修正 PiP 读取类型为真实 element/null/boolean、写入仍为 boolean;初始 native/proxy autoplay 和 URL 视图保留可选能力及 nullable URL。续播记录在读取处明确历史 JSON 形状假设,不再要求所有 Storage.get 返回已校验对象。公开 types 目录未改。
新增浏览器对照确认正常实例属性描述符顺序、原型/静态形状和构造 arity 保持;发布核心在初始 volume setter 销毁后仍安装插件并登记实例,候选现在停止。能力 getter 销毁也会停止后续 Player 安装。最初夹具使用 layers 工厂被既有 schema 拒绝,已改成合法对象,不能把该夹具错误当生产回归。扩大用例后发现 AirPlay 检测内部仍会继续读取/定义,进一步在能力 getter、picker getter 和监听注册返回后检查关闭;新 Node 用例与冻结函数对照。
proxy 在 art.template 赋值前同步销毁的检查,首先在当前候选 Chromium 复现:removeHtml=true 仍残留 DOM;false 未标记 art-destroy;返回的 proxy 仍被挂载和改类名。失败报告 core20-mounting-before.json 固定当时 bundle 指纹。现在 lifecycle 仅在模板构造期间保留私有清理引用,destroy 事件前执行对应 DOM 清理,finally 清除引用;proxy 回调结束发现关闭即停止,不再处理其返回节点。没有提前暴露 art.template,也不替调用方释放其新建 proxy。正常挂载、初始化失败回滚和后续容器复用一并复验。
此阶段 CI 445 项通过(416 单元、4 工程、25 基线),224 个生产 TS 严格检查(核心 219 + chapter 5);核心剩余一个自有 JS 总入口。同一源码 UMD 三浏览器 105 项初始化/生命周期/配置/模板回归,加上新增销毁回调内重建实例的 3 项检查,共 108 个唯一用例(其中初始化专项 24 项);无重试、跳过、意外 pageerror。见 [阶段指纹与报告](../baselines/entry-player-partial.json)。两份成功报告均记录相同候选 bundle 哈希,不是安装包验收。BASE-LIFE-48/49 登记模板挂载和跨阶段取消差异,全部七项 CORE-20 风险保持 open,等待最终出口。
接续重点:src/index.js 迁移必须给真实子系统组合提供类型证明,不能将 PlayerHost 的安装依赖交叉类型当成完整实例实现。Template 保留缺失 SSR 节点可空的实际语义;Canvas proxy 与 native video 能力不同。外部旧回调类型、内部 UI callback this、Emitter 的开放事件和模块 Host 的窄事件需要在组合处协调。不要通过全局 any 或整个实例的旧声明断言跳过这些边界。完成后再处理最终构建、安装包、自定义 UA SSR、全量浏览器与本任务独立 commit。
## 完整入口与类型接线
src/index.ts 已迁移,player/properties.ts 单独描述实际安装的读写属性和方法,不再把模块输入交叉类型当作实例实现。URL 先按实际泛型 Host 安装,再运行原顺序中的其余 34 步。Artplayer 与 Icons 的 declaration-only interface 合并明确登记为动态描述符边界,避免 class fields 提前创建或遮蔽属性;每个子系统均用实际 Artplayer 类型检查。公开 class/prototype/静态值仍按原顺序创建,bootstrap/browser.ts 负责全局发布、样式注入与 100ms 版本日志。
组合检查修正了旧模块视图中的实际不一致:只写 seek/quality/forward/backward 读取为 undefined;metadata 之前 fullscreen 可缺失;PiP 读取为 element/null/boolean;notice 显示 getter 为 boolean,写入使用 NoticeSink;字幕图标可缺失,quality html 支持 HTMLElement。事件使用开放的 CoreEvents 和按名称约束的 CoreEmission,保留原 Emitter 实现;窄接口中的媒体事件补上真实 Event payload。五个内置插件去除整段 unknown factory casts,直接检查实际 host。Storage 的原始读取和续播局部历史记录假设维持分离。
保留两处有名称、可审查的输入兼容边界:option/runtime.ts 将旧声明中的回调视图接到实际模块,不包装函数或改变 this/对象身份;PlayerTemplate 只对核心消费者所需的 SSR 节点和 media-like proxy 标明既有预期,断言局限在模板赋值处。未使用的可省略 SSR 节点继续可空;不添加提前拒绝或合成节点。它们不是对所有任意 JS 输入的运行时证明,公开消费者声明仍留 CORE-21 独立收敛。
test/types/entry-source.ts 检查真实入口满足 Player/Builtin host、参数和读写语义的正反例。根 tsconfig 纳入已有资源声明,使类型夹具能导入带 SVG/LESS 的完整源入口,没有启用 ambient Node 或 skipLibCheck。test/entry.test.js 使用仓库构建配置生成完整 UMD,在隔离服务端环境验证自定义 UA、惰性 navigator、无定时器/DOM 副作用和构造错误;build helper 提取编译入口并保留原 ES loader。类型和 SDK 台账中的当前路径同步至 index.ts/option/resolve.ts,历史快照不改写。
最新 CI 447 项、229 个生产 TS 严格检查、五组原声明消费者通过;完整入口源码 UMD 36 项三浏览器专项通过,覆盖正常描述符/静态、子类、回调身份、可省略 SSR 节点和构造销毁。旧阶段报告不沿用为当前候选证据。
## 安装包验收进行中
生产构建及语言生成通过。run-v40AgV 实际 tarball 已通过 29 项运行时、五组类型。新增安装 UMD/legacy 与直接 ESM 自定义 UA SSR;baseline 模式显式断言旧 ReferenceError,候选必须成功,负例证明旧包不能冒充候选通过。初次 ESM 夹具误认为 Node 24 没有 navigator,Macintosh 预期拒绝未发生;现在明确暂时移除并 finally 恢复 navigator,保证这个 profile 测试的是无 browser/navigator 环境,不把环境差异当包回归。具体诊断与修正日志分别为 core20-package-gates.log / core20-package-gates-fixed.log。
安装包全量 UMD/legacy 浏览器矩阵尚未完成,全部七项 CORE-20 风险仍保持 open。无依赖/锁文件/公开声明/版本变更,未推送或发布;待最终出口完成后独立提交本任务。
## 最终验收与交接
本任务源码和完整验收已完成;上面的 doing、待迁移和待验收描述保留分阶段历史。
固定 Node 24.21.0 / Yarn 1.22.22。核心全部 224 个自有 TS 文件和 chapter 5 个生产文件严格检查;只保留已登记第三方 libs/screenfull.js。CI 447 项通过(418 单元、4 工程、25 基线),另显式检查新增浏览器、内部类型、冻结夹具及安装包脚本的 lint。公开 types 和 yarn.lock 未改。
run-v40AgV 实际 tarball 的 29 项运行时、五组消费者类型检查通过;安装 UMD/legacy 分别在 Chromium 153.0.8010.12、Firefox 155.0、WebKit 26.6 完整通过 1503 项,各含 45 项 DOM/初始化专项。无重试、跳过或意外页面错误;已知旧续播/截图拒绝精确登记。完整入口、子类、实例描述符/静态形状、正常事件和生命周期均复验。
[最终证据](../baselines/entry-validation.json) 将全部当前核心源码对照隔离构建快照,再核对包内全部文件、UMD/legacy/ESM、docs 副本、22 个语言文件和第三方许可。每个浏览器用例均校验实际加载的核心与 chapter 安装文件 SHA;源码专项和两个中间阶段快照不能代替该最终产物证据。
关闭 BASE-ENV-02/03 和 BASE-LIFE-45 至 49,共七项。BASE-ENV-01 的 Windows WebKit Blob 解码、真实 Safari/Apple/AirPlay 设备与其他外部集成仍交对应发布任务。CORE-21 继续负责准确公开视图和历史声明的协调,不用虚假的交叉返回或改变运行时掩盖冲突。
结构、类型、兼容、测试、文档和交接证据已齐;本任务以独立本地 commit 交付。当前 214 项:56 完成、158 待办。下一任务 CORE-21 生成既有入口声明并校验包内容;从 src/index.ts、src/option/runtime.ts、src/player/properties.ts、types/ 及 test/types/declaration-legacy.ts 开始,逐项处理 BASE-TYPE-04/05/07/08/09/10,并复核运行时可变的 option 和媒体能力视图。两个局部输入断言的边界已记在包内 ARCHITECTURE.md,不能将其误当任意 JS 输入的验证。
最终另轮审查对照了生成 ESM 的实际差异:DOM/helper 移动和已登记修正、AirPlay 接收者与异常、35 个 Player 步骤顺序、原样回调传递、声明字段不生成实例属性、模板私有挂载清理以及 browser bootstrap 的原样式参数和延时日志。已知运行时差异均有记录及测试。提交主题:refactor(core): [CORE-20] type entry and converge initialization。
没有新增依赖、锁文件或版本变化,没有推送或发布。回退只撤销本任务源码、测试、文档/状态及构建生成产物,保留 CORE-19 及之前交付;下一任务开始前先核实本任务提交。
+2 -2
View File
@@ -4,7 +4,7 @@
## 现状证据
- [入口](../packages/artplayer/src/index.js)顺序构造所有子系统;[player](../packages/artplayer/src/player/index.js)通过 mixin 将属性安装到 art。
- [入口](../packages/artplayer/src/index.ts)顺序构造所有子系统;[player](../packages/artplayer/src/player/index.ts)按保留的安装顺序通过 mixin 将属性安装到 art,并在销毁后停止后续步骤。
- [Component](../packages/artplayer/src/utils/component.ts)负责控件缓存、DOM 和监听器;[Setting](../packages/artplayer/src/setting/index.ts)还有独立的树、渲染和更新语义。
- [Events](../packages/artplayer/src/events/index.ts)已迁移为 TS,管理 DOM 监听器和全局重绑;输入调度、内部订阅与异步请求分别通过所属模块接入实例或操作作用域。
- [声明](../packages/artplayer/types/artplayer.d.ts)与源码独立维护;[build-ts](../scripts/build-ts.js)进行文本拼接。
@@ -13,7 +13,7 @@
| 领域 | 现有文件范围 | 目标与迁移方式 |
| -------- | -------------------------------------------- | ---------------------------------------------------------------------------- |
| 公共门面 | src/index.js、player/*Mix.js | 保留原签名和属性描述符;委托内部服务,不一次性换继承结构 |
| 公共门面 | src/index.ts、player/index.ts、player/*Mix.ts | 保留原签名和属性描述符;委托内部服务,不一次性换继承结构 |
| 配置 | option、scheme、config、默认值 | 区分用户输入与归一化配置;保持默认值、合并语义和错误时机 |
| 生命周期 | constructor、destroy、Events、各模块清理 | 实例资源作用域与操作资源作用域,清理恰好一次、可重入、失败后继续释放 |
| 内部事件 | utils/emitter.js | typed event map、未知事件兼容扩展、原调用顺序和 ctx |
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 214 项,范围 22 个包及工作区/示例。
状态:todo 159 / doing 0 / blocked 0 / done 55 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 158 / doing 0 / blocked 0 / done 56 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -123,7 +123,7 @@
| CORE-17 | artplayer<br>迁移键盘、手势、焦点和全局事件 | CORE-13, CORE-16 | events/hotkey 的明确 document 与资源归属 | 跨 document 重绑、触摸/键盘、快捷键及销毁无重复响应;修复 BASE-PERF-01 pending resize debounce 销毁后执行,并验证正常 resize 不变 | H | done |
| CORE-18 | artplayer<br>迁移内置插件和提示模块 | CORE-14, CORE-16, CORE-17 | autoPlayback/autoOrientation/fastForward/lock/miniProgressBar 及 info/notice/loading/mask | 默认启用条件、storage 格式、定时器/动画清理通过;结合 BASE-PERF-01 防止销毁后重新安排 notice timer;接续 BASE-TYPE-07 的 notice.show boolean getter 与历史读类型冲突 | M | done |
| CORE-19 | artplayer<br>整理进度、质量、缩略图和截图 | CORE-10, CORE-13, CORE-16 | 相关 player/control 功能与纯计算分离 | 截图跨域失败、缩略图布局、quality/进度边界保持 | M | done |
| CORE-20 | artplayer<br>收敛核心入口与依赖方向 | CORE-14, CORE-15, CORE-18, CORE-19 | Artplayer 门面、初始化依赖与最终目录 | 所有核心自有源码迁移,公开描述符/静态接口/事件差分通过 | H | todo |
| CORE-20 | artplayer<br>收敛核心入口与依赖方向 | CORE-14, CORE-15, CORE-18, CORE-19 | Artplayer 门面、初始化依赖与最终目录 | 所有核心自有源码迁移,公开描述符/静态接口/事件差分通过 | H | done |
| CORE-21 | artplayer<br>生成核心声明并校验包内容 | CORE-20, ENG-07 | 由 TS 生成的既有入口声明与三种发布产物 | 内部类型不泄漏,旧 JS/TS 消费者与 i18n 通过;逐项完成 CORE-07 留下的 BASE-TYPE-04/05/07 声明方案,旧合法消费者与实际返回分别留证;不能以虚假的交叉返回类型或改变运行时来掩盖冲突,未决项阻止核心声明出口;处理 CORE-10 / BASE-TYPE-08 的 setter-only 属性虚构 getter 声明,并复核 PlaybackControls 精确视图与旧 toggle 声明共存策略 | H | todo |
| CORE-23 | artplayer<br>检查并补齐键盘、焦点与可访问名称 | CORE-13, CORE-14, CORE-17, ENG-05, BASE-04 | 主要控件/设置/模式退出的键盘与焦点回归、名称和字幕可用性检查及必要兼容修正 | 保持旧快捷键和 DOM/CSS 钩子;真实浏览器验证,不以静态属性检查代替交互;处理 BASE-DOM-01 的主要控件 Tab 不可达,保留既有名称与快捷键 | H | todo |
| CORE-22 | artplayer<br>核心阶段完整验收 | CORE-21, CORE-23, PILOT-01, ENG-08, ENG-10 | 核心与旧插件的可自动化完整回归、资源/性能及明确外部验证缺口 | 核心自动化和公开差异处置通过;真实环境缺口链接包集成/REL 门槛,阶段完成不代表可公开发布 | H | todo |
@@ -454,6 +454,7 @@
- CORE-17: [记录](changes/2026-09-11-CORE-17-input.md) [记录](baselines/input-validation.json)
- CORE-18: [记录](changes/2026-09-11-CORE-18-builtins-prompts.md) [记录](baselines/builtins-prompts-validation.json)
- CORE-19: [记录](changes/2026-09-11-CORE-19-capture-thumbnails.md) [记录](baselines/capture-progress-validation.json)
- CORE-20: [记录](changes/2026-09-11-CORE-20-entry.md) [记录](baselines/entry-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)
+8
View File
@@ -1,5 +1,13 @@
# 进度与证据
## 当前实施:CORE-20 核心入口与依赖收敛完成
核心全部 224 个自有源文件已迁移 TS,唯一 JS 为登记第三方 libs/screenfull.js。拆清入口、浏览器 bootstrap、DOM 叶层、实际属性/事件与初始化依赖;保持公开形状及顺序,修复构造/配置/AirPlay 销毁后继续安装和 proxy 挂载清理。
CI 447 项、229 个生产 TS 严格检查、实际 tarball 29 项运行时及五组类型通过。安装 UMD/legacy 各 1503 项三浏览器全量通过,各含 45 项 DOM/初始化专项;源码、包内文件、三个格式、docs/语言/许可指纹一致。见 [实施记录](changes/2026-09-11-CORE-20-entry.md) 和 [最终证据](baselines/entry-validation.json)。
关闭七项 CORE-20 差异;真实设备和 BASE-ENV-01 仍保留发布门槛,公开精确类型交 CORE-21。当前 214 项:56 完成、158 待办;下一项 CORE-21 生成声明及安装包类型协调。本任务独立本地提交,无推送或发布。
## 当前实施:CORE-19 进度、质量、截图与缩略图完成
截图和缩略图门面迁移 TS,拆清图像捕获、加载与布局。进度交互防止回调重入/跨源拖动继续 seek;质量切换对原生首次 seek 偏差最多修正一次,保持用户覆盖、取消与 Promise 结算。媒体/公开图片 URL 由调用方拥有,内部资源在配置、控件或实例结束时释放。保留公开 API、类型、正常事件顺序和 DOM/CSS;整列与首格缩略图裁剪是明确记录的显示修复。
+7
View File
@@ -111,3 +111,10 @@
| BASE-LIFE-43 | resolved / 已复现 | Replacing a media source revokes a caller-owned Blob URL still in use | CORE-19 |
| BASE-LIFE-44 | resolved / 已复现 | Native quality restoration can settle at a different position after its first seek | CORE-19 |
| BASE-ENV-01 | open / 已复现 | Windows WebKit native Blob video samples lack decoding support; Apple-device validation remains outstanding | REL-03, REVIEW-02 |
| BASE-ENV-02 | resolved / 已复现 | Custom iOS or Macintosh user agents dereference absent browser globals during SSR import | CORE-20 |
| BASE-LIFE-45 | resolved / 已复现 | Previous workspace safe-area measurement leaks its probe when native style access fails | CORE-20 |
| BASE-ENV-03 | resolved / 已复现 | Fallback event-path utilities dereference absent window in server environments | CORE-20 |
| BASE-LIFE-46 | resolved / 已复现 | Option initialization continues native, style and source writes after reentrant destruction | CORE-20 |
| BASE-LIFE-47 | resolved / 已复现 | AirPlay methods can call a picker or publish events and notices after destruction | CORE-20 |
| BASE-LIFE-48 | resolved / 已复现 | Proxy-time destruction cannot clean a template before its public assignment | CORE-20 |
| BASE-LIFE-49 | resolved / 已复现 | Constructor and Player initialization continue after a callback destroys the instance | CORE-20 |
+176 -1
View File
@@ -717,7 +717,8 @@
"CORE-07"
],
"evidence": [
"packages/artplayer/src/index.js"
"packages/artplayer/src/index.ts",
"packages/artplayer/src/option/resolve.ts"
],
"compatibleResolution": "Bundled validation dependency; preserve validation/default/merge behavior and failure cleanup when typing constructor boundaries.",
"closureCriteria": "固定 SDK/外部资源版本和环境;成功/失败/切换/销毁有真实证据,设备和网络限制逐项记录。"
@@ -2238,6 +2239,180 @@
"closureCriteria": "Record actual Safari and Apple device versions and native sample capabilities, then compare published/candidate load, playback, seek, source replacement and Blob ownership.",
"workspaceState": "Independent video probes on Windows WebKit 26.6 return error 4 and width zero for both local MP4 and WebM Blob samples. Chromium and Firefox decode the tested Blob sample. WebKit ownership tests explicitly report decodedBlob=false.",
"resolutionEvidence": []
},
{
"id": "BASE-ENV-02",
"title": "Custom iOS or Macintosh user agents dereference absent browser globals during SSR import",
"confirmation": "reproduced",
"status": "resolved",
"owners": [
"CORE-20"
],
"evidence": [
"test/environment.test.js",
"refactor/changes/2026-09-11-CORE-20-entry.md"
],
"compatibleResolution": "Keep UA matching, override priority and browser flags while guarding absent window and navigator globals.",
"closureCriteria": "Published failure, isolated environment flags and complete candidate entry/installed import regressions pass without new import side effects.",
"workspaceState": "Absent window/navigator are guarded without changing custom UA priority or lazy evaluation. Full UMD/legacy/ESM imports from the installed tarball pass the explicit no-browser-global profile, and published failures remain tested.",
"resolutionEvidence": [
"refactor/baselines/entry-validation.json",
"refactor/changes/2026-09-11-CORE-20-entry.md",
"test/environment.test.js"
],
"resolutionRationale": "Absent window/navigator are guarded without changing custom UA priority or lazy evaluation. Full UMD/legacy/ESM imports from the installed tarball pass the explicit no-browser-global profile, and published failures remain tested."
},
{
"id": "BASE-LIFE-45",
"title": "Previous workspace safe-area measurement leaks its probe when native style access fails",
"confirmation": "reproduced",
"status": "resolved",
"owners": [
"CORE-20"
],
"evidence": [
"test/dom-boundaries.test.js",
"test/browser/dom-boundaries.spec.js",
"test/helpers/legacy-safe-area.js",
"refactor/changes/2026-09-11-CORE-20-entry.md"
],
"compatibleResolution": "Preserve public utility exports, tree/style semantics and primary errors while closing owned probe cleanup and absent-global failures.",
"closureCriteria": "Complete entry, source/consumer types and installed-package/browser exit pass with explicit source provenance.",
"workspaceState": "The CORE-19 own-source safe-area helper is frozen with provenance; candidate finally cleanup releases its probe on native append/style failures while preserving primary errors. Installed browser regressions pass. The npm baseline does not contain this helper.",
"resolutionEvidence": [
"refactor/baselines/entry-validation.json",
"refactor/changes/2026-09-11-CORE-20-entry.md",
"test/dom-boundaries.test.js",
"test/browser/dom-boundaries.spec.js",
"test/helpers/legacy-safe-area.js"
],
"resolutionRationale": "The CORE-19 own-source safe-area helper is frozen with provenance; candidate finally cleanup releases its probe on native append/style failures while preserving primary errors. Installed browser regressions pass. The npm baseline does not contain this helper."
},
{
"id": "BASE-ENV-03",
"title": "Fallback event-path utilities dereference absent window in server environments",
"confirmation": "reproduced",
"status": "resolved",
"owners": [
"CORE-20"
],
"evidence": [
"test/dom-boundaries.test.js",
"test/browser/dom-boundaries.spec.js",
"test/helpers/legacy-safe-area.js",
"refactor/changes/2026-09-11-CORE-20-entry.md"
],
"compatibleResolution": "Preserve public utility exports, tree/style semantics and primary errors while closing owned probe cleanup and absent-global failures.",
"closureCriteria": "Complete entry, source/consumer types and installed-package/browser exit pass with explicit source provenance.",
"workspaceState": "Fallback event paths preserve native receiver, original array identity and DOM ancestry while supporting an absent window. Published failure and candidate source/browser regressions pass.",
"resolutionEvidence": [
"refactor/baselines/entry-validation.json",
"refactor/changes/2026-09-11-CORE-20-entry.md",
"test/dom-boundaries.test.js",
"test/browser/dom-boundaries.spec.js",
"test/helpers/legacy-safe-area.js"
],
"resolutionRationale": "Fallback event paths preserve native receiver, original array identity and DOM ancestry while supporting an absent window. Published failure and candidate source/browser regressions pass."
},
{
"id": "BASE-LIFE-46",
"title": "Option initialization continues native, style and source writes after reentrant destruction",
"confirmation": "reproduced",
"status": "resolved",
"owners": [
"CORE-20"
],
"evidence": [
"test/initialization.test.js",
"test/helpers/legacy-option-init.js",
"test/helpers/legacy-airplay.js",
"refactor/changes/2026-09-11-CORE-20-entry.md"
],
"compatibleResolution": "Preserve initialization and AirPlay contracts while stopping effects after lifecycle cancellation; keep real device limits explicit.",
"closureCriteria": "Final constructor, strict source types, normal/error/teardown tests and installed-package/browser acceptance pass.",
"workspaceState": "Option initialization retains ordered native/storage/style/source writes, but checks lifecycle closure after callbacks, getters and conversion boundaries. Frozen old traces and complete installed construction tests cover the difference.",
"resolutionEvidence": [
"refactor/baselines/entry-validation.json",
"refactor/changes/2026-09-11-CORE-20-entry.md",
"test/initialization.test.js",
"test/helpers/legacy-option-init.js",
"test/helpers/legacy-airplay.js"
],
"resolutionRationale": "Option initialization retains ordered native/storage/style/source writes, but checks lifecycle closure after callbacks, getters and conversion boundaries. Frozen old traces and complete installed construction tests cover the difference."
},
{
"id": "BASE-LIFE-47",
"title": "AirPlay methods can call a picker or publish events and notices after destruction",
"confirmation": "reproduced",
"status": "resolved",
"owners": [
"CORE-20"
],
"evidence": [
"test/initialization.test.js",
"test/helpers/legacy-option-init.js",
"test/helpers/legacy-airplay.js",
"refactor/changes/2026-09-11-CORE-20-entry.md"
],
"compatibleResolution": "Preserve initialization and AirPlay contracts while stopping effects after lifecycle cancellation; keep real device limits explicit.",
"closureCriteria": "Final constructor, strict source types, normal/error/teardown tests and installed-package/browser acceptance pass.",
"workspaceState": "AirPlay capability detection, picker getter/call completion, event and notice publication stop after closure. Controlled source and installed construction checks preserve normal receiver/error behavior; real AirPlay devices remain unverified release scope.",
"resolutionEvidence": [
"refactor/baselines/entry-validation.json",
"refactor/changes/2026-09-11-CORE-20-entry.md",
"test/initialization.test.js",
"test/helpers/legacy-option-init.js",
"test/helpers/legacy-airplay.js"
],
"resolutionRationale": "AirPlay capability detection, picker getter/call completion, event and notice publication stop after closure. Controlled source and installed construction checks preserve normal receiver/error behavior; real AirPlay devices remain unverified release scope."
},
{
"id": "BASE-LIFE-48",
"title": "Proxy-time destruction cannot clean a template before its public assignment",
"confirmation": "reproduced",
"status": "resolved",
"owners": [
"CORE-20"
],
"evidence": [
"test/instance-lifecycle.test.js",
"test/browser/initialization.spec.js",
"refactor/changes/2026-09-11-CORE-20-entry.md"
],
"compatibleResolution": "Preserve normal constructor order, public property visibility and first-call teardown semantics while cancelling later installation and cleaning the mounting template.",
"closureCriteria": "Strict final entry, normal/failed/reentrant construction and installed package/browser acceptance pass.",
"workspaceState": "Private mounting ownership permits cleanup before public template assignment. Installed removeHtml true/false, proxy ownership, destroy-observer order, replacement construction and failed-constructor regressions pass.",
"resolutionEvidence": [
"refactor/baselines/entry-validation.json",
"refactor/changes/2026-09-11-CORE-20-entry.md",
"test/instance-lifecycle.test.js",
"test/browser/initialization.spec.js"
],
"resolutionRationale": "Private mounting ownership permits cleanup before public template assignment. Installed removeHtml true/false, proxy ownership, destroy-observer order, replacement construction and failed-constructor regressions pass."
},
{
"id": "BASE-LIFE-49",
"title": "Constructor and Player initialization continue after a callback destroys the instance",
"confirmation": "reproduced",
"status": "resolved",
"owners": [
"CORE-20"
],
"evidence": [
"test/browser/initialization.spec.js",
"test/initialization.test.js",
"refactor/changes/2026-09-11-CORE-20-entry.md"
],
"compatibleResolution": "Preserve normal constructor order, public property visibility and first-call teardown semantics while cancelling later installation and cleaning the mounting template.",
"closureCriteria": "Strict final entry, normal/failed/reentrant construction and installed package/browser acceptance pass.",
"workspaceState": "Player installers and outer construction stop after reentrant closure without publishing later plugins or registering the dead instance. Installed normal descriptors/static/subclass behavior and getter/native-write destruction cases pass.",
"resolutionEvidence": [
"refactor/baselines/entry-validation.json",
"refactor/changes/2026-09-11-CORE-20-entry.md",
"test/browser/initialization.spec.js",
"test/initialization.test.js"
],
"resolutionRationale": "Player installers and outer construction stop after reentrant closure without publishing later plugins or registering the dead instance. Installed normal descriptors/static/subclass behavior and getter/native-write destruction cases pass."
}
]
}
+5 -2
View File
@@ -1133,11 +1133,14 @@
"CORE-18",
"CORE-19"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "Artplayer 门面、初始化依赖与最终目录",
"acceptance": "所有核心自有源码迁移,公开描述符/静态接口/事件差分通过",
"evidence": []
"evidence": [
"changes/2026-09-11-CORE-20-entry.md",
"baselines/entry-validation.json"
]
},
{
"id": "CORE-21",
+2 -1
View File
@@ -1184,7 +1184,8 @@
"CORE-07"
],
"sourceFiles": [
"packages/artplayer/src/index.js"
"packages/artplayer/src/index.ts",
"packages/artplayer/src/option/resolve.ts"
],
"contract": "Bundled validation dependency; preserve validation/default/merge behavior and failure cleanup when typing constructor boundaries.",
"validationStatus": "Source integration observed; complete real SDK/service/device matrix not yet verified.",
+1 -1
View File
@@ -87,7 +87,7 @@ export async function checkPackages({ release = false } = {}) {
const baseline = await publishedConsumer()
const installed = consumerDirectory()
try {
const oldRuntime = runtimeConsumer(baseline.dir)
const oldRuntime = runtimeConsumer(baseline.dir, { baseline: true })
const packages = []
for (const name of names) {
const archive = path.join(output, `${name}.tgz`)
+2 -2
View File
@@ -26,10 +26,10 @@ export function removeConsumer(dir) {
fs.rmSync(dir, { recursive: true, force: true })
}
export function runtimeConsumer(dir) {
export function runtimeConsumer(dir, { baseline = false } = {}) {
fs.copyFileSync(path.join(workspace, 'test/package/runtime.cjs'), path.join(dir, 'runtime.cjs'))
fs.copyFileSync(path.join(workspace, 'test/contracts/emitter.js'), path.join(dir, 'emitter.mjs'))
writeJson(path.join(dir, 'expected.json'), { version: readJson(path.join(dir, 'node_modules/artplayer/package.json')).version })
writeJson(path.join(dir, 'expected.json'), { version: readJson(path.join(dir, 'node_modules/artplayer/package.json')).version, baseline })
return JSON.parse(run(['runtime.cjs'], dir))
}
+65
View File
@@ -0,0 +1,65 @@
import { expect, test } from './fixtures.js'
for (const core of ['published', 'candidate']) {
test(`${core}: DOM utilities preserve native tree, style and event-path behavior`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
const result = await page.evaluate(() => {
const utils = window.Artplayer.utils
const root = document.createElement('div')
document.body.appendChild(root)
const child = utils.append(root, '<span class="entry">one</span>')
const textResult = utils.append(root, 'tail')
utils.setStyles(child, { color: 'rgb(1, 2, 3)', width: '12.5px', display: 'inline-block' })
const width = utils.getStyle(child, 'width')
const selected = utils.query('.entry', root) === child && utils.queryAll('.entry', root).length === 1
const replacement = document.createElement('b')
const replaced = utils.replaceElement(replacement, child) === replacement
let path = false
root.addEventListener('click', event => path = utils.includeFromEvent(event, replacement))
replacement.click()
const result = { selected, textPrecedence: textResult === child, replaced, path, width, detached: child.parentNode === null, returnIdentity: utils.remove(replacement) === replacement }
root.remove()
return result
})
expect(result).toEqual({ selected: true, textPrecedence: true, replaced: true, path: true, width: 12.5, detached: true, returnIdentity: true })
})
}
test('candidate: safe-area measurement releases the native probe after failure unlike the previous workspace implementation', async ({ page }) => {
await page.goto('/test/player.html?core=candidate&chapter=published')
const results = await page.evaluate(async () => {
const { getSafeAreaInsets: previous } = await import('/test/legacy-safe-area.js')
const results = []
for (const measure of [previous, window.Artplayer.utils.getSafeAreaInsets]) {
const before = document.body.childElementCount
const success = measure()
const successClean = document.body.childElementCount === before
const original = window.getComputedStyle
const error = new Error('controlled safe-area style failure')
let sameError = false
window.getComputedStyle = () => {
throw error
}
try {
measure()
}
catch (value) {
sameError = value === error
}
finally {
window.getComputedStyle = original
}
const remaining = document.body.childElementCount - before
while (document.body.childElementCount > before)
document.body.lastElementChild.remove()
results.push({ successClean, success, sameError, remaining })
}
return results
})
for (const result of results) {
expect(result.successClean).toBe(true)
expect(result.sameError).toBe(true)
expect(Object.values(result.success).every(value => Number.isFinite(value) && value >= 0)).toBe(true)
}
expect(results.map(result => result.remaining)).toEqual([1, 0])
})
+272
View File
@@ -0,0 +1,272 @@
import { expect, test } from './fixtures.js'
for (const removeHtml of [true, false]) {
test(`candidate: proxy-time destroy cleans the mounting template with removeHtml=${removeHtml}`, async ({ page }) => {
await page.goto('/test/player.html?core=candidate&chapter=published')
const result = await page.evaluate((removeHtml) => {
const { Artplayer } = window
const container = document.createElement('div')
document.body.append(container)
let calls = 0
let visibleTemplate
let destroyedDOM
const proxy = document.createElement('video')
proxy.className = 'owned-by-caller'
const art = new Artplayer({ container, url: '', proxy(owner) {
visibleTemplate = Object.hasOwn(owner, 'template')
owner.on('destroy', () => {
calls++
destroyedDOM = { nodes: container.childElementCount, retained: !!container.querySelector('.art-destroy') }
})
owner.destroy(removeHtml)
return proxy
} })
art.destroy(!removeHtml)
const result = {
visibleTemplate,
calls,
destroyed: art.isDestroy,
registered: Artplayer.instances.includes(art),
hasEvents: Object.hasOwn(art, 'events'),
nodes: container.childElementCount,
retained: !!container.querySelector('.art-destroy'),
destroyedDOM,
proxyUntouched: proxy.parentNode === null && proxy.className === 'owned-by-caller',
}
const next = new Artplayer({ container, url: '' })
result.reusable = Artplayer.instances.includes(next)
next.destroy()
return result
}, removeHtml)
const dom = { nodes: removeHtml ? 0 : 1, retained: !removeHtml }
expect(result).toEqual({ visibleTemplate: false, calls: 1, destroyed: true, registered: false, hasEvents: false, ...dom, destroyedDOM: dom, proxyUntouched: true, reusable: true })
})
}
test('candidate: player descriptor order and constructor/static surfaces match the published core', async ({ page }) => {
const results = []
for (const core of ['published', 'candidate']) {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
results.push(await page.evaluate(() => {
const { Artplayer } = window
const container = document.createElement('div')
document.body.append(container)
const art = new Artplayer({ container, url: '', muted: true })
const describe = (object, names) => names.map((name) => {
const property = Object.getOwnPropertyDescriptor(object, name)
return [name, property.enumerable, property.configurable, property.writable ?? null, typeof property.get, typeof property.set, typeof property.value]
})
const properties = Object.getOwnPropertyNames(art).filter((name) => {
const property = Object.getOwnPropertyDescriptor(art, name)
return !property.configurable && !property.enumerable
})
const result = {
arity: Artplayer.length,
playerArity: art.player.constructor.length,
playerKeys: Object.keys(art.player),
descriptors: describe(art, properties),
prototype: describe(Artplayer.prototype, Object.getOwnPropertyNames(Artplayer.prototype).filter(name => name !== 'constructor')),
statics: describe(Artplayer, Object.getOwnPropertyNames(Artplayer).filter(name => !['length', 'name', 'prototype'].includes(name))),
state: [art.type, art.muted, art.volume, art.currentTime, art.played, art.loaded, art.playing, art.state],
}
art.destroy()
return result
}))
}
expect(results[1]).toEqual(results[0])
expect(results[1].arity).toBe(2)
expect(results[1].playerArity).toBe(1)
expect(results[1].playerKeys).toEqual([])
})
test('candidate: proxy-time destroy listener can mount a replacement without the old constructor overwriting it', async ({ page }) => {
await page.goto('/test/player.html?core=candidate&chapter=published')
const result = await page.evaluate(() => {
const { Artplayer } = window
const container = document.createElement('div')
document.body.append(container)
let replacement
const unused = document.createElement('video')
const old = new Artplayer({ container, url: '', proxy(owner) {
owner.on('destroy', () => {
replacement = new Artplayer({ container, url: '' })
})
owner.destroy()
return unused
} })
const result = {
oldDestroyed: old.isDestroy,
registry: Artplayer.instances.length === 1 && Artplayer.instances[0] === replacement,
player: container.querySelector('.art-video-player') === replacement.template.$player,
video: container.querySelector('video') === replacement.video,
artId: container.dataset.artId === String(replacement.id),
unusedDetached: unused.parentNode === null,
}
old.destroy()
result.stillRegistered = Artplayer.instances.includes(replacement)
replacement.destroy()
return result
})
expect(result).toEqual({ oldDestroyed: true, registry: true, player: true, video: true, artId: true, unusedDetached: true, stillRegistered: true })
})
for (const core of ['published', 'candidate']) {
test(`${core}: subclasses retain receiver identity, initialization timing and static inheritance`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
const result = await page.evaluate(() => {
const { Artplayer } = window
const container = document.createElement('div')
document.body.append(container)
let subclassDefaults = 0
class Extended extends Artplayer {
static get option() {
subclassDefaults++
return super.option
}
constructor(option, ready) {
super(option, ready)
this.extension = 'after-super'
}
}
Extended.NOTICE_TIME = 321
let pluginShape
let readyShape
const art = new Extended({ container, url: '', plugins: [function (owner) {
pluginShape = this === owner && owner instanceof Extended && !Object.hasOwn(owner, 'extension')
return { name: 'subclass-plugin' }
}] }, function (owner) {
readyShape = this === owner && owner instanceof Extended && owner.extension === 'after-super'
})
const chained = art.emit('ready') === art
const result = { pluginShape, readyShape, chained, subclassDefaults, registry: Artplayer.instances.includes(art), prototype: Object.getPrototypeOf(art) === Extended.prototype, constant: art.notice.art.constructor.NOTICE_TIME, inheritedInstances: Extended.instances === Artplayer.instances }
art.destroy()
return result
})
expect(result).toEqual({ pluginShape: true, readyShape: true, chained: true, subclassDefaults: 0, registry: true, prototype: true, constant: 321, inheritedInstances: true })
})
test(`${core}: omitted unused SSR nodes stay nullable without an eager template rejection`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
const result = await page.evaluate(() => {
const { Artplayer } = window
const container = document.createElement('div')
container.innerHTML = Artplayer.html
container.querySelector('.art-danmuku').remove()
document.body.append(container)
const art = new Artplayer({ container, url: '', useSSR: true })
const result = { missing: art.template.$danmuku === null, query: art.query('.missing-node') === null, mounted: container.querySelector('.art-video-player') === art.template.$player }
art.destroy()
return result
})
expect(result).toEqual({ missing: true, query: true, mounted: true })
})
test(`${core}: teardown during capability detection stops remaining player installers`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
const result = await page.evaluate(() => {
const { Artplayer } = window
const key = 'WebKitPlaybackTargetAvailabilityEvent'
const previous = Object.getOwnPropertyDescriptor(window, key)
const container = document.createElement('div')
document.body.append(container)
let captured
let reads = 0
Object.defineProperty(window, key, { configurable: true, get() {
reads++
if (captured && !captured.isDestroy)
captured.destroy()
return undefined
} })
try {
const art = new Artplayer({ container, url: '', proxy(owner) {
captured = owner
return document.createElement('video')
} })
return {
reads,
returned: captured === art,
destroyed: art.isDestroy,
registered: Artplayer.instances.includes(art),
airplay: Object.hasOwn(art, 'airplay'),
quality: Object.hasOwn(art, 'quality'),
thumbnails: Object.hasOwn(art, 'thumbnails'),
controls: Object.hasOwn(art, 'controls'),
nodes: container.childElementCount,
}
}
finally {
if (previous)
Object.defineProperty(window, key, previous)
else
delete window[key]
}
})
expect(result).toEqual({ reads: 1, returned: true, destroyed: true, registered: core === 'published', airplay: core === 'published', quality: core === 'published', thumbnails: core === 'published', controls: core === 'published', nodes: 0 })
})
test(`${core}: destroy during initial native volume assignment stops later construction`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
const result = await page.evaluate(async () => {
const { Artplayer } = window
const container = document.createElement('div')
document.body.append(container)
const calls = []
let captured
let thrown
let returned
try {
returned = new Artplayer({
container,
url: '',
proxy(art) {
captured = art
const video = document.createElement('video')
const volume = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'volume')
Object.defineProperty(video, 'volume', {
get() { return volume.get.call(this) },
set(value) {
calls.push('volume')
volume.set.call(this, value)
if (!art.isDestroy) {
art.destroy()
calls.push('destroyed')
}
},
})
return video
},
layers: [{ name: 'later', html: 'later' }],
plugins: [(art) => {
calls.push(`plugin:${art.isDestroy}`)
return { name: 'later' }
}],
})
}
catch (error) { thrown = error.message }
if (!captured)
throw new Error(`Initialization fixture did not reach proxy: ${thrown}`)
await new Promise(resolve => setTimeout(resolve, 0))
return {
calls,
thrown: thrown ?? null,
returned: returned === captured,
destroyed: captured.isDestroy,
registered: Artplayer.instances.includes(captured),
hasLayers: Object.hasOwn(captured, 'layers'),
hasPlugins: Object.hasOwn(captured, 'plugins'),
nodes: container.childElementCount,
listeners: captured.events.destroyEvents.size,
}
})
if (core === 'candidate') {
expect(result).toEqual({ calls: ['volume', 'destroyed'], thrown: null, returned: true, destroyed: true, registered: false, hasLayers: false, hasPlugins: false, nodes: 0, listeners: 0 })
}
else {
expect(result.calls).toContain('plugin:true')
expect(result.registered).toBe(true)
expect(result.hasLayers).toBe(true)
expect(result.hasPlugins).toBe(true)
}
})
}
+1
View File
@@ -62,6 +62,7 @@ for (const extension of ['mp4', 'webm']) {
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.mp4': 'video/mp4', '.webm': 'video/webm', '.vtt': 'text/vtt', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg' }
add('/test/declaration-cues.vtt', fs.readFileSync(path.join(workspace, 'test/browser/media/declaration-cues.vtt')), { kind: 'test-subtitles', file: 'test/browser/media/declaration-cues.vtt' })
add('/test/thumbnail-grid.svg', fs.readFileSync(path.join(workspace, 'test/browser/media/thumbnail-grid.svg')), { kind: 'test-thumbnail-grid', file: 'test/browser/media/thumbnail-grid.svg' })
add('/test/legacy-safe-area.js', fs.readFileSync(path.join(workspace, 'test/helpers/legacy-safe-area.js')), { kind: 'frozen-own-source', file: 'test/helpers/legacy-safe-area.js', commit: 'ccf77c4e' })
function send(req, res, bytes, filename) {
res.setHeader('Content-Type', mime[path.extname(filename)] || 'application/octet-stream')
res.setHeader('Cache-Control', 'no-store')
+105
View File
@@ -0,0 +1,105 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
import { test } from 'node:test'
import { getSafeAreaInsets as legacySafeArea } from './helpers/legacy-safe-area.js'
import { loadModules, loadPublishedCore } from './helpers/load.js'
const names = ['append', 'remove', 'replaceElement', 'getComposedPath', 'includeFromEvent', 'getSafeAreaInsets', 'setStyle', 'setStyles', 'getStyle']
const utils = await loadModules(Object.fromEntries(names.map(name => [name, { file: 'packages/artplayer/src/utils/dom', name }])))
function globalValue(t, name, value) {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, name)
Object.defineProperty(globalThis, name, { value, configurable: true, writable: true })
t.after(() => descriptor ? Object.defineProperty(globalThis, name, descriptor) : delete globalThis[name])
}
test('event paths preserve native receiver/array identity and work without a browser window', async () => {
const old = await loadPublishedCore()
assert.throws(() => old.Artplayer.utils.getComposedPath({ target: null }), error => error.name === 'ReferenceError')
assert.deepEqual(utils.getComposedPath({ target: null }), [])
const parent = new EventTarget()
const child = Object.assign(new EventTarget(), { parentNode: parent })
assert.deepEqual(utils.getComposedPath({ target: child }), [child, parent])
const path = [child, parent]
const event = { target: child, composedPath() {
assert.equal(this, event)
return path
} }
assert.equal(utils.getComposedPath(event), path)
assert.equal(utils.includeFromEvent(event, undefined), false)
assert.equal(utils.includeFromEvent(event, parent), true)
})
test('DOM append preserves string conversion and last-element precedence; detached removal still fails', (t) => {
class Element {}
globalValue(t, 'Element', Element)
const existing = new Element()
const text = { textContent: 'text' }
const calls = []
const parent = { lastElementChild: existing, lastChild: text, insertAdjacentHTML: (...args) => calls.push(args), appendChild: child => calls.push(child) }
const data = { toString: () => '<b>converted</b>' }
assert.equal(utils.append(parent, data), existing)
assert.deepEqual(calls, [['beforeend', '<b>converted</b>']])
assert.equal(utils.append(parent, existing), existing)
assert.equal(calls[1], existing)
assert.throws(() => utils.remove({ parentNode: null }), TypeError)
const next = new Element()
const child = { parentNode: { replaceChild: (...args) => calls.push(args) } }
assert.equal(utils.replaceElement(next, child), next)
assert.deepEqual(calls.at(-1), [next, child])
})
test('style helpers preserve inherited keys, raw setter values and numeric/string reads', (t) => {
const writes = []
const element = { style: new Proxy({}, { set(target, key, value) {
writes.push([key, value])
return Reflect.set(target, key, value)
} }) }
const raw = { toString: () => '1px' }
assert.equal(utils.setStyle(element, 'width', raw), element)
assert.equal(writes[0][1], raw)
assert.equal(utils.setStyles(element, Object.assign(Object.create({ inherited: 1 }), { own: null })), element)
assert.deepEqual(writes.slice(1), [['own', null], ['inherited', 1]])
globalValue(t, 'window', { getComputedStyle: () => ({ getPropertyValue: key => key === 'width' ? '12.5px' : 'auto' }) })
assert.equal(utils.getStyle(element, 'width'), 12.5)
assert.equal(utils.getStyle(element, 'width', false), '12.5px')
assert(Number.isNaN(utils.getStyle(element, 'height')))
})
test('safe-area measurement removes its owned probe after success and style/append failures', async (t) => {
for (const boundary of ['success', 'style', 'append']) {
const error = new Error(boundary)
let attached = false
let removed = 0
const div = { style: {}, remove() {
removed++
attached = false
} }
const document = { createElement: () => div, body: { appendChild() {
attached = true
if (boundary === 'append')
throw error
} } }
const getComputedStyle = () => {
if (boundary === 'style')
throw error
return { top: '10px', right: 'auto', bottom: '2.5px', left: '0px' }
}
globalValue(t, 'document', document)
globalValue(t, 'getComputedStyle', getComputedStyle)
if (boundary === 'style') {
const published = await loadPublishedCore()
assert.equal(typeof published.Artplayer.utils.getSafeAreaInsets, 'undefined')
assert.throws(() => legacySafeArea(), value => value === error)
assert.equal(attached, true)
assert.equal(removed, 0)
attached = false
}
if (boundary === 'success')
assert.deepEqual(utils.getSafeAreaInsets(), { top: 10, right: 0, bottom: 2.5, left: 0 })
else
assert.throws(() => utils.getSafeAreaInsets(), value => value === error)
assert.equal(attached, false)
assert.equal(removed, 1)
}
})
+43
View File
@@ -0,0 +1,43 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Verify the actual compiled entry in isolated server environments.
import { test } from 'node:test'
import vm from 'node:vm'
import { compilePackage, loadPublishedCore } from './helpers/load.js'
const source = await compilePackage('artplayer', 'umd')
test('the complete core imports without DOM or timers for ordinary and custom-UA server environments', async () => {
for (const userAgent of [undefined, '', 'iPhone Safari', 'Macintosh Safari', 'Android Chrome']) {
const module = { exports: {} }
let timers = 0
const globals = { module, exports: module.exports, setTimeout() {
timers++
} }
if (userAgent !== undefined)
globals.CUSTOM_USER_AGENT = userAgent
vm.runInNewContext(source, globals, { timeout: 5000 })
const Artplayer = module.exports
assert.equal(typeof Artplayer, 'function')
assert.equal(Artplayer.length, 2)
assert.equal(Artplayer.utils.isBrowser, false)
assert.equal(Artplayer.utils.userAgent, userAgent ?? '')
assert.equal(Artplayer.instances.length, 0)
assert.match(Artplayer.html, /art-video-player/)
assert.equal(timers, 0)
assert.equal(Object.hasOwn(globals, 'Artplayer'), false)
assert.throws(() => new Artplayer({}), error => error.message === 'Artplayer can only be used in the browser environment')
if (userAgent?.startsWith('iPhone') || userAgent?.startsWith('Macintosh'))
await assert.rejects(loadPublishedCore({ CUSTOM_USER_AGENT: userAgent }), error => error.name === 'ReferenceError')
}
})
test('the full entry preserves lazy navigator access under a non-iOS custom UA', () => {
const module = { exports: {} }
const globals = { module, exports: module.exports, CUSTOM_USER_AGENT: 'Android Chrome' }
Object.defineProperty(globals, 'navigator', { get() {
throw new Error('native navigator must remain unread')
} })
vm.runInNewContext(source, globals, { timeout: 5000 })
assert.equal(module.exports.utils.userAgent, 'Android Chrome')
assert.equal(module.exports.utils.isBrowser, false)
})
+53
View File
@@ -0,0 +1,53 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
import { test } from 'node:test'
import vm from 'node:vm'
import { build } from 'esbuild'
import { loadPublishedCore, resolveSource } from './helpers/load.js'
const output = await build({ entryPoints: [resolveSource('packages/artplayer/src/utils/compatibility')], bundle: true, write: false, platform: 'node', format: 'cjs' })
const code = output.outputFiles[0].text
function evaluate(globals = {}) {
const module = { exports: {} }
const context = Object.defineProperties({ module, exports: module.exports }, Object.getOwnPropertyDescriptors(globals))
vm.runInNewContext(code, context)
return module.exports
}
test('environment flags preserve custom UA priority, empty overrides and native touch detection', () => {
const native = { userAgent: 'Macintosh Safari', maxTouchPoints: 1 }
const globals = { navigator: native, window: {}, document: {} }
const mac = evaluate(globals)
assert.equal(mac.isSafari, true)
assert.equal(mac.isIOS13, true)
assert.equal(mac.isMobile, true)
assert.equal(mac.isBrowser, true)
const empty = evaluate({ ...globals, CUSTOM_USER_AGENT: '' })
assert.equal(empty.userAgent, '')
assert.equal(empty.isMobile, false)
const phone = evaluate({ ...globals, CUSTOM_USER_AGENT: 'iPhone Safari' })
assert.equal(phone.isIOS, true)
const excluded = evaluate({ ...globals, CUSTOM_USER_AGENT: 'iPhone Safari', window: { MSStream: {} } })
assert.equal(excluded.isIOS, false)
})
test('SSR custom iOS and Macintosh UAs do not dereference absent browser globals', async () => {
for (const ua of ['iPhone Safari', 'Macintosh Safari']) {
await assert.rejects(loadPublishedCore({ CUSTOM_USER_AGENT: ua }), error => error.name === 'ReferenceError')
const candidate = evaluate({ CUSTOM_USER_AGENT: ua })
assert.equal(candidate.isBrowser, false)
assert.equal(candidate.userAgent, ua)
assert.equal(candidate.isMobile, ua.startsWith('iPhone'))
}
assert.equal(evaluate().userAgent, '')
})
test('a custom user agent retains lazy access to the native navigator', () => {
const globals = { CUSTOM_USER_AGENT: 'Android Chrome' }
Object.defineProperty(globals, 'navigator', { get() {
throw new Error('navigator must not be read')
} })
const result = evaluate(globals)
assert.equal(result.userAgent, 'Android Chrome')
assert.equal(result.isMobile, true)
})
+92
View File
@@ -0,0 +1,92 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
import { test } from 'node:test'
import { loadModules } from './helpers/load.js'
const { attrMix, cssVarMix, typeMix, themeMix, posterMix } = await loadModules(Object.fromEntries(
['attrMix', 'cssVarMix', 'typeMix', 'themeMix', 'posterMix'].map(name => [name, `packages/artplayer/src/player/${name}`]),
))
test('attr retains dynamic keys, captured media, undefined reads and native receiver errors', () => {
const symbol = Symbol('plugin')
const video = { volume: 0.5, [symbol]: 1 }
const error = new Error('native property')
Object.defineProperty(video, 'throwing', {
get() { return undefined },
set() {
throw error
},
})
const art = { template: { $video: video } }
attrMix(art)
const attr = art.attr
art.template.$video = { volume: 1 }
assert.equal(attr('volume'), 0.5)
assert.equal(attr('volume', undefined), 0.5)
assert.equal(attr(symbol, 2), undefined)
assert.equal(video[symbol], 2)
assert.equal(attr('volume', null), undefined)
assert.equal(video.volume, null)
assert.throws(() => attr('throwing', 1), e => e === error)
assert.deepEqual(Object.getOwnPropertyDescriptor(art, 'attr'), { value: attr, configurable: false, enumerable: false, writable: false })
})
test('cssVar retains falsy reads and passes truthy values to the captured native style', (t) => {
const saved = Object.getOwnPropertyDescriptor(globalThis, 'getComputedStyle')
t.after(() => saved ? Object.defineProperty(globalThis, 'getComputedStyle', saved) : delete globalThis.getComputedStyle)
const writes = []
const player = { style: { setProperty: (...args) => {
writes.push(args)
} } }
globalThis.getComputedStyle = (element) => {
assert.equal(element, player)
return { getPropertyValue: key => `value:${key}` }
}
const art = { template: { $player: player } }
cssVarMix(art)
const cssVar = art.cssVar
art.template.$player = {}
for (const value of [undefined, null, false, 0, '', Number.NaN])
assert.equal(cssVar('--custom', value), 'value:--custom')
const object = { toString: () => 'native coercion' }
assert.equal(cssVar('--custom', object), undefined)
assert.equal(writes[0][1], object)
assert.equal(Object.getOwnPropertyDescriptor(art, 'cssVar').enumerable, false)
})
test('type and theme preserve forwarding and immutable accessor descriptors', () => {
const calls = []
const option = { type: 'mp4' }
const art = { option, cssVar(...args) {
calls.push(args)
return 'computed-theme'
} }
typeMix(art)
themeMix(art)
art.type = 'custom'
assert.equal(option.type, 'custom')
assert.equal(art.type, 'custom')
assert.equal(art.theme, 'computed-theme')
art.theme = ''
assert.deepEqual(calls, [['--art-theme'], ['--art-theme', '']])
for (const name of ['type', 'theme']) {
const descriptor = Object.getOwnPropertyDescriptor(art, name)
assert.equal(descriptor.enumerable, false)
assert.equal(descriptor.configurable, false)
assert.equal(typeof descriptor.get, 'function')
assert.equal(typeof descriptor.set, 'function')
}
})
test('poster keeps historical quoted extraction, empty fallback and original target', () => {
const poster = { style: { backgroundImage: 'url("first.png")' } }
const art = { template: { $poster: poster } }
posterMix(art)
assert.equal(art.poster, 'first.png')
poster.style.backgroundImage = 'url(unquoted.png)'
assert.equal(art.poster, '')
art.template.$poster = { style: {} }
art.poster = 'new.png'
assert.equal(poster.style.backgroundImage, 'url(new.png)')
assert.equal(Object.getOwnPropertyDescriptor(art, 'poster').configurable, false)
})
@@ -1,4 +1,7 @@
import { def } from '../utils'
// Frozen own-source fixture: ccf77c4e packages/artplayer/src/player/airplayMix.js
// Original file SHA-256: b9ce49489e75045b6a1df4d8ed28f123c1b105ac4c04fefaabcffe4353ad004d
// Only the relative utils import is relocated; the function body is unchanged.
import { def } from '../../packages/artplayer/src/utils'
export default function airplayMix(art) {
const {
@@ -1,4 +1,7 @@
import { clamp, setStyle } from '../utils'
// Frozen own-source fixture: ccf77c4e packages/artplayer/src/player/optionInit.js
// Original file SHA-256: 9d1d9d276da2403a580498c62f947373e000d9cd6e4bef5831d272c3055b5592
// Only the relative utils import is relocated; the function body is unchanged.
import { clamp, setStyle } from '../../packages/artplayer/src/utils'
export default function optionInit(art) {
const {
+17
View File
@@ -0,0 +1,17 @@
// Frozen own-source regression fixture: ccf77c4e packages/artplayer/src/utils/dom.js
// Function SHA-256: bbf533f90288190b5f058e5dfed1a25856ccfeaf3a3b6aba5d2c7f74ce947bda
export function getSafeAreaInsets() {
const div = document.createElement('div')
div.style.cssText
= 'position:fixed;top:env(safe-area-inset-top,0px);right:env(safe-area-inset-right,0px);bottom:env(safe-area-inset-bottom,0px);left:env(safe-area-inset-left,0px);pointer-events:none;visibility:hidden;'
document.body.appendChild(div)
const style = getComputedStyle(div)
const insets = {
top: Number.parseFloat(style.top) || 0,
right: Number.parseFloat(style.right) || 0,
bottom: Number.parseFloat(style.bottom) || 0,
left: Number.parseFloat(style.left) || 0,
}
div.remove()
return insets
}
+7 -3
View File
@@ -29,14 +29,18 @@ export async function loadModules(exports, root = workspace) {
return importCode(outputFiles[0].contents)
}
export async function loadPackage(name, root = workspace) {
export async function compilePackage(name, format = 'es', root = workspace) {
const project = path.join(root, 'packages', name)
const config = getViteBuildConfig({ entry: getEntryFile(project), name: getGlobalName(name), format: 'es', fileName: 'index.js', minify: false })
const config = getViteBuildConfig({ entry: getEntryFile(project), name: getGlobalName(name), format, fileName: 'index.js', minify: false })
config.build.write = false
const result = await viteBuild({ root: project, ...config })
const chunks = (Array.isArray(result) ? result : [result]).flatMap(item => item.output).filter(item => item.type === 'chunk')
assert.equal(chunks.length, 1, 'Source fixture loader requires one self-contained JS chunk')
return importCode(chunks[0].code)
return chunks[0].code
}
export async function loadPackage(name, root = workspace) {
return importCode(await compilePackage(name, 'es', root))
}
function commonJs(code, globals = {}) {
+214
View File
@@ -0,0 +1,214 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
import { test } from 'node:test'
import { loadModules } from './helpers/load.js'
const { optionInit, previousInit, airplayMix, previousAirplay, beginLifecycle, getScope } = await loadModules({
optionInit: 'packages/artplayer/src/player/optionInit',
previousInit: 'test/helpers/legacy-option-init',
airplayMix: 'packages/artplayer/src/player/airplayMix',
previousAirplay: 'test/helpers/legacy-airplay',
beginLifecycle: { file: 'packages/artplayer/src/lifecycle/instance', name: 'beginLifecycle' },
getScope: { file: 'packages/artplayer/src/lifecycle/instance', name: 'getScope' },
})
function initialHost(boundary) {
const calls = []
let art
const record = (kind, key, value) => {
calls.push([kind, key, value])
if (`${kind}:${key}` === boundary)
getScope(art).dispose()
}
const video = new Proxy({}, { set(target, key, value) {
record('video', key, value)
return Reflect.set(target, key, value)
} })
const poster = { style: new Proxy({}, { set(target, key, value) {
record('poster', key, value)
return Reflect.set(target, key, value)
} }) }
art = {
option: { moreVideoAttr: { controls: false }, muted: true, volume: 2, poster: 'poster.png', autoplay: true, playsInline: true, theme: 'green', cssVar: { '--custom': '2' }, url: 'video.mp4' },
storage: { get(key) {
record('storage', key, undefined)
return 0.4
} },
template: { $video: video, $poster: poster },
attr: (key, value) => record('attr', key, value),
cssVar: (key, value) => record('css', key, value),
get muted() { return undefined },
set muted(value) { record('player', 'muted', value) },
get url() { return undefined },
set url(value) { record('player', 'url', value) },
}
beginLifecycle(art)
return { art, calls }
}
test('option initialization retains native/DOM/write order, storage precedence and CSS mutation', () => {
const old = initialHost()
const current = initialHost()
assert.equal(previousInit(old.art), undefined)
assert.equal(optionInit(current.art), undefined)
assert.deepEqual(current.calls, old.calls)
assert.deepEqual(current.calls, [
['attr', 'controls', false],
['player', 'muted', true],
['video', 'volume', 1],
['storage', 'volume', undefined],
['video', 'volume', 0.4],
['poster', 'backgroundImage', 'url(poster.png)'],
['video', 'autoplay', true],
['video', 'playsInline', true],
['video', 'webkit-playsinline', true],
['css', '--custom', '2'],
['css', '--art-theme', 'green'],
['player', 'url', 'video.mp4'],
])
assert.deepEqual(current.art.option.cssVar, { '--custom': '2', '--art-theme': 'green' })
})
test('option initialization stops after reentrant teardown from each external write boundary', () => {
for (const boundary of ['attr:controls', 'player:muted', 'video:volume', 'storage:volume', 'poster:backgroundImage', 'video:autoplay', 'video:playsInline', 'css:--custom']) {
const old = initialHost(boundary)
const current = initialHost(boundary)
previousInit(old.art)
optionInit(current.art)
assert(current.calls.length < old.calls.length, boundary)
assert.deepEqual(current.calls.at(-1).slice(0, 2).join(':'), boundary)
assert.equal(current.calls.some(call => call[0] === 'player' && call[1] === 'url'), false)
}
})
test('initial option value getters and native conversion cannot write after teardown', () => {
for (const field of ['moreVideoAttr', 'cssVar', 'url', 'volume']) {
const { art, calls } = initialHost()
if (field === 'moreVideoAttr' || field === 'cssVar') {
const key = field === 'cssVar' ? '--custom' : 'controls'
Object.defineProperty(art.option[field], key, { enumerable: true, get() {
getScope(art).dispose()
return 'stale'
} })
}
else {
Object.defineProperty(art.option, field, { get() {
getScope(art).dispose()
return field === 'url' ? 'stale.mp4' : 1
} })
}
optionInit(art)
assert.equal(calls.some(call => call[2] === 'stale' || call[2] === 'stale.mp4'), false)
assert.equal(calls.some(call => call[0] === 'player' && call[1] === 'url'), false)
if (field === 'volume')
assert.equal(calls.some(call => call[0] === 'video' && call[1] === 'volume'), false)
}
})
function airplayHost(t, supported = true) {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'window')
Object.defineProperty(globalThis, 'window', { configurable: true, writable: true, value: supported ? { WebKitPlaybackTargetAvailabilityEvent() {} } : {} })
t.after(() => descriptor ? Object.defineProperty(globalThis, 'window', descriptor) : delete globalThis.window)
const calls = []
let listener
const video = Object.assign(new EventTarget(), { webkitShowPlaybackTargetPicker() {
assert.equal(this, video)
calls.push('picker')
} })
const art = { template: { $video: video }, proxy(target, name, callback) {
assert.equal(target, video)
assert.equal(name, 'webkitplaybacktargetavailabilitychanged')
listener = callback
}, i18n: { get: key => key }, notice: { show: '' }, emit: name => calls.push(name) }
beginLifecycle(art)
return { art, calls, availability: value => listener({ availability: value }) }
}
test('AirPlay retains availability, captured native receiver, event order and unavailable notice', (t) => {
const { art, calls, availability } = airplayHost(t)
airplayMix(art)
const airplay = art.airplay
assert.equal(airplay(), undefined)
assert.deepEqual(calls, ['picker', 'airplay'])
availability('not-available')
airplay()
assert.equal(art.notice.show, 'AirPlay Not Available')
assert.deepEqual(calls, ['picker', 'airplay'])
availability('available')
airplay()
assert.deepEqual(calls, ['picker', 'airplay', 'picker', 'airplay'])
assert.deepEqual(Object.getOwnPropertyDescriptor(art, 'airplay'), { value: airplay, configurable: false, enumerable: false, writable: false })
})
test('AirPlay installation stops after capability getters or registration destroy its owner', (t) => {
for (const install of [previousAirplay, airplayMix]) {
for (const boundary of ['capability', 'picker', 'registration']) {
const { art } = airplayHost(t)
const reads = []
Object.defineProperty(globalThis.window, 'WebKitPlaybackTargetAvailabilityEvent', { get() {
reads.push('capability')
if (boundary === 'capability')
getScope(art).dispose()
return true
} })
Object.defineProperty(art.template.$video, 'webkitShowPlaybackTargetPicker', { get() {
reads.push('picker')
if (boundary === 'picker')
getScope(art).dispose()
return () => {}
} })
art.proxy = () => {
reads.push('registration')
if (boundary === 'registration')
getScope(art).dispose()
}
install(art)
assert.equal(Object.hasOwn(art, 'airplay'), install === previousAirplay)
const expected = ['capability', 'picker', 'registration']
assert.deepEqual(reads, install === previousAirplay ? expected : expected.slice(0, expected.indexOf(boundary) + 1))
}
}
})
test('AirPlay calls and picker completion cannot publish effects after destruction', (t) => {
for (const install of [previousAirplay, airplayMix]) {
const { art, calls, availability } = airplayHost(t)
install(art)
getScope(art).dispose()
availability('available')
art.airplay()
assert.deepEqual(calls, install === previousAirplay ? ['picker', 'airplay'] : [])
}
const { art, calls } = airplayHost(t)
art.template.$video.webkitShowPlaybackTargetPicker = () => {
calls.push('picker')
getScope(art).dispose()
}
airplayMix(art)
art.airplay()
assert.deepEqual(calls, ['picker'])
})
test('AirPlay unavailable translation and picker getters respect teardown; native errors still throw', (t) => {
const unavailable = airplayHost(t, false)
unavailable.art.i18n.get = () => {
getScope(unavailable.art).dispose()
return 'stale notice'
}
airplayMix(unavailable.art)
unavailable.art.airplay()
assert.equal(unavailable.art.notice.show, '')
const { art, calls } = airplayHost(t)
airplayMix(art)
const error = new Error('picker failed')
art.template.$video.webkitShowPlaybackTargetPicker = () => {
throw error
}
assert.throws(() => art.airplay(), value => value === error)
Object.defineProperty(art.template.$video, 'webkitShowPlaybackTargetPicker', { get() {
getScope(art).dispose()
return () => calls.push('stale picker')
} })
art.airplay()
assert.deepEqual(calls, [])
})
+44 -2
View File
@@ -4,11 +4,53 @@ import { test } from 'node:test'
import { loadModules } from './helpers/load.js'
const file = 'packages/artplayer/src/lifecycle/instance'
const { beginLifecycle, destroyInstance, finishLifecycle, getScope, urlMix } = await loadModules({
...Object.fromEntries(['beginLifecycle', 'destroyInstance', 'finishLifecycle', 'getScope'].map(name => [name, { file, name }])),
const { beginLifecycle, destroyInstance, duringTemplateMount, finishLifecycle, getScope, urlMix } = await loadModules({
...Object.fromEntries(['beginLifecycle', 'destroyInstance', 'duringTemplateMount', 'finishLifecycle', 'getScope'].map(name => [name, { file, name }])),
urlMix: 'packages/artplayer/src/player/urlMix',
})
test('mounting template cleanup precedes destroy without exposing an early public property', () => {
const calls = []
const owner = { isDestroy: false, reset() {
calls.push('reset')
}, emit() {
calls.push('destroy')
} }
beginLifecycle(owner)
const template = { $video: {}, destroy(removeHtml) {
calls.push(['template', removeHtml])
} }
duringTemplateMount(owner, template, () => {
assert.equal(Object.hasOwn(owner, 'template'), false)
destroyInstance(owner, [], false, true)
})
assert.deepEqual(calls, [['template', false], 'destroy'])
assert(owner.isDestroy)
assert.equal(Object.hasOwn(owner, 'template'), false)
})
test('temporary mounting ownership is cleared after failures and completed mounting', () => {
for (const fail of [false, true]) {
let cleanups = 0
const owner = { isDestroy: false, reset() {}, emit() {} }
beginLifecycle(owner)
const template = { destroy() {
cleanups++
} }
const failure = new Error('mount failed')
const mount = () => duringTemplateMount(owner, template, () => {
if (fail)
throw failure
})
if (fail)
assert.throws(mount, error => error === failure)
else
mount()
destroyInstance(owner, [], true, true)
assert.equal(cleanups, 0)
}
})
test('instance teardown preserves order, public state and first-call removeHtml across reentry', () => {
const calls = []
const instances = []
+4 -3
View File
@@ -20,15 +20,16 @@ test('Package checks reject missing files, missing wildcard exports and internal
test('Actual isolated runtime rejects removed default exports and required files', async () => {
const { dir } = await publishedConsumer()
try {
assert.equal(runtimeConsumer(dir).checks.length, 18 + Object.keys(emitterContracts).length)
assert.equal(runtimeConsumer(dir, { baseline: true }).checks.length, 20 + Object.keys(emitterContracts).length)
assert.throws(() => runtimeConsumer(dir), /Command failed/)
const esm = path.join(dir, 'node_modules/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.mjs')
const original = fs.readFileSync(esm)
fs.writeFileSync(esm, 'export const removedDefault = true\n')
assert.throws(() => runtimeConsumer(dir), /Command failed/)
assert.throws(() => runtimeConsumer(dir, { baseline: true }), /Command failed/)
fs.writeFileSync(esm, original)
const entry = path.join(dir, 'node_modules/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.js')
fs.unlinkSync(entry)
assert.throws(() => runtimeConsumer(dir), /Command failed/)
assert.throws(() => runtimeConsumer(dir, { baseline: true }), /Command failed/)
}
finally {
removeConsumer(dir)
+6
View File
@@ -22,6 +22,12 @@ export restrictions and shared Emitter behavior. Public property descriptors and
default configuration are compared against the fixed published core/chapter.
This is a targeted compatibility check, not a complete behavioral API comparison.
Custom-UA SSR profiles run actual UMD/legacy bytes and direct ESM imports with no
window/document/navigator shim. The frozen baseline's iOS/Macintosh ReferenceErrors
are asserted only in explicit baseline mode; candidates must import successfully.
The checks retain callback-free server imports, no UMD timers, and the browser-only
constructor error. A negative fixture verifies the old package fails candidate mode.
Public, chapter-options, chapter-exports, language and legacy-plugin consumers run with
TS 5.9.3 (Node10, NodeNext CJS/ESM and Bundler) and TS 4.3.5 (Node10). NodeNext CJS also
checks import-equals/require and namespaced types. PKG-CHAPTER-04 resolved the initial
+61
View File
@@ -14,6 +14,7 @@ const vm = require('node:vm');
checks.push(id)
}
const root = __dirname
const expected = JSON.parse(fs.readFileSync(path.join(root, 'expected.json')))
const load = relative => fs.readFileSync(path.join(root, 'node_modules', relative), 'utf8')
const core = require('artplayer')
const chapter = require('artplayer-plugin-chapter')
@@ -61,6 +62,66 @@ const vm = require('node:vm');
}
}
check('SSR.constructor-browser-only', observations.constructorErrors.length === 6)
observations.customUA = []
const userAgents = ['', 'Android Chrome', 'iPhone Safari', 'Macintosh Safari']
for (const filename of ['artplayer.js', 'artplayer.legacy.js']) {
for (const userAgent of userAgents) {
const module = { exports: {} }
let timers = 0
const context = { module, exports: module.exports, CUSTOM_USER_AGENT: userAgent, setTimeout() {
timers++
} }
const historicalFailure = expected.baseline && /^(?:iPhone|Macintosh)/.test(userAgent)
const evaluate = () => vm.runInNewContext(load(`artplayer/dist/${filename}`), context, { timeout: 2000 })
if (historicalFailure) {
assert.throws(evaluate, error => error.name === 'ReferenceError')
}
else {
evaluate()
assert.equal(module.exports.utils.userAgent, userAgent)
assert.equal(module.exports.utils.isBrowser, false)
assert.equal(module.exports.instances.length, 0)
assert.throws(() => Reflect.construct(module.exports, [{}]), error => error.message === 'Artplayer can only be used in the browser environment')
}
assert.equal(timers, 0)
observations.customUA.push({ filename, userAgent, historicalFailure: !!historicalFailure, timers })
}
}
check('SSR.custom-ua-umd-legacy', observations.customUA.length === 8)
const userAgentDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'CUSTOM_USER_AGENT')
const navigatorDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'navigator')
try {
// Node exposes navigator in newer versions; this profile intentionally checks
// server imports without either browser globals or a navigator shim.
assert(delete globalThis.navigator)
for (const userAgent of userAgents) {
globalThis.CUSTOM_USER_AGENT = userAgent
const url = pathToFileURL(path.join(root, 'node_modules/artplayer/dist/artplayer.mjs'))
url.searchParams.set('custom-user-agent', userAgent)
const historicalFailure = expected.baseline && /^(?:iPhone|Macintosh)/.test(userAgent)
if (historicalFailure) {
await assert.rejects(import(url.href), error => error.name === 'ReferenceError')
}
else {
const { default: Player } = await import(url.href)
assert.equal(Player.utils.userAgent, userAgent)
assert.equal(Player.utils.isBrowser, false)
assert.equal(Player.instances.length, 0)
}
observations.customUA.push({ filename: 'artplayer.mjs', userAgent, historicalFailure: !!historicalFailure })
}
}
finally {
if (userAgentDescriptor)
Object.defineProperty(globalThis, 'CUSTOM_USER_AGENT', userAgentDescriptor)
else
delete globalThis.CUSTOM_USER_AGENT
if (navigatorDescriptor)
Object.defineProperty(globalThis, 'navigator', navigatorDescriptor)
else
delete globalThis.navigator
}
check('SSR.custom-ua-esm', observations.customUA.length === 12)
observations.languages = []
const languages = fs.readdirSync(path.join(root, 'node_modules/artplayer/dist/i18n')).filter(file => file.endsWith('.mjs')).map(file => file.slice(0, -4)).sort()
assert.deepEqual(languages, ['ar', 'cs', 'es', 'fa', 'fr', 'id', 'pl', 'ru', 'tr', 'vi', 'zh-tw'])
+84
View File
@@ -0,0 +1,84 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
import { test } from 'node:test'
import { loadModules } from './helpers/load.js'
const { Storage } = await loadModules({ Storage: 'packages/artplayer/src/storage' })
function environment(t) {
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'window')
t.after(() => descriptor ? Object.defineProperty(globalThis, 'window', descriptor) : delete globalThis.window)
const values = new Map()
const native = {
getItem: key => values.get(key) ?? null,
setItem: (key, value) => values.set(key, value),
removeItem: key => values.delete(key),
}
globalThis.window = { localStorage: native }
return { values, native, storage: new Storage() }
}
test('storage preserves envelope, mutable name/settings fields and synchronous returns', (t) => {
const { storage, values } = environment(t)
assert.deepEqual(Object.keys(storage), ['name', 'settings'])
assert.equal(storage.name, 'artplayer_settings')
assert.deepEqual(storage.get(), {})
assert.equal(storage.set('volume', 0.5), undefined)
assert.equal(values.get('artplayer_settings'), '{"volume":0.5}')
storage.set('times', { example: 7 })
assert.deepEqual(storage.get(), { volume: 0.5, times: { example: 7 } })
assert.equal(storage.del('volume'), undefined)
assert.deepEqual(storage.get(), { times: { example: 7 } })
storage.name = 'custom'
storage.set('plugin', true)
assert.equal(values.get('custom'), '{"plugin":true}')
assert.equal(storage.clear(), undefined)
assert.equal(values.has('custom'), false)
assert.equal(values.has('artplayer_settings'), true)
})
test('storage retains truthy key selection and historical JSON payload shapes', (t) => {
const { storage, values } = environment(t)
for (const payload of [[], [1, 2], 'text', 4, true, null]) {
values.set(storage.name, JSON.stringify(payload))
const expected = payload || {}
assert.deepEqual(storage.get(), expected)
assert.deepEqual(storage.get(''), expected)
assert.deepEqual(storage.get(0), expected)
}
storage.settings.fallback = 9
values.set(storage.name, 'bad JSON')
assert.equal(storage.get('fallback'), 9)
assert.equal(storage.get(), storage.settings)
})
test('storage falls back on denied native access and retains in-memory references', (t) => {
const { storage } = environment(t)
Object.defineProperty(window, 'localStorage', { get() {
throw new Error('denied')
} })
const value = { plugin: true }
assert.equal(storage.set('value', value), undefined)
assert.equal(storage.get('value'), value)
storage.del('value')
assert.equal(storage.get('value'), undefined)
const previous = storage.settings
storage.clear()
assert.notEqual(storage.settings, previous)
assert.deepEqual(storage.settings, {})
})
test('storage write failure does not merge fallback into successful native reads', (t) => {
const { storage, native, values } = environment(t)
values.set(storage.name, '{"volume":0.2}')
native.setItem = () => {
throw new Error('quota')
}
storage.set('volume', 0.7)
assert.equal(storage.settings.volume, 0.7)
assert.equal(storage.get('volume'), 0.2)
native.getItem = () => {
throw new Error('denied')
}
assert.equal(storage.get('volume'), 0.7)
})
+47
View File
@@ -0,0 +1,47 @@
import type { PipProperty } from '../../packages/artplayer/src/display/types'
import type { DisplayStates } from '../../packages/artplayer/src/media/playback'
import type { AirplayHost } from '../../packages/artplayer/src/player/airplayMix'
import type { AttributeMethods } from '../../packages/artplayer/src/player/attrMix'
import type { CssVariableMethods } from '../../packages/artplayer/src/player/cssVarMix'
import type { InitialOptionHost } from '../../packages/artplayer/src/player/optionInit'
import type { PlaybackStorage } from '../../packages/artplayer/src/plugins/auto-playback/records'
import config from '../../packages/artplayer/src/config'
import airplayMix from '../../packages/artplayer/src/player/airplayMix'
import optionInit from '../../packages/artplayer/src/player/optionInit'
import stateMix from '../../packages/artplayer/src/player/stateMix'
import Storage from '../../packages/artplayer/src/storage'
import { append, getStyle } from '../../packages/artplayer/src/utils/dom'
declare const element: HTMLElement
declare const attrs: AttributeMethods
declare const css: CssVariableMethods
declare const initial: InitialOptionHost
declare const airplay: AirplayHost
const raw: unknown = attrs.attr(Symbol('extension'))
const text: string = css.cssVar('--custom')
const numeric: number = getStyle(element, 'width')
const authored: string = getStyle(element, 'width', false)
const node: Element | ChildNode | null = append(element, '')
const storage = new Storage()
const records: PlaybackStorage = storage
declare const pip: PipProperty
const display: DisplayStates = {
mini: false,
fullscreen: false,
fullscreenWeb: false,
get pip(): HTMLElement | null | boolean { return pip.pip },
set pip(value: boolean) { pip.pip = value },
}
stateMix(display)
const stored: unknown = storage.get('extension')
config.events.push('custom-event')
config.methods = ['customMethod']
optionInit(initial)
airplayMix(airplay)
// @ts-expect-error DOM append can return a text node or null.
const guaranteedElement: HTMLElement = append(element, '')
// @ts-expect-error Raw storage reads are not validated records.
const validated: Record<string, unknown> = storage.get()
// @ts-expect-error String style reads are distinct from numeric reads.
const wrong: number = getStyle(element, 'width', false)
export { authored, display, guaranteedElement, node, numeric, raw, records, stored, text, validated, wrong }
+36
View File
@@ -0,0 +1,36 @@
import type { PlayerHost } from '../../packages/artplayer/src/player'
import type { BuiltinHost } from '../../packages/artplayer/src/plugins/builtins'
import Artplayer from '../../packages/artplayer/src'
const art = new Artplayer({ container: '#player', url: '' }, function (player) {
const same: Artplayer = player
this.option.plugins.push((core) => {
core.currentTime = '1.5'
return { name: 'source-extension', player: same }
})
})
const playerHost: PlayerHost<Artplayer> = art
const builtinHost: BuiltinHost = art
const seeking: undefined = art.seek
const quality: undefined = art.quality
const fullscreen: boolean | undefined = art.fullscreen
const noticeVisible: boolean = art.notice.show
art.notice.show = new Error('test')
art.seek = '1.5'
art.quality = [{ html: document.createElement('span'), url: '/video.mp4' }]
art.on('document:keydown', (event) => {
const key: string = event.key
return key
})
art.on('extension:event', (...values) => values)
art.emit('video:timeupdate', new Event('timeupdate'))
art.emit('extension:event', { custom: true })
// @ts-expect-error The native media event payload must be an Event.
art.emit('video:timeupdate', 'not an event')
// @ts-expect-error Reconnect errors include the attempt count.
art.emit('error', new Error('test'))
// @ts-expect-error A setter-only seek is not a readable numeric value.
const position: number = art.seek
// @ts-expect-error Notice visibility is not the last notice message.
const message: string = art.notice.show
export { builtinHost, fullscreen, message, noticeVisible, playerHost, position, quality, seeking }
+4 -6
View File
@@ -1,10 +1,8 @@
import type { PluginFactory, PluginHost } from '../../packages/artplayer/src/plugins/types'
import type Artplayer from '../../packages/artplayer/src'
import type { PluginFactory } from '../../packages/artplayer/src/plugins/types'
import Plugins from '../../packages/artplayer/src/plugins'
interface Host extends PluginHost<Host> {
id: number
}
declare const host: Host
declare const host: Artplayer
const registry = new Plugins(host)
const sync: typeof registry = registry.add(function (art) {
const id: number = this.id + art.id
@@ -12,7 +10,7 @@ const sync: typeof registry = registry.add(function (art) {
})
const asyncResult: Promise<typeof registry> = registry.add(async () => ({ name: 'async' }))
const mixed: typeof registry | Promise<typeof registry> = registry.add(() => Math.random() ? Promise.resolve(1) : 1)
const untyped: PluginFactory<Host> = () => null
const untyped: PluginFactory<Artplayer> = () => null
const unknown: typeof registry | Promise<typeof registry> = registry.add(untyped)
// @ts-expect-error Synchronous plugins do not always return a Promise.
const bad: Promise<typeof registry> = registry.add(() => ({ name: 'sync' }))
+1
View File
@@ -2,6 +2,7 @@
"extends": "./tsconfig.base.json",
"include": [
"test/types/*.ts",
"types/assets.d.ts",
"types/option-validator.d.ts"
]
}