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

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

No files matched your search

+108 -11
View File
@@ -1,8 +1,106 @@
# Core implementation and migration map
ArtPlayer keeps its existing constructor, player mixins, plugins and DOM/CSS hooks.
The production entry is still `src/index.js`; this document marks actual migrated
boundaries rather than describing the entire core as TypeScript.
The production entry is `src/index.ts`. All owned core production modules use
TypeScript; libs/screenfull.js remains an audited third-party source file.
## Entry and bootstrap (CORE-20)
The typed entry checks subsystem composition. Seven leaf
modules now have explicit source types: storage, environment flags and the attr,
cssVar, type, theme and poster mixins. Attribute access remains a deliberately
dynamic native/proxy property boundary; cssVar retains its truthy write condition
and passes values directly to the native style setter. The poster getter keeps
its historical quoted-value extraction and empty fallback. These small property
adapters remain single files rather than acquiring redundant forwarding layers.
storage.ts preserves the artplayer_settings JSON envelope and mutable name/settings
fields. Raw reads return unknown, including historically accepted JSON primitives;
this is not schema validation. Read errors use settings by reference, write errors
update that fallback, and successful native reads do not merge it. Changing that
failure policy requires an explicit compatibility decision rather than a type cast.
utils/dom.ts is a compatibility export surface. dom/tree.ts owns node/query/class
operations; dom/styles.ts owns native style conversion; dom/event-path.ts owns
composed/fallback paths; dom/measure.ts owns geometry, flex and safe-area probes;
dom/presentation.ts composes tree operations with environment flags for icons and
tooltips. None imports the utils barrel. The public export names remain the same.
Safe-area probes are removed even when append or style access fails. The frozen
CORE-19 helper is test-only own-source provenance; npm 5.4.0 has no such helper.
Typed callers distinguish optional nodes from known builtin markup. Setting
renderers reuse component/dom.ts appendElement only where they append known element
markup or icon wrappers. Cache assertions follow existing has checks; template
assertions describe mounted required nodes and preserve historical failures if
consumers remove those nodes. Public append still returns the actual final element,
text node or null; detached remove is not silently changed to a no-op.
Environment flags retain custom UA priority and lazy navigator reads. A custom
iOS/Macintosh UA can now be evaluated without window/navigator during SSR; fallback
event paths also work without window. Use environment, storage, facade-properties
and dom-boundaries Node tests and the browser dom-boundaries spec. entry.test.js and
the isolated package consumer also exercise these profiles through complete bundles.
config/index.ts retains mutable public media-property/method/event/prototype arrays
in their original order. player/optionInit.ts has a minimal initialization host and
preserves attribute -> muted -> configured volume -> stored volume -> poster ->
autoplay -> inline flags -> theme/CSS -> source order. External getter, storage and
setter boundaries cannot continue writing once lifecycle teardown closes the owner.
player/airplayMix.ts retains native picker receiver, availability and event order;
closed instances do not invoke the picker or update a notice. Native AirPlay errors
still propagate. Its controlled tests do not certify an actual Apple receiver.
test/initialization.test.js compares frozen CORE-19 functions with the new code;
test/types/core-initialization.ts checks raw storage, mutable config and nullable DOM
results. Internal PiP consumers accept the native element/null/boolean getter and
boolean setter; the public declaration compatibility work remains in CORE-21.
The auto-playback consumer owns the legacy unvalidated record assumption locally;
Storage does not promise that arbitrary persisted values are PlaybackTimes.
player/index.ts installs URL handling first with the actual generic host, then runs
the remaining 34 steps in one ordered table. Its PlayerHost is derived from the
owning modules' required capabilities, including deferred property/callback needs;
it is not proof that those properties already exist before Player construction.
The constructor keeps its empty instance shape and stops before each later installer
when the owner closes. src/index.ts similarly stops between subsystem constructors.
Do not reorder the table or initialize public fields eagerly: descriptor insertion,
callback access and the absence of not-yet-created subsystem properties are visible.
lifecycle/instance.ts temporarily owns the mounting Template while its constructor
runs. A proxy callback can destroy the instance before art.template is assigned;
teardown still clears/marks the DOM before the destroy event, with the first
removeHtml choice. The temporary reference is released in finally. Template.init
does not adopt or restyle a returned proxy after that callback destroys its owner.
The caller still owns a proxy it creates. test/browser/initialization.spec.js covers
these boundaries plus normal descriptor/static order and early native setters.
player/properties.ts describes the actual installed descriptors, separately from
installer inputs. The entry and Icons merge declaration-only interfaces with their
classes because emitted fields would shadow installed getters or change property
order. All subsystem assignments and mutable static assignments keep their original
order. bootstrap/browser.ts owns global publication, style injection and the deferred
version log; server imports do none of those browser operations.
option/runtime.ts is the compatibility boundary between old external callback
declarations and internal module views. It keeps validated merge behavior, callback
identity and runtime receivers; it does not wrap callbacks or assert the whole player
as the legacy declaration class. template/types.ts names the PlayerTemplate contract:
canonical template nodes plus a native/media-like proxy. The entry makes one scoped
assertion for caller-provided SSR/proxy inputs. This is a compatibility precondition,
not runtime validation; no eager missing-node rejection or synthesized nodes are added.
Template itself and query helpers keep nullable results. Complete public declaration
generation and consumer views remain CORE-21 work.
events/core-types.ts names known core/native payloads while leaving extension event
names open. CoreEmission narrows a module's emitted names against that shared map.
NoticeSink describes writing arbitrary notice values without pretending its getter
returns a message. Actual Notice visibility is boolean. Internal views also retain
undefined reads for setter-only properties and pre-metadata fullscreen, optional
subtitle icons, HTMLElement quality labels, and raw storage values.
plugins/builtins.ts now checks all five builtin hosts against the real assembled
Artplayer and no longer casts entire factories through unknown. The source entry
fixture checks this composition, native event payloads and property reads; ordinary
consumer fixtures still check the separate published declarations in five modes.
## Screenshot capture (CORE-19)
@@ -115,8 +213,8 @@ including its timer and previous playback rate. Touch move/end/cancel, lock, pau
source disposal and player destruction release the press. The native media element
still performs its own defaultPlaybackRate reset on load/source replacement.
Resource cleanup must not create a second press or overwrite a newer reentrant one.
The typed builtin factory cast is confined to the existing JS constructor boundary;
CORE-20 must revisit it when the full constructor host becomes typed.
The typed constructor checks the complete builtin host in CORE-20; the former
factory casts through unknown have been removed.
Run `test/notice.test.js` and `test/fast-forward.test.js` through the Node runner;
their matching `test/types` fixtures check source inference and retained consumers.
@@ -588,7 +686,7 @@ queries; component/types.ts defines the minimal generic host and callback shapes
component/resources.ts owns each entry's child ResourceScope, DOM proxies and guarded
Emitter subscriptions. control/resources.ts specializes the shared subscriptions with
UIEvents; it does not create a second event bus. Builtins depend on these capabilities,
not the full player class. CORE-20 still has to integrate the constructor facade.
not the full player class. The typed entry checks that it supplies these capabilities.
Control index.ts preserves routing and visibility behavior. builtins.ts preserves
installation order and option/platform conditions. selector.ts owns item binding,
@@ -704,8 +802,8 @@ minimal option hosts and internal sync/Promise return types. builtins.ts install
five existing builtins in their original order and reads each condition at its turn.
It keeps the mobile/live exclusions, including no fastForward for live media. The
constructor captures option once before builtin installation, then traverses the
same live user-plugin array. All five builtin implementations are now TypeScript;
their minimum-host factory casts remain confined to this constructor boundary until CORE-20.
same live user-plugin array. All five builtin implementations are TypeScript, and
BuiltinHost checks their combined requirements against the actual constructor host.
registration.ts owns result naming and non-enumerable/non-writable/non-configurable
registry properties. Names still prefer result.name, then factory.name, then the
@@ -983,9 +1081,8 @@ descriptors, boolean shim playing precedence, duration normalization and live la
reads are unchanged. play/pause/playing capture the original media object; duration
reads template.$video on each access, as before.
The JS construction facade still assembles these hosts dynamically. Its complete
static integration belongs to CORE-20; capability consumers migrate in their own
CORE tasks. Do not assume these types already validate every third-party proxy.
The typed construction facade checks these host requirements while retaining the
original staged initialization. Do not assume these types validate every third-party proxy.
Public art.video remains the original object and retains its existing declaration
for consumer compatibility. A public proxy typing extension requires separate
consumer checks; internal code must not use that old declaration to hide a canvas.
@@ -1117,5 +1214,5 @@ core files in docs/compiled when committing a shippable core change.
For new utility behavior, extend the same old/new contract tests. Clearly separate
intentional defect corrections from preserved behavior. For timers or Blob URLs,
also identify the owner and verify cleanup in the consuming module. Remaining
constructor, playback, setting, input and capability migrations are recorded
public declaration convergence, accessibility and release validation are recorded
in refactor/tasks.json and should extend this map as they land.
+1 -1
View File
@@ -67,4 +67,4 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var n=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1",a={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var n=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),r.exports);const o=t(n),i="5.4.1";function a(t,e){let r=document.getElementById(t);r||(r=document.createElement("style"),r.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.appendChild(r)}):(document.head||document.documentElement).appendChild(r)),r.textContent=e}const s=globalThis.CUSTOM_USER_AGENT??("undefined"===typeof navigator?"":navigator.userAgent),l=/^(?:(?!chrome|android).)*safari/i.test(s),c=/iPad|iPhone|iPod/i.test(s)&&("undefined"===typeof window||!window.MSStream),d=c||s.includes("Macintosh")&&"undefined"!==typeof navigator&&navigator.maxTouchPoints>=1,p=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(s)||d,u="undefined"!==typeof window&&"undefined"!==typeof document;const h={properties:["audioTracks","autoplay","buffered","controller","controlLine truncated
+1 -1
View File
@@ -67,4 +67,4 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,n=Object.getOwnPropertyDescriptors,o=Object.getOwnPropertySymbols,i=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,s=(t,r,n)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[r]=n,l=(t,e)=>{for(var r in e||(e={}))i.call(e,r)&&s(t,r,e[r]);if(o)for(var r of o(e))a.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,n(e)),d=(t,e,r)=>new Promise((n,o)=>{var i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function u(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var p,h={exports:{}};var f=(p||(p=1,h.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),h.exports);const m=u(f),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.defineProperties,r=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,a=(e,r,n)=>r in e?t(e,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[r]=n,s=(t,e)=>{for(var r in e||(e={}))o.call(e,r)&&a(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&a(t,r,e[r]);return t},l=(t,n)=>e(t,r(n)),c=(t,e,r)=>new Promise((n,o)=>{var i=t=>{try{s(r.next(t))}catch(e){o(e)}},a=t=>{try{s(r.throw(t))}catch(e){o(e)}},s=t=>t.done?n(t.value):Promise.resolve(t.value).then(i,a);s((r=r.apply(t,e)).next())});function d(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var u,p={exports:{}};var h=(u||(u=1,p.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=t(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return"GeneratorFunction"===n(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(n(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(o=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function n(t){return t.constructor?t.constructor.name:null}function o(t,e){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return i(t,e,n),a(t,e,n),function(t,e,n){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=n.slice();c.push(r),i(s,l,c),a(s,l,c),o(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(n.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=n.slice();d.push(s),i(l,c,d),a(l,c,d),o(l,c,d)})}}(t,e,n),t}function i(t,e,n){if("string"===r(e)){var o=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return o===t}):e.toLowerCase().trim()===o))throw new Error("[Type Error]: '".concat(n.join("."),"' require '").concat(e,"' type, but got '").concat(o,"'"))}}function a(t,e,n){if("function"===r(e)){var o=e(t,r(t),n);if(!0!==o){var i=r(o);throw"string"===i?new Error(o):"error"===i?o:new Error("[Validator Error]: The scheme for '".concat(n.join("."),"' validator require return true, but got '").concat(o,"'"))}}}return o.kindOf=r,o}()),p.exports);const f=d(h),m="5.4.1";function g(t,e){let r=document.getElementById(t);r||(r=document.createElement("style"),r.id=t,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{document.head.apLine truncated
+345 -210
View File
@@ -221,6 +221,44 @@ function requireOptionValidator() {
var optionValidatorExports = requireOptionValidator();
const validator = /* @__PURE__ */ getDefaultExportFromCjs(optionValidatorExports);
const version$1 = "5.4.1";
function setStyleText(id2, style) {
let $style = document.getElementById(id2);
if (!$style) {
$style = document.createElement("style");
$style.id = id2;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
document.head.appendChild($style);
});
} else {
(document.head || document.documentElement).appendChild($style);
}
}
$style.textContent = style;
}
const customAgent = globalThis.CUSTOM_USER_AGENT;
const userAgent = customAgent ?? (typeof navigator === "undefined" ? "" : navigator.userAgent);
const isSafari = /^(?:(?!chrome|android).)*safari/i.test(userAgent);
const isIOS = /iPad|iPhone|iPod/i.test(userAgent) && (typeof window === "undefined" || !window.MSStream);
const isIOS13 = isIOS || userAgent.includes("Macintosh") && typeof navigator !== "undefined" && navigator.maxTouchPoints >= 1;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || isIOS13;
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
function publishBrowserEntry(Artplayer2, style) {
if (!isBrowser)
return;
window.Artplayer = Artplayer2;
setStyleText("artplayer-style", style);
setTimeout(() => {
if (Artplayer2.LOG_VERSION) {
console.log(
`%c ArtPlayer %c ${Artplayer2.version} %c https://artplayer.org`,
"color: #fff; background: #5f5f5f",
"color: #fff; background: #4bc729",
""
);
}
}, 100);
}
const config$1 = {
properties: [
"audioTracks",
@@ -387,6 +425,16 @@ function isClosing(owner) {
const state2 = stateOf$1(owner);
return state2.destroying || state2.scope.closed;
}
function duringTemplateMount(owner, template, initialize) {
const state2 = stateOf$1(owner);
const previous = state2.mountingTemplate;
state2.mountingTemplate = template;
try {
initialize();
} finally {
state2.mountingTemplate = previous;
}
}
function ownContainer(owner, container, rollback) {
const current2 = containers.get(container);
if (current2 && current2 !== owner)
@@ -420,7 +468,7 @@ function destroyInstance(owner, instances2, removeHtml, removeSource, failed = f
if (removeSource && owner.template?.$video)
attempt(() => owner.reset());
attempt(() => state2.scope.dispose());
attempt(() => owner.template?.destroy(removeHtml));
attempt(() => (owner.template || state2.mountingTemplate)?.destroy(removeHtml));
const index = instances2.indexOf(owner);
if (index !== -1)
instances2.splice(index, 1);
@@ -490,12 +538,122 @@ function controlEvents(art, element) {
proxy: (target, name, callback) => proxyEntry(art, element, target, name, callback)
};
}
const userAgent = globalThis?.CUSTOM_USER_AGENT ?? (typeof navigator !== "undefined" ? navigator.userAgent : "");
const isSafari = /^(?:(?!chrome|android).)*safari/i.test(userAgent);
const isIOS = /iPad|iPhone|iPod/i.test(userAgent) && !window.MSStream;
const isIOS13 = isIOS || userAgent.includes("Macintosh") && navigator.maxTouchPoints >= 1;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || isIOS13;
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
function getComposedPath(event) {
if (event.composedPath)
return event.composedPath();
const path = [];
let node = event.target;
while (node) {
path.push(node);
node = node.parentNode ?? null;
}
if (typeof window !== "undefined" && !path.includes(window))
path.push(window);
return path;
}
function includeFromEvent(event, target) {
return getComposedPath(event).includes(target);
}
function supportsFlex() {
const div = document.createElement("div");
div.style.display = "flex";
return div.style.display === "flex";
}
function getRect(el) {
return el.getBoundingClientRect();
}
function isInViewport(el, offset = 0) {
const rect = el.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
const vertInView = rect.top - offset <= windowHeight && rect.top + rect.height + offset >= 0;
const horInView = rect.left - offset <= windowWidth + offset && rect.left + rect.width + offset >= 0;
return vertInView && horInView;
}
function getSafeAreaInsets() {
const div = document.createElement("div");
div.style.cssText = "position:fixed;top:env(safe-area-inset-top,0px);right:env(safe-area-inset-right,0px);bottom:env(safe-area-inset-bottom,0px);left:env(safe-area-inset-left,0px);pointer-events:none;visibility:hidden;";
try {
document.body.appendChild(div);
const style = getComputedStyle(div);
return {
top: Number.parseFloat(style.top) || 0,
right: Number.parseFloat(style.right) || 0,
bottom: Number.parseFloat(style.bottom) || 0,
left: Number.parseFloat(style.left) || 0
};
} finally {
div.remove();
}
}
function query(selector, parent = document) {
return parent.querySelector(selector);
}
function queryAll(selector, parent = document) {
return Array.from(parent.querySelectorAll(selector));
}
function addClass(target, className2) {
return target.classList.add(className2);
}
function removeClass(target, className2) {
return target.classList.remove(className2);
}
function hasClass(target, className2) {
return target.classList.contains(className2);
}
function append(parent, child) {
if (child instanceof Element) {
parent.appendChild(child);
} else {
parent.insertAdjacentHTML("beforeend", String(child));
}
return parent.lastElementChild || parent.lastChild;
}
function remove(child) {
return child.parentNode.removeChild(child);
}
function siblings(target) {
return Array.from(target.parentElement.children).filter((item) => item !== target);
}
function inverseClass(target, className2) {
siblings(target).forEach((item) => removeClass(item, className2));
addClass(target, className2);
}
function replaceElement(newChild, oldChild) {
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
}
function createElement(tag) {
return document.createElement(tag);
}
function tooltip(target, msg, pos = "top") {
if (isMobile)
return;
target.setAttribute("aria-label", msg);
addClass(target, "hint--rounded");
addClass(target, `hint--${pos}`);
}
function getIcon(key = "", html2 = "") {
const icon = createElement("i");
addClass(icon, "art-icon");
addClass(icon, `art-icon-${key}`);
append(icon, html2);
return icon;
}
function setStyle(element, key, value) {
const style = element.style;
style[key] = value;
return element;
}
function setStyles(element, styles) {
for (const key in styles)
setStyle(element, key, styles[key]);
return element;
}
function getStyle(element, key, numberType = true) {
const value = window.getComputedStyle(element, null).getPropertyValue(key);
return numberType ? Number.parseFloat(value) : value;
}
function requestImage(url, scale, owner) {
return new Promise((resolve, reject) => {
const request = owner ? owner.child() : new ResourceScope();
@@ -622,136 +780,6 @@ function loadImg(url, scale) {
function loadThumbnailImage(url, scale, owner) {
return requestImage(url, scale, owner);
}
function setStyleText(id2, style) {
let $style = document.getElementById(id2);
if (!$style) {
$style = document.createElement("style");
$style.id = id2;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
document.head.appendChild($style);
});
} else {
(document.head || document.documentElement).appendChild($style);
}
}
$style.textContent = style;
}
function query(selector, parent = document) {
return parent.querySelector(selector);
}
function queryAll(selector, parent = document) {
return Array.from(parent.querySelectorAll(selector));
}
function addClass(target, className2) {
return target.classList.add(className2);
}
function removeClass(target, className2) {
return target.classList.remove(className2);
}
function hasClass(target, className2) {
return target.classList.contains(className2);
}
function append(parent, child) {
if (child instanceof Element) {
parent.appendChild(child);
} else {
parent.insertAdjacentHTML("beforeend", String(child));
}
return parent.lastElementChild || parent.lastChild;
}
function remove(child) {
return child.parentNode.removeChild(child);
}
function setStyle(element, key, value) {
element.style[key] = value;
return element;
}
function setStyles(element, styles) {
for (const key in styles) {
setStyle(element, key, styles[key]);
}
return element;
}
function getStyle(element, key, numberType = true) {
const value = window.getComputedStyle(element, null).getPropertyValue(key);
return numberType ? Number.parseFloat(value) : value;
}
function siblings(target) {
return Array.from(target.parentElement.children).filter((item) => item !== target);
}
function inverseClass(target, className2) {
siblings(target).forEach((item) => removeClass(item, className2));
addClass(target, className2);
}
function tooltip(target, msg, pos = "top") {
if (isMobile)
return;
target.setAttribute("aria-label", msg);
addClass(target, "hint--rounded");
addClass(target, `hint--${pos}`);
}
function isInViewport(el, offset = 0) {
const rect = el.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
const vertInView = rect.top - offset <= windowHeight && rect.top + rect.height + offset >= 0;
const horInView = rect.left - offset <= windowWidth + offset && rect.left + rect.width + offset >= 0;
return vertInView && horInView;
}
function includeFromEvent(event, target) {
return getComposedPath(event).includes(target);
}
function replaceElement(newChild, oldChild) {
oldChild.parentNode.replaceChild(newChild, oldChild);
return newChild;
}
function createElement(tag) {
return document.createElement(tag);
}
function getIcon(key = "", html2 = "") {
const icon = createElement("i");
addClass(icon, "art-icon");
addClass(icon, `art-icon-${key}`);
append(icon, html2);
return icon;
}
function supportsFlex() {
const div = document.createElement("div");
div.style.display = "flex";
return div.style.display === "flex";
}
function getRect(el) {
return el.getBoundingClientRect();
}
function getComposedPath(event) {
if (event.composedPath)
return event.composedPath();
const path = [];
let node = event.target;
while (node) {
path.push(node);
node = node.parentNode;
}
if (!path.includes(window) && window !== void 0) {
path.push(window);
}
return path;
}
function getSafeAreaInsets() {
const div = document.createElement("div");
div.style.cssText = "position:fixed;top:env(safe-area-inset-top,0px);right:env(safe-area-inset-right,0px);bottom:env(safe-area-inset-bottom,0px);left:env(safe-area-inset-left,0px);pointer-events:none;visibility:hidden;";
document.body.appendChild(div);
const style = getComputedStyle(div);
const insets = {
top: Number.parseFloat(style.top) || 0,
right: Number.parseFloat(style.right) || 0,
bottom: Number.parseFloat(style.bottom) || 0,
left: Number.parseFloat(style.left) || 0
};
div.remove();
return insets;
}
class ArtPlayerError extends Error {
constructor(message, context) {
super(message);
@@ -3444,7 +3472,12 @@ function resolveOption(input, defaults2) {
merged.container = input.container;
return validator(merged, scheme);
}
function resolveRuntimeOption(input, defaults2) {
return resolveOption(input, defaults2);
}
function airplayMix(art) {
if (isClosing(art))
return;
const {
i18n,
notice,
@@ -3452,8 +3485,16 @@ function airplayMix(art) {
template: { $video }
} = art;
let available = true;
if (window.WebKitPlaybackTargetAvailabilityEvent && $video.webkitShowPlaybackTargetPicker) {
const supported = window.WebKitPlaybackTargetAvailabilityEvent;
if (isClosing(art))
return;
const picker = supported && $video.webkitShowPlaybackTargetPicker;
if (isClosing(art))
return;
if (picker) {
proxy($video, "webkitplaybacktargetavailabilitychanged", (event) => {
if (isClosing(art))
return;
switch (event.availability) {
case "available":
available = true;
@@ -3466,13 +3507,23 @@ function airplayMix(art) {
} else {
available = false;
}
if (isClosing(art))
return;
def(art, "airplay", {
value() {
if (isClosing(art))
return;
if (available) {
$video.webkitShowPlaybackTargetPicker();
art.emit("airplay");
const picker2 = $video.webkitShowPlaybackTargetPicker;
if (isClosing(art))
return;
Reflect.apply(picker2, $video, []);
if (!isClosing(art))
art.emit("airplay");
} else {
notice.show = i18n.get("AirPlay Not Available");
const message = i18n.get("AirPlay Not Available");
if (!isClosing(art))
notice.show = message;
}
}
});
@@ -3534,9 +3585,10 @@ function attrMix(art) {
} = art;
def(art, "attr", {
value(key, value) {
const target = $video;
if (value === void 0)
return $video[key];
$video[key] = value;
return target[key];
target[key] = value;
}
});
}
@@ -4678,41 +4730,93 @@ function miniMix(art) {
def(art, "mini", mini(art));
}
function optionInit(art) {
if (isClosing(art))
return;
const {
option,
storage,
template: { $video, $poster }
} = art;
for (const key in option.moreVideoAttr) {
art.attr(key, option.moreVideoAttr[key]);
if (isClosing(art))
return;
const value = option.moreVideoAttr[key];
if (isClosing(art))
return;
art.attr(key, value);
}
if (isClosing(art))
return;
if (option.muted) {
art.muted = option.muted;
const value = option.muted;
if (isClosing(art))
return;
art.muted = value;
}
if (isClosing(art))
return;
if (option.volume) {
$video.volume = clamp(option.volume, 0, 1);
const value = clamp(option.volume, 0, 1);
if (isClosing(art))
return;
$video.volume = value;
}
if (isClosing(art))
return;
const volumeStorage = storage.get("volume");
if (isClosing(art))
return;
if (typeof volumeStorage === "number") {
$video.volume = clamp(volumeStorage, 0, 1);
}
if (isClosing(art))
return;
if (option.poster) {
setStyle($poster, "backgroundImage", `url(${option.poster})`);
const value = `url(${option.poster})`;
if (isClosing(art))
return;
setStyle($poster, "backgroundImage", value);
}
if (isClosing(art))
return;
if (option.autoplay) {
$video.autoplay = option.autoplay;
const value = option.autoplay;
if (isClosing(art))
return;
$video.autoplay = value;
}
if (isClosing(art))
return;
if (option.playsInline) {
if (isClosing(art))
return;
$video.playsInline = true;
if (isClosing(art))
return;
$video["webkit-playsinline"] = true;
}
if (isClosing(art))
return;
if (option.theme) {
option.cssVar["--art-theme"] = option.theme;
const styles = option.cssVar;
const theme = option.theme;
if (isClosing(art))
return;
styles["--art-theme"] = theme;
}
for (const key in option.cssVar) {
art.cssVar(key, option.cssVar[key]);
if (isClosing(art))
return;
const value = option.cssVar[key];
if (isClosing(art))
return;
art.cssVar(key, value);
}
art.url = option.url;
if (isClosing(art))
return;
const url = option.url;
if (!isClosing(art))
art.url = url;
}
function pauseMix(art) {
const {
@@ -5614,43 +5718,52 @@ function volumeMix(art) {
}
});
}
const installers = [
attrMix,
playMix,
pauseMix,
toggleMix,
seekMix,
volumeMix,
currentTimeMix,
durationMix,
switchMix,
playbackRateMix,
aspectRatioMix,
screenshotMix,
fullscreenMix,
fullscreenWebMix,
pipMix,
loadedMix,
playedMix,
playingMix,
autoSizeMix,
rectMix,
flipMix,
miniMix,
posterMix,
autoHeightMix,
cssVarMix,
themeMix,
typeMix,
stateMix,
subtitleOffsetMix,
airplayMix,
qualityMix,
thumbnailsMix,
eventInit,
optionInit
];
class Player {
constructor(art) {
if (isClosing(art))
return;
urlMix(art);
attrMix(art);
playMix(art);
pauseMix(art);
toggleMix(art);
seekMix(art);
volumeMix(art);
currentTimeMix(art);
durationMix(art);
switchMix(art);
playbackRateMix(art);
aspectRatioMix(art);
screenshotMix(art);
fullscreenMix(art);
fullscreenWebMix(art);
pipMix(art);
loadedMix(art);
playedMix(art);
playingMix(art);
autoSizeMix(art);
rectMix(art);
flipMix(art);
miniMix(art);
posterMix(art);
autoHeightMix(art);
cssVarMix(art);
themeMix(art);
typeMix(art);
stateMix(art);
subtitleOffsetMix(art);
airplayMix(art);
qualityMix(art);
thumbnailsMix(art);
eventInit(art);
optionInit(art);
for (const install of installers) {
if (isClosing(art))
return;
install(art);
}
}
}
const owners$2 = /* @__PURE__ */ new WeakMap();
@@ -6136,9 +6249,8 @@ function installBuiltins(registry, option) {
registry.add(lock);
if (!isClosing(registry.art) && option.autoPlayback && !option.isLive)
registry.add(autoPlayback);
if (!isClosing(registry.art) && option.autoOrientation && isMobile) {
if (!isClosing(registry.art) && option.autoOrientation && isMobile)
registry.add(autoOrientation);
}
if (!isClosing(registry.art) && option.fastForward && isMobile && !option.isLive)
registry.add(fastForward);
}
@@ -7112,7 +7224,7 @@ function createSettingHeader(setting2, item) {
setStyle($item, "height", `${SETTING_ITEM_HEIGHT}px`);
addClass($item, "art-setting-item");
addClass($item, "art-setting-item-back");
const $left = append($item, '<div class="art-setting-item-left"></div>');
const $left = appendElement($item, '<div class="art-setting-item-left"></div>');
const $icon = document.createElement("div");
addClass($icon, "art-setting-item-left-icon");
append($icon, arrowLeft2);
@@ -7153,8 +7265,8 @@ function createSettingItem(setting2, item, isUpdate = false) {
$item.dataset.value = String(item.value || "");
if (!current2())
return;
const $left = append($item, '<div class="art-setting-item-left"></div>');
const $right = append($item, '<div class="art-setting-item-right"></div>');
const $left = appendElement($item, '<div class="art-setting-item-left"></div>');
const $right = appendElement($item, '<div class="art-setting-item-right"></div>');
const $icon = document.createElement("div");
addClass($icon, "art-setting-item-left-icon");
switch (type) {
@@ -7204,8 +7316,8 @@ function createSettingItem(setting2, item, isUpdate = false) {
case "switch": {
const $switch = document.createElement("div");
addClass($switch, "art-setting-item-right-icon");
const $switchOn = append($switch, icons.switchOn);
const $switchOff = append($switch, icons.switchOff);
const $switchOn = appendElement($switch, icons.switchOn);
const $switchOff = appendElement($switch, icons.switchOff);
const initialSwitch = item.switch;
if (!current2())
return;
@@ -7992,8 +8104,10 @@ class Template {
);
this.query = this.query.bind(this);
ownContainer(art, this.$container, captureTemplate(this.$container));
this.$container.dataset.artId = String(art.id);
this.init();
duringTemplateMount(art, this, () => {
this.$container.dataset.artId = String(art.id);
this.init();
});
}
static get html() {
return html;
@@ -8009,6 +8123,8 @@ class Template {
bindNodes(this);
if (option.proxy) {
const video = option.proxy.call(this.art, this.art);
if (isClosing(this.art))
return;
assertProxy(video);
replaceElement(video, this.$video);
video.className = "art-video";
@@ -8097,7 +8213,7 @@ class Artplayer extends Emitter {
throw new Error("Artplayer can only be used in the browser environment");
}
this.id = ++id;
this.option = resolveOption(option, Artplayer.option);
this.option = resolveRuntimeOption(option, Artplayer.option);
this.isLock = false;
this.isReady = false;
this.isFocus = false;
@@ -8107,21 +8223,53 @@ class Artplayer extends Emitter {
beginLifecycle(this);
try {
this.template = new Template(this);
if (getScope(this).closed)
return;
this.events = new Events(this);
this.storage = new Storage(this);
if (getScope(this).closed)
return;
this.storage = new Storage();
if (getScope(this).closed)
return;
this.icons = new Icons(this);
if (getScope(this).closed)
return;
this.i18n = new I18n(this);
if (getScope(this).closed)
return;
this.notice = new Notice(this);
if (getScope(this).closed)
return;
this.player = new Player(this);
if (getScope(this).closed)
return;
this.layers = new Layer(this);
if (getScope(this).closed)
return;
this.controls = new Control(this);
if (getScope(this).closed)
return;
this.contextmenu = new Contextmenu(this);
if (getScope(this).closed)
return;
this.subtitle = new Subtitle(this);
if (getScope(this).closed)
return;
this.info = new Info(this);
if (getScope(this).closed)
return;
this.loading = new Loading(this);
if (getScope(this).closed)
return;
this.hotkey = new Hotkey(this);
if (getScope(this).closed)
return;
this.mask = new Mask(this);
if (getScope(this).closed)
return;
this.setting = new Setting(this);
if (getScope(this).closed)
return;
this.plugins = new Plugins(this);
if (getScope(this).closed)
return;
@@ -8227,20 +8375,7 @@ Artplayer.FULLSCREEN_WEB_IN_BODY = true;
Artplayer.LOG_VERSION = true;
Artplayer.USE_RAF = false;
Artplayer.REMOVE_SRC_WHEN_DESTROY = true;
if (isBrowser) {
window.Artplayer = Artplayer;
setStyleText("artplayer-style", css);
setTimeout(() => {
if (Artplayer.LOG_VERSION) {
console.log(
`%c ArtPlayer %c ${Artplayer.version} %c https://artplayer.org`,
"color: #fff; background: #5f5f5f",
"color: #fff; background: #4bc729",
""
);
}
}, 100);
}
publishBrowserEntry(Artplayer, css);
export {
Artplayer as default
};
@@ -0,0 +1,21 @@
import { setStyleText } from '../style/inject'
import { isBrowser } from '../utils/compatibility'
export default function publishBrowserEntry<Constructor extends { version: string, LOG_VERSION: boolean }>(Artplayer: Constructor, style: string): void {
if (!isBrowser)
return
(window as Window & { Artplayer?: Constructor }).Artplayer = Artplayer
setStyleText('artplayer-style', style)
setTimeout(() => {
if (Artplayer.LOG_VERSION) {
// eslint-disable-next-line no-console
console.log(
`%c ArtPlayer %c ${Artplayer.version} %c https://artplayer.org`,
'color: #fff; background: #5f5f5f',
'color: #fff; background: #4bc729',
'',
)
}
}, 100)
}
@@ -1,4 +1,12 @@
export default {
export interface MediaConfig {
properties: string[]
methods: string[]
events: string[]
prototypes: string[]
}
// Public arrays intentionally remain mutable and retain their historical order.
const config: MediaConfig = {
properties: [
'audioTracks',
'autoplay',
@@ -79,3 +87,5 @@ export default {
'webkitExitFullscreen',
],
}
export default config
+2
View File
@@ -18,6 +18,8 @@ import { checkSelector, renderSelector } from './selector'
export default class Control extends Component<ControlHost> {
declare isHover: boolean
declare timer: number
declare setting?: HTMLDivElement
declare thumbnails?: HTMLDivElement
constructor(art: ControlHost) {
super(art)
@@ -7,7 +7,8 @@ export interface ProgressPositionHost {
isRotate: boolean
top: number
height: number
seek: number | string
get seek(): undefined
set seek(value: number | string)
emit: (name: 'setBar', kind: 'played', percentage: number, event: ProgressEvent) => unknown
}
+2 -2
View File
@@ -37,7 +37,7 @@ export function checkSelector(target?: SelectorItem): void {
const item = target.$control_option![index]!
item.default = item === target
if (item.default)
inverseClass(item.$control_item, 'art-current')
inverseClass(item.$control_item!, 'art-current')
}
}
@@ -69,7 +69,7 @@ export function renderSelector(art: ControlHost, check: (target?: SelectorItem)
if (scope.closed)
return
const path = getComposedPath(event)
const item = option.selector!.find(item => path.includes(item.$control_item))
const item = option.selector!.find(item => path.includes(item.$control_item!))
if (!item)
return
const current = ++generation
+6 -4
View File
@@ -1,5 +1,6 @@
import type { SubscriptionHost } from '../component/resources'
import type { ComponentHost, EntryInput, EntryOption } from '../component/types'
import type { PipProperty } from '../display/types'
export interface UIEvents {
'mousemove': [MouseEvent]
@@ -27,11 +28,11 @@ export interface UIEvents {
'resize': []
}
export interface QualityItem { html: string, url: string, default?: boolean }
export interface QualityItem { html: string | HTMLElement, url: string, default?: boolean }
type IconName = 'play' | 'pause' | 'volume' | 'volumeClose' | 'fullscreenOn' | 'fullscreenOff' | 'fullscreenWebOn' | 'fullscreenWebOff' | 'pip' | 'setting' | 'screenshot' | 'airplay'
export interface ControlHost extends ComponentHost, SubscriptionHost<UIEvents> {
export interface ControlHost extends ComponentHost, SubscriptionHost<UIEvents>, PipProperty {
template: {
$player: HTMLDivElement
$bottom: HTMLDivElement
@@ -67,10 +68,11 @@ export interface ControlHost extends ComponentHost, SubscriptionHost<UIEvents> {
loaded: number
top: number
height: number
fullscreen: boolean
fullscreen?: boolean
fullscreenWeb: boolean
pip: boolean
get seek(): undefined
set seek(second: number)
get quality(): undefined
set quality(value: QualityItem[])
play: () => unknown
pause: () => unknown
@@ -1,11 +1,12 @@
import type { SubscriptionHost } from '../component/resources'
import type { NoticeSink } from '../notice'
export interface OrientationHost extends SubscriptionHost<{ fullscreenWeb: [boolean], fullscreen: [boolean] }> {
constructor: { AUTO_ORIENTATION_TIME?: number }
template: { $player: HTMLElement, $video: { readonly videoWidth: number, readonly videoHeight: number } }
fullscreenWeb: boolean
isRotate: boolean
notice: { set show(value: unknown) }
notice: NoticeSink
emit: (name: 'resize') => unknown
}
+9 -9
View File
@@ -1,23 +1,23 @@
import type { SubscriptionHost } from '../component/resources'
import type { CoreEmission } from '../events/core-types'
import type { OptionalMediaCapabilities } from '../media/types'
import type { NoticeSink } from '../notice'
export interface WebFullscreenHost {
export interface WebFullscreenHost extends CoreEmission<'fullscreenWeb' | 'resize'> {
constructor: { FULLSCREEN_WEB_IN_BODY: boolean }
template: { $container: HTMLElement, $player: HTMLElement }
state: string
emit: (name: 'fullscreenWeb' | 'resize', ...args: unknown[]) => unknown
}
export interface WebFullscreenProperty {
fullscreenWeb: boolean
}
export interface FullscreenHost {
export interface FullscreenHost extends CoreEmission<'fullscreen' | 'fullscreenError' | 'resize'> {
template: { $player: HTMLElement, $video: HTMLElement & OptionalMediaCapabilities }
state: string
notice: { set show(value: unknown) }
notice: NoticeSink
i18n: { get: (key: string) => string }
emit: (name: 'fullscreen' | 'fullscreenError' | 'resize', ...args: unknown[]) => unknown
once: (name: 'video:loadedmetadata', listener: () => void) => unknown
on: (name: 'document:webkitfullscreenchange', listener: () => void) => unknown
off: (name: 'video:loadedmetadata' | 'document:webkitfullscreenchange', listener: () => void) => unknown
@@ -37,7 +37,7 @@ export interface NativeFullscreenAdapter {
export interface PipHost {
template: { $video: HTMLElement & OptionalMediaCapabilities & { disablePictureInPicture?: boolean } }
state: string
notice: { set show(value: unknown) }
notice: NoticeSink
i18n: { get: (key: string) => string }
emit: (name: 'pip', value: boolean) => unknown
}
@@ -48,9 +48,9 @@ export interface PipProperty {
}
export interface MiniEvents {
'video:playing': []
'video:pause': []
'video:timeupdate': []
'video:playing': [Event]
'video:pause': [Event]
'video:timeupdate': [Event]
'document:mousemove': [MouseEvent]
'document:mouseup': [MouseEvent]
}
+22
View File
@@ -0,0 +1,22 @@
interface EventPathSource {
target: EventTarget | null
composedPath?: () => EventTarget[]
}
export function getComposedPath(event: EventPathSource): EventTarget[] {
if (event.composedPath)
return event.composedPath()
const path: EventTarget[] = []
let node = event.target
while (node) {
path.push(node)
node = (node as EventTarget & { parentNode?: EventTarget | null }).parentNode ?? null
}
if (typeof window !== 'undefined' && !path.includes(window))
path.push(window)
return path
}
export function includeFromEvent(event: EventPathSource, target: EventTarget | null | undefined): boolean {
return getComposedPath(event).includes(target as EventTarget)
}
+37
View File
@@ -0,0 +1,37 @@
export function supportsFlex(): boolean {
const div = document.createElement('div')
div.style.display = 'flex'
return div.style.display === 'flex'
}
export function getRect(el: Pick<Element, 'getBoundingClientRect'>): DOMRect {
return el.getBoundingClientRect()
}
export function isInViewport(el: Pick<Element, 'getBoundingClientRect'>, offset = 0): boolean {
const rect = el.getBoundingClientRect()
const windowHeight = window.innerHeight || document.documentElement.clientHeight
const windowWidth = window.innerWidth || document.documentElement.clientWidth
const vertInView = rect.top - offset <= windowHeight && rect.top + rect.height + offset >= 0
const horInView = rect.left - offset <= windowWidth + offset && rect.left + rect.width + offset >= 0
return vertInView && horInView
}
export function getSafeAreaInsets(): { top: number, right: number, bottom: number, left: number } {
const div = document.createElement('div')
div.style.cssText
= 'position:fixed;top:env(safe-area-inset-top,0px);right:env(safe-area-inset-right,0px);bottom:env(safe-area-inset-bottom,0px);left:env(safe-area-inset-left,0px);pointer-events:none;visibility:hidden;'
try {
document.body.appendChild(div)
const style = getComputedStyle(div)
return {
top: Number.parseFloat(style.top) || 0,
right: Number.parseFloat(style.right) || 0,
bottom: Number.parseFloat(style.bottom) || 0,
left: Number.parseFloat(style.left) || 0,
}
}
finally {
div.remove()
}
}
@@ -0,0 +1,18 @@
import { isMobile } from '../utils/compatibility'
import { addClass, append, createElement } from './tree'
export function tooltip(target: Element, msg: string | number, pos = 'top'): void {
if (isMobile)
return
target.setAttribute('aria-label', msg as string)
addClass(target, 'hint--rounded')
addClass(target, `hint--${pos}`)
}
export function getIcon(key = '', html: string | HTMLElement = ''): HTMLElement {
const icon = createElement('i')
addClass(icon, 'art-icon')
addClass(icon, `art-icon-${key}`)
append(icon, html)
return icon
}
+22
View File
@@ -0,0 +1,22 @@
interface StyledElement { style: CSSStyleDeclaration }
export function setStyle<T extends StyledElement>(element: T, key: PropertyKey, value: unknown): T {
// Preserve bracket assignment, including native conversion and thrown setters.
const style = element.style as unknown as Record<PropertyKey, unknown>
style[key] = value
return element
}
export function setStyles<T extends StyledElement>(element: T, styles: object): T {
for (const key in styles)
setStyle(element, key, (styles as Record<string, unknown>)[key])
return element
}
export function getStyle(element: Element, key: string, numberType?: true): number
export function getStyle(element: Element, key: string, numberType: false): string
export function getStyle(element: Element, key: string, numberType: boolean): number | string
export function getStyle(element: Element, key: string, numberType = true): number | string {
const value = window.getComputedStyle(element, null).getPropertyValue(key)
return numberType ? Number.parseFloat(value) : value
}
+54
View File
@@ -0,0 +1,54 @@
export function query<T extends Element = Element>(selector: string, parent: ParentNode = document): T | null {
return parent.querySelector<T>(selector)
}
export function queryAll<T extends Element = Element>(selector: string, parent: ParentNode = document): T[] {
return Array.from(parent.querySelectorAll<T>(selector))
}
export function addClass(target: Pick<Element, 'classList'>, className: string): void {
return target.classList.add(className)
}
export function removeClass(target: Pick<Element, 'classList'>, className: string): void {
return target.classList.remove(className)
}
export function hasClass(target: Pick<Element, 'classList'>, className: string): boolean {
return target.classList.contains(className)
}
export function append(parent: Element, child: unknown): Element | ChildNode | null {
if (child instanceof Element) {
parent.appendChild(child)
}
else {
parent.insertAdjacentHTML('beforeend', String(child))
}
return parent.lastElementChild || parent.lastChild
}
export function remove<T extends Node>(child: T): T {
// Detached nodes retain their historical failure rather than becoming no-ops.
return child.parentNode!.removeChild(child)
}
export function siblings(target: Element): Element[] {
return Array.from(target.parentElement!.children).filter(item => item !== target)
}
export function inverseClass(target: Element, className: string): void {
siblings(target).forEach(item => removeClass(item, className))
addClass(target, className)
}
export function replaceElement<T extends Node>(newChild: T, oldChild: Node): T {
oldChild.parentNode!.replaceChild(newChild, oldChild)
return newChild
}
export function createElement<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElementTagNameMap[K]
export function createElement(tag: string): HTMLElement
export function createElement(tag: string): HTMLElement {
return document.createElement(tag)
}
@@ -0,0 +1,30 @@
import type { UIEvents } from '../control/types'
// The emitter stays open to extension names. Known native events retain their
// payloads so subsystem subscriptions are checked against the assembled player.
export interface CoreEvents extends UIEvents, Record<PropertyKey, unknown[]> {
[name: `video:${string}`]: [event: Event]
'ready': []
'error': [error: unknown, attempt: number]
'fullscreenError': [error: unknown]
'destroy': []
'restart': [string]
'focus': [Event]
'lock': [boolean]
'view': [boolean]
'subtitleOffset': [number]
'seek': [time: number, requestedTime: number | string]
'document:click': [Event]
'document:contextmenu': [Event]
'document:keydown': [KeyboardEvent]
'document:touchmove': [Event]
'document:touchend': [Event]
'document:touchcancel': [Event]
'window:resize': [Event]
'window:orientationchange': [Event]
'window:scroll': [Event]
}
export interface CoreEmission<Names extends keyof CoreEvents = keyof CoreEvents> {
emit: <Name extends Names>(name: Name, ...args: [...CoreEvents[Name]]) => unknown
}
+1 -1
View File
@@ -5,6 +5,6 @@ export default function moveInit(art: MoveHost, events: Pick<PointerRegistry, 'p
const { $player } = art.template
events.proxy($player, 'mousemove', (event) => {
if (!isClosing(art))
art.emit('mousemove', event)
art.emit('mousemove', event as MouseEvent)
})
}
@@ -1,4 +1,5 @@
import type { SubscriptionHost } from '../component/resources'
import type { NoticeSink } from '../notice'
export interface ResizeEvents {
'resize': []
@@ -13,7 +14,7 @@ export interface ResizeHost extends SubscriptionHost<ResizeEvents> {
aspectRatio: string
state: string
autoSize: () => unknown
notice: { show: string }
notice: NoticeSink
emit: (name: 'resize') => unknown
}
+8 -2
View File
@@ -1,10 +1,14 @@
import { def, getIcon } from '../utils'
import defaults from './defaults'
export default class Icons {
// eslint-disable-next-line ts/no-unsafe-declaration-merging -- The constructor defines a getter for every default key without class fields.
interface Icons extends Record<keyof typeof defaults, HTMLElement> {}
// eslint-disable-next-line ts/no-unsafe-declaration-merging -- The merged mapped interface describes the getters installed below.
class Icons {
readonly [key: string]: HTMLElement
constructor(art: { option: { icons: Record<string, string | HTMLElement> } }) {
constructor(art: { option: { icons: Readonly<Record<string, string | HTMLElement | undefined>> } }) {
const icons: Record<string, string | HTMLElement | undefined> = { ...defaults, ...art.option.icons }
for (const key in icons) {
def(this, key, {
@@ -13,3 +17,5 @@ export default class Icons {
}
}
}
export default Icons
@@ -1,5 +1,11 @@
import type { CoreEvents } from './events/core-types'
import type { RuntimeOption } from './option/runtime'
import type { OptionInput } from './option/types'
import type { PlayerProperties } from './player/properties'
import type { PlayerTemplate } from './template/types'
import validator from 'option-validator'
import { version } from '../package.json'
import publishBrowserEntry from './bootstrap/browser'
import config from './config'
import Contextmenu from './contextmenu'
import Control from './control'
@@ -14,7 +20,7 @@ import Loading from './loading'
import Mask from './mask'
import Notice from './notice'
import createDefaults from './option/defaults'
import resolveOption from './option/resolve'
import resolveRuntimeOption from './option/runtime'
import Player from './player'
import Plugins from './plugins'
import scheme from './scheme'
@@ -27,9 +33,76 @@ import * as utils from './utils'
import Emitter from './utils/emitter'
let id = 0
const instances = []
export default class Artplayer extends Emitter {
constructor(option, readyCallback) {
const instances: Artplayer[] = []
// eslint-disable-next-line ts/no-unsafe-declaration-merging -- Player installs these descriptors; emitting fields would shadow them.
interface Artplayer extends PlayerProperties {
constructor: typeof Artplayer
}
// eslint-disable-next-line ts/no-unsafe-declaration-merging -- The merged interface describes installed descriptors and the inherited constructor.
class Artplayer extends Emitter<CoreEvents> {
declare static STYLE: string
declare static DEBUG: boolean
declare static CONTEXTMENU: boolean
declare static NOTICE_TIME: number
declare static SETTING_WIDTH: number
declare static SETTING_ITEM_WIDTH: number
declare static SETTING_ITEM_HEIGHT: number
declare static RESIZE_TIME: number
declare static SCROLL_TIME: number
declare static SCROLL_GAP: number
declare static AUTO_PLAYBACK_MAX: number
declare static AUTO_PLAYBACK_MIN: number
declare static AUTO_PLAYBACK_TIMEOUT: number
declare static RECONNECT_TIME_MAX: number
declare static RECONNECT_SLEEP_TIME: number
declare static CONTROL_HIDE_TIME: number
declare static DBCLICK_TIME: number
declare static DBCLICK_FULLSCREEN: boolean
declare static MOBILE_DBCLICK_PLAY: boolean
declare static MOBILE_CLICK_PLAY: boolean
declare static AUTO_ORIENTATION_TIME: number
declare static INFO_LOOP_TIME: number
declare static FAST_FORWARD_VALUE: number
declare static FAST_FORWARD_TIME: number
declare static TOUCH_MOVE_RATIO: number
declare static VOLUME_STEP: number
declare static SEEK_STEP: number
declare static PLAYBACK_RATE: number[]
declare static ASPECT_RATIO: string[]
declare static FLIP: string[]
declare static FULLSCREEN_WEB_IN_BODY: boolean
declare static LOG_VERSION: boolean
declare static USE_RAF: boolean
declare static REMOVE_SRC_WHEN_DESTROY: boolean
declare id: number
declare option: RuntimeOption<Artplayer>
declare isLock: boolean
declare isReady: boolean
declare isFocus: boolean
declare isInput: boolean
declare isRotate: boolean
declare isDestroy: boolean
declare template: PlayerTemplate<Artplayer>
declare events: Events
declare storage: Storage
declare icons: Icons
declare i18n: I18n
declare notice: Notice
declare player: Player<Artplayer>
declare layers: Layer
declare controls: Control
declare contextmenu: Contextmenu
declare subtitle: Subtitle
declare info: Info
declare loading: Loading
declare hotkey: Hotkey<Artplayer>
declare mask: Mask
declare setting: Setting
declare plugins: Plugins<Artplayer>
constructor(option: OptionInput, readyCallback?: (this: Artplayer, art: Artplayer) => unknown) {
super()
if (!utils.isBrowser) {
@@ -38,7 +111,7 @@ export default class Artplayer extends Emitter {
this.id = ++id
this.option = resolveOption(option, Artplayer.option)
this.option = resolveRuntimeOption<Artplayer>(option, Artplayer.option)
this.isLock = false
this.isReady = false
@@ -49,22 +122,54 @@ export default class Artplayer extends Emitter {
beginLifecycle(this)
try {
this.template = new Template(this)
this.template = new Template<Artplayer>(this) as PlayerTemplate<Artplayer>
if (getScope(this).closed)
return
this.events = new Events(this)
this.storage = new Storage(this)
if (getScope(this).closed)
return
this.storage = new Storage()
if (getScope(this).closed)
return
this.icons = new Icons(this)
if (getScope(this).closed)
return
this.i18n = new I18n(this)
if (getScope(this).closed)
return
this.notice = new Notice(this)
this.player = new Player(this)
if (getScope(this).closed)
return
this.player = new Player<Artplayer>(this)
if (getScope(this).closed)
return
this.layers = new Layer(this)
if (getScope(this).closed)
return
this.controls = new Control(this)
if (getScope(this).closed)
return
this.contextmenu = new Contextmenu(this)
if (getScope(this).closed)
return
this.subtitle = new Subtitle(this)
if (getScope(this).closed)
return
this.info = new Info(this)
if (getScope(this).closed)
return
this.loading = new Loading(this)
this.hotkey = new Hotkey(this)
if (getScope(this).closed)
return
this.hotkey = new Hotkey<Artplayer>(this)
if (getScope(this).closed)
return
this.mask = new Mask(this)
if (getScope(this).closed)
return
this.setting = new Setting(this)
if (getScope(this).closed)
return
this.plugins = new Plugins(this)
if (getScope(this).closed)
@@ -76,7 +181,7 @@ export default class Artplayer extends Emitter {
if (Artplayer.DEBUG) {
// eslint-disable-next-line no-console
const log = msg => console.log(`[ART.${this.id}] -> ${msg}`)
const log = (msg: string) => console.log(`[ART.${this.id}] -> ${msg}`)
log(`Version@${Artplayer.version}`)
for (let index = 0; index < config.events.length; index++) {
this.on(`video:${config.events[index]}`, event => log(`Event@${event.type}`))
@@ -194,19 +299,6 @@ Artplayer.LOG_VERSION = true
Artplayer.USE_RAF = false
Artplayer.REMOVE_SRC_WHEN_DESTROY = true
if (utils.isBrowser) {
window.Artplayer = Artplayer
utils.setStyleText('artplayer-style', style)
publishBrowserEntry(Artplayer, style)
setTimeout(() => {
if (Artplayer.LOG_VERSION) {
// eslint-disable-next-line no-console
console.log(
`%c ArtPlayer %c ${Artplayer.version} %c https://artplayer.org`,
'color: #fff; background: #5f5f5f',
'color: #fff; background: #4bc729',
'',
)
}
}, 100)
}
export default Artplayer
@@ -1,5 +1,6 @@
import type { SubscriptionHost } from '../component/resources'
import type { ProgressPositionHost } from '../control/progress/position'
import type { NoticeSink } from '../notice'
export interface GestureHost extends ProgressPositionHost, SubscriptionHost<{
'document:touchend': [Event]
@@ -12,7 +13,7 @@ export interface GestureHost extends ProgressPositionHost, SubscriptionHost<{
isLock: boolean
width: number
currentTime: number
notice: { show: string }
notice: NoticeSink
}
export interface Drag {
+4 -2
View File
@@ -6,8 +6,10 @@ export interface HotkeyHost extends SubscriptionHost<{ 'document:keydown': [Keyb
template: { $player: HTMLElement }
isFocus: boolean
fullscreenWeb: boolean
backward: number
forward: number
get backward(): undefined
set backward(value: number)
get forward(): undefined
set forward(value: number)
volume: number
toggle: () => unknown
emit: (name: 'hotkey' | 'keydown', event: KeyboardEvent) => unknown
@@ -23,7 +23,7 @@ export interface ClickHost extends FocusHost {
template: { $player: HTMLElement, $video: EventTarget }
isLock: boolean
fullscreen: boolean
fullscreen?: boolean
toggle: () => unknown
emit: (name: 'focus' | 'blur' | 'click' | 'dblclick', event: Event) => unknown
}
@@ -35,5 +35,5 @@ export interface HoverHost {
export interface MoveHost {
template: { $player: HTMLElement }
emit: (name: 'mousemove', event: Event) => unknown
emit: (name: 'mousemove', event: MouseEvent) => unknown
}
+22 -2
View File
@@ -1,10 +1,15 @@
import ResourceScope, { ResourceCleanupError } from './scope'
interface LifecycleTemplate {
$video?: unknown
destroy: (removeHtml: boolean) => void
}
interface LifecycleOwner {
isDestroy: boolean
reset: () => void
emit: (name: 'destroy') => unknown
template?: { $video?: unknown, destroy: (removeHtml: boolean) => void }
template?: LifecycleTemplate
}
interface State {
@@ -13,6 +18,7 @@ interface State {
destroying: boolean
rollback?: () => void
releaseContainer?: () => void
mountingTemplate?: LifecycleTemplate
}
const states = new WeakMap<object, State>()
@@ -48,6 +54,20 @@ export function isClosing(owner: object): boolean {
return state.destroying || state.scope.closed
}
// The proxy callback runs before the public art.template assignment. Keep its
// in-progress template reachable by teardown without changing that public order.
export function duringTemplateMount(owner: object, template: LifecycleTemplate, initialize: () => void): void {
const state = stateOf(owner)
const previous = state.mountingTemplate
state.mountingTemplate = template
try {
initialize()
}
finally {
state.mountingTemplate = previous
}
}
export function ownContainer(owner: object, container: Element, rollback: () => void): void {
const current = containers.get(container)
if (current && current !== owner)
@@ -84,7 +104,7 @@ export function destroyInstance(owner: LifecycleOwner, instances: LifecycleOwner
if (removeSource && owner.template?.$video)
attempt(() => owner.reset())
attempt(() => state.scope.dispose())
attempt(() => owner.template?.destroy(removeHtml))
attempt(() => (owner.template || state.mountingTemplate)?.destroy(removeHtml))
const index = instances.indexOf(owner)
if (index !== -1)
instances.splice(index, 1)
+6 -7
View File
@@ -1,15 +1,13 @@
import type { CoreEmission } from '../../events/core-types'
import type { NoticeSink } from '../../notice'
export type MediaEventName = `video:${string}`
export type MediaListener = (event?: unknown) => void
export interface MediaEvents {
export interface MediaEvents extends CoreEmission<MediaEventName | 'ready' | 'resize' | 'error'> {
on: (name: MediaEventName, callback: MediaListener) => unknown
once: (name: MediaEventName, callback: MediaListener) => unknown
off: (name: MediaEventName, callback: MediaListener) => unknown
emit: {
(name: MediaEventName, event: Event): unknown
(name: 'ready' | 'resize'): unknown
(name: 'error', error: unknown, attempt: number): unknown
}
}
export interface MediaUI {
@@ -28,11 +26,12 @@ export interface MediaEventHost extends MediaEvents, MediaUI {
isReady: boolean
readonly playing: boolean
url: string | null
get seek(): undefined
set seek(time: number)
play: () => unknown
option: { url: string, loop: boolean }
i18n: { get: (key: string) => string }
notice: { set show(message: string) }
notice: NoticeSink
constructor: { RECONNECT_TIME_MAX: number, RECONNECT_SLEEP_TIME: number }
}
+2 -1
View File
@@ -1,3 +1,4 @@
import type { NoticeSink } from '../notice'
import type { ResolvedOption } from '../option/types'
import type { PlaybackMethods } from './types'
@@ -7,7 +8,7 @@ export interface MediaHost<Media> {
interface NoticeHost {
i18n: { get: (key: string) => string }
notice: { get show(): unknown, set show(value: string | Error) }
notice: NoticeSink
emit: (name: 'play' | 'pause') => unknown
}
+6 -3
View File
@@ -1,3 +1,6 @@
import type { PipProperty } from '../display/types'
import type { NoticeSink } from '../notice'
export interface Position {
readonly duration: number
get currentTime(): number
@@ -14,13 +17,13 @@ export interface SeekMethods {
}
export interface SeekHost extends Position {
notice: { set show(message: string) }
notice: NoticeSink
emit: (name: 'seek', currentTime: number, requestedTime: number | string) => unknown
}
export interface PlaybackNotice {
i18n: { get: (key: string) => string }
notice: { set show(message: string) }
notice: NoticeSink
}
export interface VolumeHost extends PlaybackNotice {
@@ -30,7 +33,7 @@ export interface VolumeHost extends PlaybackNotice {
}
export type DisplayState = 'mini' | 'pip' | 'fullscreen' | 'fullscreenWeb'
export type DisplayStates = Record<DisplayState, boolean>
export type DisplayStates = Record<'mini' | 'fullscreenWeb', boolean> & PipProperty & { fullscreen?: boolean }
export interface StateProperty {
get state(): DisplayState | 'standard'
+6 -1
View File
@@ -2,6 +2,11 @@ import type { SubscriptionHost } from './component/resources'
import { getScope, isClosing } from './lifecycle/instance'
import { addClass, removeClass } from './utils'
export interface NoticeSink {
get show(): unknown
set show(value: unknown)
}
interface NoticeHost extends SubscriptionHost<{ destroy: [] }> {
constructor: { NOTICE_TIME: number }
template: { $player: HTMLElement, $noticeInner: HTMLElement }
@@ -24,7 +29,7 @@ function cancelNotice(notice: Notice): void {
}
}
export default class Notice {
export default class Notice implements NoticeSink {
declare art: NoticeHost
declare timer: ReturnType<typeof setTimeout> | null
+28
View File
@@ -0,0 +1,28 @@
import type { ContextmenuHost } from '../contextmenu/types'
import type { ControlHost } from '../control/types'
import type { LayerHost } from '../layer'
import type { MediaSurface } from '../media/types'
import type { PluginFactory } from '../plugins/types'
import type { SettingItem } from '../setting/types'
import type { UrlHost } from '../source/types'
import type { OptionInput, ResolvedOption } from './types'
import resolveOption from './resolve'
interface RuntimeCallbacks<Host> {
proxy?: (this: Host, art: Host) => unknown
plugins: PluginFactory<Host>[]
customType: UrlHost<MediaSurface, Host>['option']['customType']
layers: LayerHost['option']['layers']
controls: ControlHost['option']['controls']
contextmenu: ContextmenuHost['option']['contextmenu']
settings: SettingItem[]
}
export type RuntimeOption<Host> = Omit<ResolvedOption, keyof RuntimeCallbacks<Host>> & RuntimeCallbacks<Host>
export default function resolveRuntimeOption<Host>(input: OptionInput, defaults: ResolvedOption): RuntimeOption<Host> {
// The external declaration facade and internal module views describe the same
// caller-owned callbacks. Do not wrap, clone or rebind their runtime identity.
// All non-callback options still derive directly from the validated merge type.
return resolveOption(input, defaults) as unknown as RuntimeOption<Host>
}
@@ -1,40 +0,0 @@
import { def } from '../utils'
export default function airplayMix(art) {
const {
i18n,
notice,
proxy,
template: { $video },
} = art
let available = true
if (window.WebKitPlaybackTargetAvailabilityEvent && $video.webkitShowPlaybackTargetPicker) {
proxy($video, 'webkitplaybacktargetavailabilitychanged', (event) => {
switch (event.availability) {
case 'available':
available = true
break
case 'not-available':
available = false
break
}
})
}
else {
available = false
}
def(art, 'airplay', {
value() {
if (available) {
$video.webkitShowPlaybackTargetPicker()
art.emit('airplay')
}
else {
notice.show = i18n.get('AirPlay Not Available')
}
},
})
}
@@ -0,0 +1,70 @@
import type { NoticeSink } from '../notice'
import { isClosing } from '../lifecycle/instance'
import { def } from '../utils'
export interface AirplayHost {
template: { $video: EventTarget & { webkitShowPlaybackTargetPicker?: () => void } }
i18n: { get: (key: string) => string }
notice: NoticeSink
proxy: (target: EventTarget, name: string, listener: (event: Event & { availability?: string }) => void) => unknown
emit: (name: 'airplay') => unknown
}
export default function airplayMix(art: AirplayHost): void {
if (isClosing(art))
return
const {
i18n,
notice,
proxy,
template: { $video },
} = art
let available = true
const supported = (window as Window & { WebKitPlaybackTargetAvailabilityEvent?: unknown }).WebKitPlaybackTargetAvailabilityEvent
if (isClosing(art))
return
const picker = supported && $video.webkitShowPlaybackTargetPicker
if (isClosing(art))
return
if (picker) {
proxy($video, 'webkitplaybacktargetavailabilitychanged', (event) => {
if (isClosing(art))
return
switch (event.availability) {
case 'available':
available = true
break
case 'not-available':
available = false
break
}
})
}
else {
available = false
}
if (isClosing(art))
return
def(art, 'airplay', {
value() {
if (isClosing(art))
return
if (available) {
const picker = $video.webkitShowPlaybackTargetPicker
if (isClosing(art))
return
Reflect.apply(picker!, $video, [])
if (!isClosing(art))
art.emit('airplay')
}
else {
const message = i18n.get('AirPlay Not Available')
if (!isClosing(art))
notice.show = message
}
},
})
}
-15
View File
@@ -1,15 +0,0 @@
import { def } from '../utils'
export default function attrMix(art) {
const {
template: { $video },
} = art
def(art, 'attr', {
value(key, value) {
if (value === undefined)
return $video[key]
$video[key] = value
},
})
}
+24
View File
@@ -0,0 +1,24 @@
import { def } from '../utils'
export interface AttributeMethods {
attr: {
(key: PropertyKey): unknown
(key: PropertyKey, value: unknown): unknown
}
}
export default function attrMix(art: { template: { $video: object } }): void {
const {
template: { $video },
} = art
def(art, 'attr', {
value(key: PropertyKey, value?: unknown): unknown {
// attr intentionally exposes arbitrary native/proxy properties, including symbols.
const target = $video as Record<PropertyKey, unknown>
if (value === undefined)
return target[key]
target[key] = value
},
})
}
@@ -1,16 +0,0 @@
import { def } from '../utils'
export default function cssVarMix(art) {
const { $player } = art.template
def(art, 'cssVar', {
value(key, value) {
if (value) {
return $player.style.setProperty(key, value)
}
else {
return getComputedStyle($player).getPropertyValue(key)
}
},
})
}
@@ -0,0 +1,24 @@
import { def } from '../utils'
export interface CssVariableMethods {
cssVar: {
(key: string): string
(key: string, value: unknown): string | void
}
}
export default function cssVarMix(art: { template: { $player: HTMLElement } }): void {
const { $player } = art.template
def(art, 'cssVar', {
value(key: string, value?: unknown): string | void {
if (value) {
// Retain Web IDL conversion and the historical truthy write condition.
return $player.style.setProperty(key, value as string)
}
else {
return getComputedStyle($player).getPropertyValue(key)
}
},
})
}
-75
View File
@@ -1,75 +0,0 @@
import airplayMix from './airplayMix'
import aspectRatioMix from './aspectRatioMix'
import attrMix from './attrMix'
import autoHeightMix from './autoHeightMix'
import autoSizeMix from './autoSizeMix'
import cssVarMix from './cssVarMix'
import currentTimeMix from './currentTimeMix'
import durationMix from './durationMix'
import eventInit from './eventInit'
import flipMix from './flipMix'
import fullscreenMix from './fullscreenMix'
import fullscreenWebMix from './fullscreenWebMix'
import loadedMix from './loadedMix'
import miniMix from './miniMix'
import optionInit from './optionInit'
import pauseMix from './pauseMix'
import pipMix from './pipMix'
import playbackRateMix from './playbackRateMix'
import playedMix from './playedMix'
import playingMix from './playingMix'
import playMix from './playMix'
import posterMix from './posterMix'
import qualityMix from './qualityMix'
import rectMix from './rectMix'
import screenshotMix from './screenshotMix'
import seekMix from './seekMix'
import stateMix from './stateMix'
import subtitleOffsetMix from './subtitleOffsetMix'
import switchMix from './switchMix'
import themeMix from './themeMix'
import thumbnailsMix from './thumbnailsMix'
import toggleMix from './toggleMix'
import typeMix from './typeMix'
import urlMix from './urlMix'
import volumeMix from './volumeMix'
export default class Player {
constructor(art) {
urlMix(art)
attrMix(art)
playMix(art)
pauseMix(art)
toggleMix(art)
seekMix(art)
volumeMix(art)
currentTimeMix(art)
durationMix(art)
switchMix(art)
playbackRateMix(art)
aspectRatioMix(art)
screenshotMix(art)
fullscreenMix(art)
fullscreenWebMix(art)
pipMix(art)
loadedMix(art)
playedMix(art)
playingMix(art)
autoSizeMix(art)
rectMix(art)
flipMix(art)
miniMix(art)
posterMix(art)
autoHeightMix(art)
cssVarMix(art)
themeMix(art)
typeMix(art)
stateMix(art)
subtitleOffsetMix(art)
airplayMix(art)
qualityMix(art)
thumbnailsMix(art)
eventInit(art)
optionInit(art)
}
}
+128
View File
@@ -0,0 +1,128 @@
import type { MediaSurface } from '../media/types'
import type { UrlHost } from '../source/types'
import { isClosing } from '../lifecycle/instance'
import airplayMix from './airplayMix'
import aspectRatioMix from './aspectRatioMix'
import attrMix from './attrMix'
import autoHeightMix from './autoHeightMix'
import autoSizeMix from './autoSizeMix'
import cssVarMix from './cssVarMix'
import currentTimeMix from './currentTimeMix'
import durationMix from './durationMix'
import eventInit from './eventInit'
import flipMix from './flipMix'
import fullscreenMix from './fullscreenMix'
import fullscreenWebMix from './fullscreenWebMix'
import loadedMix from './loadedMix'
import miniMix from './miniMix'
import optionInit from './optionInit'
import pauseMix from './pauseMix'
import pipMix from './pipMix'
import playbackRateMix from './playbackRateMix'
import playedMix from './playedMix'
import playingMix from './playingMix'
import playMix from './playMix'
import posterMix from './posterMix'
import qualityMix from './qualityMix'
import rectMix from './rectMix'
import screenshotMix from './screenshotMix'
import seekMix from './seekMix'
import stateMix from './stateMix'
import subtitleOffsetMix from './subtitleOffsetMix'
import switchMix from './switchMix'
import themeMix from './themeMix'
import thumbnailsMix from './thumbnailsMix'
import toggleMix from './toggleMix'
import typeMix from './typeMix'
import urlMix from './urlMix'
import volumeMix from './volumeMix'
// Installation requirements come from the owning modules. Some properties are
// installed here and consumed only by later callbacks, after construction.
type PlayerDependencies = Parameters<typeof airplayMix>[0]
& Parameters<typeof aspectRatioMix>[0]
& Parameters<typeof attrMix>[0]
& Parameters<typeof autoHeightMix>[0]
& Parameters<typeof autoSizeMix>[0]
& Parameters<typeof cssVarMix>[0]
& Parameters<typeof currentTimeMix>[0]
& Parameters<typeof durationMix>[0]
& Parameters<typeof eventInit>[0]
& Parameters<typeof flipMix>[0]
& Parameters<typeof fullscreenMix>[0]
& Parameters<typeof fullscreenWebMix>[0]
& Parameters<typeof loadedMix>[0]
& Parameters<typeof miniMix>[0]
& Parameters<typeof optionInit>[0]
& Parameters<typeof pauseMix>[0]
& Parameters<typeof pipMix>[0]
& Parameters<typeof playbackRateMix>[0]
& Parameters<typeof playedMix>[0]
& Parameters<typeof playingMix>[0]
& Parameters<typeof playMix>[0]
& Parameters<typeof posterMix>[0]
& Parameters<typeof qualityMix>[0]
& Parameters<typeof rectMix>[0]
& Parameters<typeof screenshotMix>[0]
& Parameters<typeof seekMix>[0]
& Parameters<typeof stateMix>[0]
& Parameters<typeof subtitleOffsetMix>[0]
& Parameters<typeof switchMix>[0]
& Parameters<typeof themeMix>[0]
& Parameters<typeof thumbnailsMix>[0]
& Parameters<typeof toggleMix>[0]
& Parameters<typeof typeMix>[0]
& Parameters<typeof volumeMix>[0]
// The order is observable through descriptors, subscriptions and option writes.
const installers: readonly ((art: PlayerDependencies) => void)[] = [
attrMix,
playMix,
pauseMix,
toggleMix,
seekMix,
volumeMix,
currentTimeMix,
durationMix,
switchMix,
playbackRateMix,
aspectRatioMix,
screenshotMix,
fullscreenMix,
fullscreenWebMix,
pipMix,
loadedMix,
playedMix,
playingMix,
autoSizeMix,
rectMix,
flipMix,
miniMix,
posterMix,
autoHeightMix,
cssVarMix,
themeMix,
typeMix,
stateMix,
subtitleOffsetMix,
airplayMix,
qualityMix,
thumbnailsMix,
eventInit,
optionInit,
]
export type PlayerHost<Host> = UrlHost<MediaSurface, Host> & PlayerDependencies
export default class Player<Host extends PlayerHost<Host>> {
constructor(art: Host) {
if (isClosing(art))
return
urlMix<MediaSurface, Host>(art)
for (const install of installers) {
if (isClosing(art))
return
install(art)
}
}
}
@@ -1,49 +0,0 @@
import { clamp, setStyle } from '../utils'
export default function optionInit(art) {
const {
option,
storage,
template: { $video, $poster },
} = art
for (const key in option.moreVideoAttr) {
art.attr(key, option.moreVideoAttr[key])
}
if (option.muted) {
art.muted = option.muted
}
if (option.volume) {
$video.volume = clamp(option.volume, 0, 1)
}
const volumeStorage = storage.get('volume')
if (typeof volumeStorage === 'number') {
$video.volume = clamp(volumeStorage, 0, 1)
}
if (option.poster) {
setStyle($poster, 'backgroundImage', `url(${option.poster})`)
}
if (option.autoplay) {
$video.autoplay = option.autoplay
}
if (option.playsInline) {
$video.playsInline = true
$video['webkit-playsinline'] = true
}
if (option.theme) {
option.cssVar['--art-theme'] = option.theme
}
for (const key in option.cssVar) {
art.cssVar(key, option.cssVar[key])
}
art.url = option.url
}
+117
View File
@@ -0,0 +1,117 @@
import type { ResolvedOption } from '../option/types'
import type { AttributeMethods } from './attrMix'
import type { CssVariableMethods } from './cssVarMix'
import { isClosing } from '../lifecycle/instance'
import { clamp, setStyle } from '../utils'
export interface InitialOptionHost extends AttributeMethods, CssVariableMethods {
option: Pick<ResolvedOption, 'moreVideoAttr' | 'muted' | 'volume' | 'poster' | 'autoplay' | 'playsInline' | 'theme' | 'cssVar' | 'url'>
storage: { get: (key: string) => unknown }
template: {
$video: Pick<HTMLMediaElement, 'volume'> & { 'autoplay'?: boolean, 'playsInline'?: boolean, 'webkit-playsinline'?: boolean }
$poster: HTMLElement
}
muted: boolean
get url(): string | null
set url(value: string)
}
export default function optionInit(art: InitialOptionHost): void {
if (isClosing(art))
return
const {
option,
storage,
template: { $video, $poster },
} = art
for (const key in option.moreVideoAttr) {
if (isClosing(art))
return
const value = (option.moreVideoAttr as Record<string, unknown>)[key]
if (isClosing(art))
return
art.attr(key, value)
}
if (isClosing(art))
return
if (option.muted) {
const value = option.muted
if (isClosing(art))
return
art.muted = value
}
if (isClosing(art))
return
if (option.volume) {
const value = clamp(option.volume, 0, 1)
if (isClosing(art))
return
$video.volume = value
}
if (isClosing(art))
return
const volumeStorage = storage.get('volume')
if (isClosing(art))
return
if (typeof volumeStorage === 'number') {
$video.volume = clamp(volumeStorage, 0, 1)
}
if (isClosing(art))
return
if (option.poster) {
const value = `url(${option.poster})`
if (isClosing(art))
return
setStyle($poster, 'backgroundImage', value)
}
if (isClosing(art))
return
if (option.autoplay) {
const value = option.autoplay
if (isClosing(art))
return
$video.autoplay = value
}
if (isClosing(art))
return
if (option.playsInline) {
if (isClosing(art))
return
$video.playsInline = true
if (isClosing(art))
return
$video['webkit-playsinline'] = true
}
if (isClosing(art))
return
if (option.theme) {
const styles = option.cssVar
const theme = option.theme
if (isClosing(art))
return
styles['--art-theme'] = theme
}
for (const key in option.cssVar) {
if (isClosing(art))
return
const value = (option.cssVar as Record<string, unknown>)[key]
if (isClosing(art))
return
art.cssVar(key, value)
}
if (isClosing(art))
return
const url = option.url
if (!isClosing(art))
art.url = url
}
@@ -1,6 +1,6 @@
import { def, setStyle } from '../utils'
export default function posterMix(art) {
export default function posterMix(art: { template: { $poster: HTMLElement } }): void {
const {
template: { $poster },
} = art
@@ -8,13 +8,13 @@ export default function posterMix(art) {
def(art, 'poster', {
get: () => {
try {
return $poster.style.backgroundImage.match(/"(.*)"/)[1]
return $poster.style.backgroundImage.match(/"(.*)"/)![1]!
}
catch {
return ''
}
},
set(url) {
set(url: string) {
setStyle($poster, 'backgroundImage', `url(${url})`)
},
})
@@ -0,0 +1,43 @@
import type { Thumbnails } from '../../types/option'
import type { QualityItem } from '../control/types'
import type { PipProperty } from '../display/types'
import type { RectState } from '../media/hosts'
import type { Position, SeekMethods, StateProperty } from '../media/playback'
import type { SwitchMethods } from '../source/types'
import type { AttributeMethods } from './attrMix'
import type { CssVariableMethods } from './cssVarMix'
// Runtime descriptors installed by Player, distinct from each installer's inputs.
// Declaring these on the entry must never emit class fields before installation.
export interface PlayerProperties extends AttributeMethods, CssVariableMethods, Position, SeekMethods, SwitchMethods, RectState, PipProperty, StateProperty {
get url(): string | null
set url(value: string)
play: () => Promise<unknown>
pause: () => unknown
toggle: () => unknown
volume: number
muted: boolean
playbackRate: number
aspectRatio: string
flip: string
fullscreen?: boolean
fullscreenWeb: boolean
mini: boolean
readonly loaded: number
readonly loadedTime: number
readonly played: number
readonly playing: boolean
autoSize: () => void
autoHeight: () => void
poster: string
theme: string
type: string
subtitleOffset: number
airplay: () => void
get quality(): undefined
set quality(value: QualityItem[])
thumbnails: Thumbnails
getDataURL: () => Promise<string>
getBlobUrl: () => Promise<string>
screenshot: (name?: string) => Promise<string>
}
+5 -4
View File
@@ -1,12 +1,13 @@
import type { ComponentHost, EntryOption } from '../component/types'
import type { QualityItem } from '../control/types'
import type { ComponentHost } from '../component/types'
import type { ControlOption, QualityItem } from '../control/types'
import type { NoticeSink } from '../notice'
import { captureSelection } from '../component/selection'
import { isClosing } from '../lifecycle/instance'
import { def } from '../utils'
export interface QualityHost extends ComponentHost {
controls: { update: (option: EntryOption<QualityHost>) => unknown }
notice: { set show(message: string) }
controls: { update: (option: ControlOption) => unknown }
notice: NoticeSink
i18n: { get: (key: string) => string }
switchQuality: (url: string) => Promise<unknown>
}
@@ -1,4 +1,5 @@
import type { CaptureMedia } from '../capture/frame'
import type { NoticeSink } from '../notice'
import { captureBlobUrl, drawFrame } from '../capture/frame'
import { isClosing } from '../lifecycle/instance'
import { captureSource } from '../source/operation'
@@ -6,7 +7,7 @@ import { def, download, secondToTime } from '../utils'
export interface ScreenshotHost {
template: { $video: CaptureMedia }
notice: { set show(value: unknown) }
notice: NoticeSink
getDataURL: () => Promise<string>
emit: (name: 'screenshot', dataUri: string) => unknown
}
@@ -1,11 +1,12 @@
import type { CssVariableMethods } from './cssVarMix'
import { def } from '../utils'
export default function themeMix(art) {
export default function themeMix(art: CssVariableMethods): void {
def(art, 'theme', {
get() {
return art.cssVar('--art-theme')
},
set(theme) {
set(theme: string) {
art.cssVar('--art-theme', theme)
},
})
@@ -1,11 +1,11 @@
import { def } from '../utils'
export default function typeMix(art) {
export default function typeMix(art: { option: { type: string } }): void {
def(art, 'type', {
get() {
return art.option.type
},
set(type) {
set(type: string) {
art.option.type = type
},
})
@@ -24,7 +24,8 @@ export interface ResumePromptHost extends ComponentHost, SubscriptionHost<Prompt
i18n: { get: (key: string) => string }
storage: PlaybackStorage
option: { id?: string, url: string }
seek: number
get seek(): undefined
set seek(value: number)
play: () => unknown
}
@@ -5,7 +5,7 @@ import { isClosing } from '../../lifecycle/instance'
export type PlaybackTimes = Record<string, number>
export interface PlaybackStorage {
get: (key: 'times') => PlaybackTimes | null | undefined
get: (key: 'times') => unknown
set: (key: 'times', value: PlaybackTimes) => void
del: (key: 'times') => void
}
@@ -19,7 +19,9 @@ export interface PlaybackRecordHost extends SubscriptionHost<{ 'video:timeupdate
}
export function readTimes(storage: PlaybackStorage): PlaybackTimes {
return storage.get('times') || {}
// Historical storage is unvalidated JSON. Keep its truthy fallback and localize
// the legacy record assumption to this consumer, rather than all Storage reads.
return (storage.get('times') || {}) as PlaybackTimes
}
export function installPlaybackRecords(art: PlaybackRecordHost): void {
+14 -10
View File
@@ -1,4 +1,4 @@
import type { BuiltinRegistry, PluginFactory, PluginHost } from './types'
import type { BuiltinRegistry, PluginHost } from './types'
import { isClosing } from '../lifecycle/instance'
import { isMobile } from '../utils/compatibility'
import autoOrientation from './autoOrientation'
@@ -7,18 +7,22 @@ import fastForward from './fastForward'
import lock from './lock'
import miniProgressBar from './miniProgressBar'
export default function installBuiltins<Host extends PluginHost<Host>>(registry: BuiltinRegistry<Host>, option: PluginHost<Host>['option']): void {
export type BuiltinHost = Parameters<typeof miniProgressBar>[0]
& Parameters<typeof lock>[0]
& Parameters<typeof autoPlayback>[0]
& Parameters<typeof autoOrientation>[0]
& Parameters<typeof fastForward>[0]
export default function installBuiltins<Host extends PluginHost<Host> & BuiltinHost>(registry: BuiltinRegistry<Host>, option: PluginHost<Host>['option']): void {
// Read each condition at its original turn; earlier plugins may change options.
if (!isClosing(registry.art) && option.miniProgressBar && !option.isLive)
registry.add(miniProgressBar as unknown as PluginFactory<Host>)
registry.add(miniProgressBar)
if (!isClosing(registry.art) && option.lock && isMobile)
registry.add(lock as unknown as PluginFactory<Host>)
registry.add(lock)
if (!isClosing(registry.art) && option.autoPlayback && !option.isLive)
registry.add(autoPlayback as unknown as PluginFactory<Host>)
if (!isClosing(registry.art) && option.autoOrientation && isMobile) {
// The JS constructor supplies the complete host before builtin installation.
registry.add(autoOrientation as unknown as PluginFactory<Host>)
}
registry.add(autoPlayback)
if (!isClosing(registry.art) && option.autoOrientation && isMobile)
registry.add(autoOrientation)
if (!isClosing(registry.art) && option.fastForward && isMobile && !option.isLive)
registry.add(fastForward as unknown as PluginFactory<Host>)
registry.add(fastForward)
}
+2 -1
View File
@@ -1,10 +1,11 @@
import type { BuiltinHost } from './builtins'
import type { PluginFactory, PluginHost, PluginRegistration } from './types'
import { isClosing } from '../lifecycle/instance'
import { errorHandle } from '../utils/error'
import installBuiltins from './builtins'
import registerPlugin from './registration'
export default class Plugins<Host extends PluginHost<Host>> {
export default class Plugins<Host extends PluginHost<Host> & BuiltinHost> {
declare art: Host
declare id: number
+7 -6
View File
@@ -1,5 +1,6 @@
import type { SettingKind } from './selection'
import type { SettingItem, SettingManager, SettingRange } from './types'
import { appendElement } from '../component/dom'
import { isClosing } from '../lifecycle/instance'
import { timeout } from '../lifecycle/resources'
import { addClass, append, def, has, inverseClass, setStyle } from '../utils'
@@ -38,7 +39,7 @@ export function createSettingHeader(setting: SettingManager, item: SettingItem):
setStyle($item, 'height', `${SETTING_ITEM_HEIGHT}px`)
addClass($item, 'art-setting-item')
addClass($item, 'art-setting-item-back')
const $left = append($item, '<div class="art-setting-item-left"></div>')
const $left = appendElement($item, '<div class="art-setting-item-left"></div>')
const $icon = document.createElement('div')
addClass($icon, 'art-setting-item-left-icon')
append($icon, arrowLeft)
@@ -88,8 +89,8 @@ export function createSettingItem(setting: SettingManager, item: SettingItem, is
if (!current())
return
const $left = append($item, '<div class="art-setting-item-left"></div>')
const $right = append($item, '<div class="art-setting-item-right"></div>')
const $left = appendElement($item, '<div class="art-setting-item-left"></div>')
const $right = appendElement($item, '<div class="art-setting-item-right"></div>')
const $icon = document.createElement('div')
addClass($icon, 'art-setting-item-left-icon')
@@ -149,8 +150,8 @@ export function createSettingItem(setting: SettingManager, item: SettingItem, is
case 'switch': {
const $switch = document.createElement('div')
addClass($switch, 'art-setting-item-right-icon')
const $switchOn = append($switch, icons.switchOn)
const $switchOff = append($switch, icons.switchOff)
const $switchOn = appendElement($switch, icons.switchOn)
const $switchOff = appendElement($switch, icons.switchOff)
const initialSwitch = item.switch
if (!current())
return
@@ -308,7 +309,7 @@ export function renderSetting(setting: SettingManager, option = setting.option):
const restoreNavigation = () => {
setting.active = previous
if (previous && setting.cache.has(previous))
inverseClass(setting.cache.get(previous), 'art-current')
inverseClass(setting.cache.get(previous)!, 'art-current')
for (const [key, value, priority] of previousLayout)
setting.$parent.style.setProperty(key, value, priority)
}
+2 -2
View File
@@ -37,7 +37,7 @@ export interface SettingEvents {
'blur': []
'focus': [event: Event]
'resize': []
'video:ratechange': []
'video:ratechange': [Event]
'aspectRatio': [value: string]
'flip': [value: string]
'subtitleOffset': [value: number]
@@ -48,7 +48,7 @@ export interface SettingHost extends ComponentHost, SubscriptionHost<SettingEven
controls: { setting?: HTMLElement }
option: { setting: boolean, settings: SettingItem[], playbackRate: boolean, aspectRatio: boolean, flip: boolean, subtitleOffset: boolean }
constructor: { SETTING_WIDTH: number, SETTING_ITEM_HEIGHT: number, SETTING_ITEM_WIDTH: number, PLAYBACK_RATE: number[], ASPECT_RATIO: string[], FLIP: string[] }
icons: Record<'arrowLeft' | 'arrowRight' | 'config' | 'check' | 'switchOn' | 'switchOff' | 'playbackRate' | 'aspectRatio' | 'flip' | 'subtitle', HTMLElement>
icons: Record<'arrowLeft' | 'arrowRight' | 'config' | 'check' | 'switchOn' | 'switchOff' | 'playbackRate' | 'aspectRatio' | 'flip', HTMLElement> & { subtitle?: HTMLElement }
proxy: ComponentHost['events']['proxy']
i18n: { get: (key: string) => string }
setting: SettingManager
+1 -1
View File
@@ -155,7 +155,7 @@ export function updateSetting(setting: SettingManager, target: SettingItem): Set
if (rendered) {
setting.active = previous
if (previous && setting.cache.has(previous))
inverseClass(setting.cache.get(previous), 'art-current')
inverseClass(setting.cache.get(previous)!, 'art-current')
for (const [key, value, priority] of layout)
setting.$parent.style.setProperty(key, value, priority)
}
+4 -2
View File
@@ -1,5 +1,7 @@
import type { NoticeSink } from '../notice'
export type SourceEvent = 'video:error' | 'video:loadedmetadata' | 'video:canplay' | 'video:seeked' | 'seek'
export type SourceListener = (event?: unknown) => void
export type SourceListener = (event?: unknown, ...extra: unknown[]) => void
export interface SourceEvents {
on: (name: SourceEvent, callback: SourceListener) => unknown
@@ -35,7 +37,7 @@ export interface SwitchHost extends UrlTarget, SourceEvents {
playbackRate: number
pause: () => unknown
play: () => unknown
notice: { set show(value: string) }
notice: NoticeSink
}
export interface SwitchMethods {
@@ -1,12 +1,16 @@
export default class Storage {
declare name: string
declare settings: Record<PropertyKey, unknown>
constructor() {
this.name = 'artplayer_settings'
this.settings = {}
}
get(key) {
get(key?: PropertyKey): unknown {
try {
const storage = JSON.parse(window.localStorage.getItem(this.name)) || {}
// Preserve JSON.parse(null), primitive payloads and truthy-key selection.
const storage = (JSON.parse(window.localStorage.getItem(this.name) as string) || {}) as Record<PropertyKey, unknown>
return key ? storage[key] : storage
}
catch {
@@ -14,7 +18,7 @@ export default class Storage {
}
}
set(key, value) {
set(key: PropertyKey, value: unknown): void {
try {
const storage = Object.assign({}, this.get(), {
[key]: value,
@@ -26,9 +30,9 @@ export default class Storage {
}
}
del(key) {
del(key: PropertyKey): void {
try {
const storage = this.get()
const storage = this.get() as Record<PropertyKey, unknown>
delete storage[key]
window.localStorage.setItem(this.name, JSON.stringify(storage))
}
@@ -37,7 +41,7 @@ export default class Storage {
}
}
clear() {
clear(): void {
try {
window.localStorage.removeItem(this.name)
}
+3 -2
View File
@@ -1,6 +1,7 @@
import type { Subtitle as SubtitleInput } from '../../types/subtitle'
import type { ComponentHost } from '../component/types'
import type { OptionalMediaCapabilities } from '../media/types'
import type { NoticeSink } from '../notice'
export type SubtitleOption = Required<SubtitleInput>
@@ -20,7 +21,7 @@ export interface SubtitleHost extends ComponentHost {
proxy: ComponentHost['events']['proxy']
on: (name: 'video:timeupdate', callback: () => void) => unknown
off: (name: 'video:timeupdate', callback: () => void) => unknown
notice: { set show(value: unknown) }
notice: NoticeSink
i18n: { get: (key: string) => string }
}
@@ -38,7 +39,7 @@ export interface SubtitleOffsetHost {
template: { $track?: SubtitleTrack | null }
subtitle: { readonly cues: SubtitleCue[], update: () => void }
readonly duration: number
notice: { set show(value: string) }
notice: NoticeSink
i18n: { get: (key: string) => string }
emit: (name: 'subtitleOffset', value: number) => unknown
}
+11 -7
View File
@@ -1,5 +1,5 @@
import type { TemplateHost, TemplateNodes } from './template/types'
import { ownContainer } from './lifecycle/instance'
import { duringTemplateMount, isClosing, ownContainer } from './lifecycle/instance'
import captureTemplate from './lifecycle/template-rollback'
import html from './template/html'
import bindNodes from './template/nodes'
@@ -56,8 +56,10 @@ export default class Template<Host extends TemplateHost<Host>> implements Templa
this.query = this.query.bind(this)
ownContainer(art, this.$container, captureTemplate(this.$container))
this.$container.dataset.artId = String(art.id)
this.init()
duringTemplateMount(art, this, () => {
this.$container.dataset.artId = String(art.id)
this.init()
})
}
static get html(): string {
@@ -79,18 +81,20 @@ export default class Template<Host extends TemplateHost<Host>> implements Templa
if (option.proxy) {
const video = option.proxy.call(this.art, this.art)
if (isClosing(this.art))
return
assertProxy(video)
replaceElement(video, this.$video)
replaceElement(video, this.$video!)
video.className = 'art-video'
this.$video = video
}
if (option.backdrop) {
addClass(this.$player, 'art-backdrop')
addClass(this.$player!, 'art-backdrop')
}
if (isMobile) {
addClass(this.$player, 'art-mobile')
addClass(this.$player!, 'art-mobile')
}
}
@@ -99,7 +103,7 @@ export default class Template<Host extends TemplateHost<Host>> implements Templa
this.$container.innerHTML = ''
}
else {
addClass(this.$player, 'art-destroy')
addClass(this.$player!, 'art-destroy')
}
}
}
+13
View File
@@ -1,3 +1,16 @@
import type { MediaSurface } from '../media/types'
import type Template from '../template'
// Consumers of the canonical template require these nodes. SSR callers supply
// the same structure and proxy callers supply a media-like element; neither
// contract is proven by a DOM query. Keep this assertion at the entry boundary.
// Template itself and all public query helpers retain nullable query results.
type RequiredPlayerNode = Exclude<keyof TemplateNodes, '$danmuku' | '$controlsCenter' | '$notice' | '$mask' | '$info'>
export type PlayerTemplate<Host extends TemplateHost<Host>> = Template<Host> & {
[Name in RequiredPlayerNode]: NonNullable<TemplateNodes[Name]>
} & { $video: MediaSurface, $mini?: HTMLElement }
export interface TemplateHost<Host> {
id: number
option: {
@@ -1,6 +0,0 @@
export const userAgent = globalThis?.CUSTOM_USER_AGENT ?? (typeof navigator !== 'undefined' ? navigator.userAgent : '')
export const isSafari = /^(?:(?!chrome|android).)*safari/i.test(userAgent)
export const isIOS = /iPad|iPhone|iPod/i.test(userAgent) && !window.MSStream
export const isIOS13 = isIOS || (userAgent.includes('Macintosh') && navigator.maxTouchPoints >= 1)
export const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || isIOS13
export const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'
@@ -0,0 +1,7 @@
const customAgent = (globalThis as { CUSTOM_USER_AGENT?: string }).CUSTOM_USER_AGENT
export const userAgent = customAgent ?? (typeof navigator === 'undefined' ? '' : navigator.userAgent)
export const isSafari = /^(?:(?!chrome|android).)*safari/i.test(userAgent)
export const isIOS = /iPad|iPhone|iPod/i.test(userAgent) && (typeof window === 'undefined' || !(window as Window & { MSStream?: unknown }).MSStream)
export const isIOS13 = isIOS || (userAgent.includes('Macintosh') && typeof navigator !== 'undefined' && navigator.maxTouchPoints >= 1)
export const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || isIOS13
export const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'
-145
View File
@@ -1,145 +0,0 @@
import { isMobile } from './compatibility'
export { loadImg } from '../image/load'
export { setStyleText } from '../style/inject'
/** @param {string} selector @param {ParentNode} parent */
export function query(selector, parent = document) {
return parent.querySelector(selector)
}
export function queryAll(selector, parent = document) {
return Array.from(parent.querySelectorAll(selector))
}
export function addClass(target, className) {
return target.classList.add(className)
}
export function removeClass(target, className) {
return target.classList.remove(className)
}
export function hasClass(target, className) {
return target.classList.contains(className)
}
export function append(parent, child) {
if (child instanceof Element) {
parent.appendChild(child)
}
else {
parent.insertAdjacentHTML('beforeend', String(child))
}
return parent.lastElementChild || parent.lastChild
}
export function remove(child) {
return child.parentNode.removeChild(child)
}
export function setStyle(element, key, value) {
element.style[key] = value
return element
}
export function setStyles(element, styles) {
for (const key in styles) {
setStyle(element, key, styles[key])
}
return element
}
export function getStyle(element, key, numberType = true) {
const value = window.getComputedStyle(element, null).getPropertyValue(key)
return numberType ? Number.parseFloat(value) : value
}
export function siblings(target) {
return Array.from(target.parentElement.children).filter(item => item !== target)
}
export function inverseClass(target, className) {
siblings(target).forEach(item => removeClass(item, className))
addClass(target, className)
}
export function tooltip(target, msg, pos = 'top') {
if (isMobile)
return
target.setAttribute('aria-label', msg)
addClass(target, 'hint--rounded')
addClass(target, `hint--${pos}`)
}
export function isInViewport(el, offset = 0) {
const rect = el.getBoundingClientRect()
const windowHeight = window.innerHeight || document.documentElement.clientHeight
const windowWidth = window.innerWidth || document.documentElement.clientWidth
const vertInView = rect.top - offset <= windowHeight && rect.top + rect.height + offset >= 0
const horInView = rect.left - offset <= windowWidth + offset && rect.left + rect.width + offset >= 0
return vertInView && horInView
}
export function includeFromEvent(event, target) {
return getComposedPath(event).includes(target)
}
export function replaceElement(newChild, oldChild) {
oldChild.parentNode.replaceChild(newChild, oldChild)
return newChild
}
export function createElement(tag) {
return document.createElement(tag)
}
/** @param {string} key @param {string | HTMLElement} html */
export function getIcon(key = '', html = '') {
const icon = createElement('i')
addClass(icon, 'art-icon')
addClass(icon, `art-icon-${key}`)
append(icon, html)
return icon
}
export function supportsFlex() {
const div = document.createElement('div')
div.style.display = 'flex'
return div.style.display === 'flex'
}
export function getRect(el) {
return el.getBoundingClientRect()
}
export function getComposedPath(event) {
if (event.composedPath)
return event.composedPath()
const path = []
let node = event.target
while (node) {
path.push(node)
node = node.parentNode
}
if (!path.includes(window) && window !== undefined) {
path.push(window)
}
return path
}
export function getSafeAreaInsets() {
const div = document.createElement('div')
div.style.cssText
= 'position:fixed;top:env(safe-area-inset-top,0px);right:env(safe-area-inset-right,0px);bottom:env(safe-area-inset-bottom,0px);left:env(safe-area-inset-left,0px);pointer-events:none;visibility:hidden;'
document.body.appendChild(div)
const style = getComputedStyle(div)
const insets = {
top: Number.parseFloat(style.top) || 0,
right: Number.parseFloat(style.right) || 0,
bottom: Number.parseFloat(style.bottom) || 0,
left: Number.parseFloat(style.left) || 0,
}
div.remove()
return insets
}
+7
View File
@@ -0,0 +1,7 @@
export { getComposedPath, includeFromEvent } from '../dom/event-path'
export { getRect, getSafeAreaInsets, isInViewport, supportsFlex } from '../dom/measure'
export { getIcon, tooltip } from '../dom/presentation'
export { getStyle, setStyle, setStyles } from '../dom/styles'
export { addClass, append, createElement, hasClass, inverseClass, query, queryAll, remove, removeClass, replaceElement, siblings } from '../dom/tree'
export { loadImg } from '../image/load'
export { setStyleText } from '../style/inject'