mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
refactor(core): [CORE-20] type entry and converge initialization
This commit is contained in:
1 parent
ccf77c4ed2
commit
ee895a558f
100 files changed
+33854
-839
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
Vendored
+1
-1
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+3
-3
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+3
-2
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 及之前交付;下一任务开始前先核实本任务提交。
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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;整列与首格缩略图裁剪是明确记录的显示修复。
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 = {}) {
|
||||
|
||||
@@ -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, [])
|
||||
})
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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' }))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"extends": "./tsconfig.base.json",
|
||||
"include": [
|
||||
"test/types/*.ts",
|
||||
"types/assets.d.ts",
|
||||
"types/option-validator.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user