refactor(core): CORE-10 type playback properties and add compatible controls view

This commit is contained in:
Harvey Zhao committed 2026-09-11 02:51:55 +08:00
1 parent 44f4e083a9
commit 5e31274f1d
48 files changed
+3846 -248

No files matched your search

+8
View File
@@ -367,6 +367,13 @@ export declare class Player {
reset(): void reset(): void
} }
/** Accurate playback method view; assign an existing player without a runtime wrapper. */
export interface PlaybackControls {
play: () => Promise<void>
pause: () => void
toggle: () => Promise<void> | void
}
export type CustomType export type CustomType
= | 'flv' = | 'flv'
| 'm3u8' | 'm3u8'
@@ -1167,6 +1174,7 @@ export type {
Icons, Icons,
Option, Option,
OptionInput, OptionInput,
PlaybackControls,
Player, Player,
PluginFactory, PluginFactory,
Plugins, Plugins,
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao * (c) 2017-2026 Harvey Zhao
* Released under the MIT License. * Released under the MIT License.
*/ */
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),r.exports);const a=t(o),n="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated !function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),r.exports);const a=t(o),n="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao * (c) 2017-2026 Harvey Zhao
* Released under the MIT License. * Released under the MIT License.
*/ */
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,n)=>{var a=t=>{try{s(r.next(t))}catch(e){n(e)}},i=t=>{try{s(r.throw(t))}catch(e){n(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated !function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,n)=>{var a=t=>{try{s(r.next(t))}catch(e){n(e)}},i=t=>{try{s(r.throw(t))}catch(e){n(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
+20 -36
View File
@@ -2753,10 +2753,10 @@ function currentTimeMix(art) {
def(art, "currentTime", { def(art, "currentTime", {
get: () => $video.currentTime || 0, get: () => $video.currentTime || 0,
set: (time2) => { set: (time2) => {
time2 = Number.parseFloat(time2); const parsed = Number.parseFloat(time2);
if (Number.isNaN(time2)) if (Number.isNaN(parsed))
return; return;
$video.currentTime = clamp(time2, 0, art.duration); $video.currentTime = clamp(parsed, 0, art.duration);
} }
}); });
} }
@@ -3133,13 +3133,10 @@ function fullscreenWebMix(art) {
} }
function loadedMix(art) { function loadedMix(art) {
const { $video } = art.template; const { $video } = art.template;
def(art, "loaded", { const target = art;
get: () => art.loadedTime / $video.duration def(art, "loaded", { get: () => target.loadedTime / $video.duration });
});
def(art, "loadedTime", { def(art, "loadedTime", {
get: () => { get: () => $video.buffered.length ? $video.buffered.end($video.buffered.length - 1) : 0
return $video.buffered.length ? $video.buffered.end($video.buffered.length - 1) : 0;
}
}); });
} }
function miniMix(art) { function miniMix(art) {
@@ -3384,11 +3381,8 @@ function pipMix(art) {
} }
} }
function playbackRateMix(art) { function playbackRateMix(art) {
const { const { template: { $video }, i18n, notice } = art;
template: { $video }, const target = art;
i18n,
notice
} = art;
def(art, "playbackRate", { def(art, "playbackRate", {
get() { get() {
return $video.playbackRate; return $video.playbackRate;
@@ -3400,15 +3394,13 @@ function playbackRateMix(art) {
$video.playbackRate = rate; $video.playbackRate = rate;
notice.show = `${i18n.get("Rate")}: ${rate === 1 ? i18n.get("Normal") : `${rate}x`}`; notice.show = `${i18n.get("Rate")}: ${rate === 1 ? i18n.get("Normal") : `${rate}x`}`;
} else { } else {
art.playbackRate = 1; target.playbackRate = 1;
} }
} }
}); });
} }
function playedMix(art) { function playedMix(art) {
def(art, "played", { def(art, "played", { get: () => art.currentTime / art.duration });
get: () => art.currentTime / art.duration
});
} }
function playingMix(art) { function playingMix(art) {
const { $video } = art.template; const { $video } = art.template;
@@ -3629,20 +3621,20 @@ function seekMix(art) {
def(art, "seek", { def(art, "seek", {
set(time2) { set(time2) {
art.currentTime = time2; art.currentTime = time2;
if (art.duration) { if (art.duration)
notice.show = `${secondToTime(art.currentTime)} / ${secondToTime(art.duration)}`; notice.show = `${secondToTime(art.currentTime)} / ${secondToTime(art.duration)}`;
}
art.emit("seek", art.currentTime, time2); art.emit("seek", art.currentTime, time2);
} }
}); });
const target = art;
def(art, "forward", { def(art, "forward", {
set(time2) { set(time2) {
art.seek = art.currentTime + time2; target.seek = art.currentTime + time2;
} }
}); });
def(art, "backward", { def(art, "backward", {
set(time2) { set(time2) {
art.seek = art.currentTime - time2; target.seek = art.currentTime - time2;
} }
}); });
} }
@@ -3653,9 +3645,8 @@ function stateMix(art) {
set(name) { set(name) {
for (let index = 0; index < states2.length; index++) { for (let index = 0; index < states2.length; index++) {
const prop = states2[index]; const prop = states2[index];
if (prop !== name && art[prop]) { if (prop !== name && art[prop])
art[prop] = false; art[prop] = false;
}
} }
} }
}); });
@@ -3902,11 +3893,10 @@ function thumbnailsMix(art) {
function toggleMix(art) { function toggleMix(art) {
def(art, "toggle", { def(art, "toggle", {
value() { value() {
if (art.playing) { if (art.playing)
return art.pause(); return art.pause();
} else { else
return art.play(); return art.play();
}
} }
}); });
} }
@@ -3995,20 +3985,14 @@ function urlMix(art) {
}); });
} }
function volumeMix(art) { function volumeMix(art) {
const { const { template: { $video }, i18n, notice, storage } = art;
template: { $video },
i18n,
notice,
storage
} = art;
def(art, "volume", { def(art, "volume", {
get: () => $video.volume || 0, get: () => $video.volume || 0,
set: (percentage) => { set: (percentage) => {
$video.volume = clamp(percentage, 0, 1); $video.volume = clamp(percentage, 0, 1);
notice.show = `${i18n.get("Volume")}: ${Number.parseInt($video.volume * 100, 10)}`; notice.show = `${i18n.get("Volume")}: ${Number.parseInt(String($video.volume * 100), 10)}`;
if ($video.volume !== 0) { if ($video.volume !== 0)
storage.set("volume", $video.volume); storage.set("volume", $video.volume);
}
} }
}); });
def(art, "muted", { def(art, "muted", {
+1 -1
View File
@@ -49,7 +49,7 @@
"ci:build": "yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports", "ci:build": "yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js", "test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
"typecheck": "node scripts/typecheck.mjs", "typecheck": "node scripts/typecheck.mjs",
"test:unit": "node --test test/playback.test.js test/dash-control.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js", "test:unit": "node --test test/playback.test.js test/dash-control.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js",
"test": "yarn test:node && yarn test:baseline", "test": "yarn test:node && yarn test:baseline",
"test:browser": "playwright test", "test:browser": "playwright test",
"test:browser:install": "playwright install chromium firefox webkit", "test:browser:install": "playwright install chromium firefox webkit",
+26
View File
@@ -318,6 +318,32 @@ finding, not a claim that a standalone debounce can know when its owner is destr
## Verification and maintenance ## Verification and maintenance
### Playback properties (CORE-10)
The toggle, currentTime, seek/forward/backward, volume/muted, playbackRate,
played, loaded/loadedTime and state mixins are TypeScript modules.
`media/playback.ts` defines their minimal timing, notice, storage and display-state
hosts; toggle infers the union of its actual play/pause result types.
Keep JS coercion and read/write differences explicit: currentTime uses parseFloat,
seek emits both the clamped result and original request, and forward/backward
retain native addition/subtraction behavior. Volume uses parseInt-style percentage
formatting and stores nonzero actual media volume. Falsy playbackRate resets to 1;
equal rates do not change the notice. Raw played/loaded ratios may be NaN or Infinity.
Do not replace those rules without a separate compatibility decision.
Most media getters capture the original video; duration reads the current template.
The state setter only disables other modes, never enables its named mode. Native
fullscreen/PiP implementation and event forwarding migrate in their own tasks.
Public command-property getter discrepancies are BASE-TYPE-08, not runtime APIs
to invent. The optional public PlaybackControls view accurately models toggle
without changing the player's historical method declarations.
`test/playback-properties.test.js` covers coercion, storage, descriptors, ranges,
state priority and captured references. Its browser counterpart compares actual
published/candidate media; type fixtures cover source inference and five installed
consumer modes. Existing play/source/canvas tests continue to guard playback.
### Source operations (CORE-09) ### Source operations (CORE-09)
`player/urlMix.ts` and `player/switchMix.ts` preserve the public descriptors. `player/urlMix.ts` and `player/switchMix.ts` preserve the public descriptors.
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao * (c) 2017-2026 Harvey Zhao
* Released under the MIT License. * Released under the MIT License.
*/ */
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),r.exports);const a=t(o),n="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated !function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),r.exports);const a=t(o),n="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao * (c) 2017-2026 Harvey Zhao
* Released under the MIT License. * Released under the MIT License.
*/ */
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,n)=>{var a=t=>{try{s(r.next(t))}catch(e){n(e)}},i=t=>{try{s(r.throw(t))}catch(e){n(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated !function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,n)=>{var a=t=>{try{s(r.next(t))}catch(e){n(e)}},i=t=>{try{s(r.throw(t))}catch(e){n(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
+20 -36
View File
@@ -2753,10 +2753,10 @@ function currentTimeMix(art) {
def(art, "currentTime", { def(art, "currentTime", {
get: () => $video.currentTime || 0, get: () => $video.currentTime || 0,
set: (time2) => { set: (time2) => {
time2 = Number.parseFloat(time2); const parsed = Number.parseFloat(time2);
if (Number.isNaN(time2)) if (Number.isNaN(parsed))
return; return;
$video.currentTime = clamp(time2, 0, art.duration); $video.currentTime = clamp(parsed, 0, art.duration);
} }
}); });
} }
@@ -3133,13 +3133,10 @@ function fullscreenWebMix(art) {
} }
function loadedMix(art) { function loadedMix(art) {
const { $video } = art.template; const { $video } = art.template;
def(art, "loaded", { const target = art;
get: () => art.loadedTime / $video.duration def(art, "loaded", { get: () => target.loadedTime / $video.duration });
});
def(art, "loadedTime", { def(art, "loadedTime", {
get: () => { get: () => $video.buffered.length ? $video.buffered.end($video.buffered.length - 1) : 0
return $video.buffered.length ? $video.buffered.end($video.buffered.length - 1) : 0;
}
}); });
} }
function miniMix(art) { function miniMix(art) {
@@ -3384,11 +3381,8 @@ function pipMix(art) {
} }
} }
function playbackRateMix(art) { function playbackRateMix(art) {
const { const { template: { $video }, i18n, notice } = art;
template: { $video }, const target = art;
i18n,
notice
} = art;
def(art, "playbackRate", { def(art, "playbackRate", {
get() { get() {
return $video.playbackRate; return $video.playbackRate;
@@ -3400,15 +3394,13 @@ function playbackRateMix(art) {
$video.playbackRate = rate; $video.playbackRate = rate;
notice.show = `${i18n.get("Rate")}: ${rate === 1 ? i18n.get("Normal") : `${rate}x`}`; notice.show = `${i18n.get("Rate")}: ${rate === 1 ? i18n.get("Normal") : `${rate}x`}`;
} else { } else {
art.playbackRate = 1; target.playbackRate = 1;
} }
} }
}); });
} }
function playedMix(art) { function playedMix(art) {
def(art, "played", { def(art, "played", { get: () => art.currentTime / art.duration });
get: () => art.currentTime / art.duration
});
} }
function playingMix(art) { function playingMix(art) {
const { $video } = art.template; const { $video } = art.template;
@@ -3629,20 +3621,20 @@ function seekMix(art) {
def(art, "seek", { def(art, "seek", {
set(time2) { set(time2) {
art.currentTime = time2; art.currentTime = time2;
if (art.duration) { if (art.duration)
notice.show = `${secondToTime(art.currentTime)} / ${secondToTime(art.duration)}`; notice.show = `${secondToTime(art.currentTime)} / ${secondToTime(art.duration)}`;
}
art.emit("seek", art.currentTime, time2); art.emit("seek", art.currentTime, time2);
} }
}); });
const target = art;
def(art, "forward", { def(art, "forward", {
set(time2) { set(time2) {
art.seek = art.currentTime + time2; target.seek = art.currentTime + time2;
} }
}); });
def(art, "backward", { def(art, "backward", {
set(time2) { set(time2) {
art.seek = art.currentTime - time2; target.seek = art.currentTime - time2;
} }
}); });
} }
@@ -3653,9 +3645,8 @@ function stateMix(art) {
set(name) { set(name) {
for (let index = 0; index < states2.length; index++) { for (let index = 0; index < states2.length; index++) {
const prop = states2[index]; const prop = states2[index];
if (prop !== name && art[prop]) { if (prop !== name && art[prop])
art[prop] = false; art[prop] = false;
}
} }
} }
}); });
@@ -3902,11 +3893,10 @@ function thumbnailsMix(art) {
function toggleMix(art) { function toggleMix(art) {
def(art, "toggle", { def(art, "toggle", {
value() { value() {
if (art.playing) { if (art.playing)
return art.pause(); return art.pause();
} else { else
return art.play(); return art.play();
}
} }
}); });
} }
@@ -3995,20 +3985,14 @@ function urlMix(art) {
}); });
} }
function volumeMix(art) { function volumeMix(art) {
const { const { template: { $video }, i18n, notice, storage } = art;
template: { $video },
i18n,
notice,
storage
} = art;
def(art, "volume", { def(art, "volume", {
get: () => $video.volume || 0, get: () => $video.volume || 0,
set: (percentage) => { set: (percentage) => {
$video.volume = clamp(percentage, 0, 1); $video.volume = clamp(percentage, 0, 1);
notice.show = `${i18n.get("Volume")}: ${Number.parseInt($video.volume * 100, 10)}`; notice.show = `${i18n.get("Volume")}: ${Number.parseInt(String($video.volume * 100), 10)}`;
if ($video.volume !== 0) { if ($video.volume !== 0)
storage.set("volume", $video.volume); storage.set("volume", $video.volume);
}
} }
}); });
def(art, "muted", { def(art, "muted", {
+38
View File
@@ -0,0 +1,38 @@
export interface Position {
readonly duration: number
get currentTime(): number
set currentTime(time: number | string)
}
export interface SeekMethods {
get seek(): undefined
set seek(time: number | string)
get forward(): undefined
set forward(time: number)
get backward(): undefined
set backward(time: number)
}
export interface SeekHost extends Position {
notice: { set show(message: string) }
emit: (name: 'seek', currentTime: number, requestedTime: number | string) => unknown
}
export interface PlaybackNotice {
i18n: { get: (key: string) => string }
notice: { set show(message: string) }
}
export interface VolumeHost extends PlaybackNotice {
template: { $video: Pick<HTMLMediaElement, 'volume' | 'muted'> }
storage: { set: (key: 'volume', value: number) => unknown }
emit: (name: 'muted', value: boolean) => unknown
}
export type DisplayState = 'mini' | 'pip' | 'fullscreen' | 'fullscreenWeb'
export type DisplayStates = Record<DisplayState, boolean>
export interface StateProperty {
get state(): DisplayState | 'standard'
set state(name: string)
}
@@ -1,15 +0,0 @@
import { clamp, def } from '../utils'
export default function currentTimeMix(art) {
const { $video } = art.template
def(art, 'currentTime', {
get: () => $video.currentTime || 0,
set: (time) => {
time = Number.parseFloat(time)
if (Number.isNaN(time))
return
$video.currentTime = clamp(time, 0, art.duration)
},
})
}
@@ -0,0 +1,17 @@
import type { MediaHost } from '../media/hosts'
import type { Position } from '../media/playback'
import { clamp, def } from '../utils'
export default function currentTimeMix(art: MediaHost<Pick<HTMLMediaElement, 'currentTime'>> & Pick<Position, 'duration'>): asserts art is typeof art & Position {
const { $video } = art.template
def(art, 'currentTime', {
get: () => $video.currentTime || 0,
set: (time: number | string) => {
// parseFloat performs the historical JS coercion; a cast must not change it.
const parsed = Number.parseFloat(time as string)
if (Number.isNaN(parsed))
return
$video.currentTime = clamp(parsed, 0, art.duration)
},
})
}
@@ -1,15 +0,0 @@
import { def } from '../utils'
export default function loadedMix(art) {
const { $video } = art.template
def(art, 'loaded', {
get: () => art.loadedTime / $video.duration,
})
def(art, 'loadedTime', {
get: () => {
return $video.buffered.length ? $video.buffered.end($video.buffered.length - 1) : 0
},
})
}
@@ -0,0 +1,17 @@
import type { MediaHost } from '../media/hosts'
import { def } from '../utils'
type LoadedHost = MediaHost<Pick<HTMLMediaElement, 'duration' | 'buffered'>>
interface LoadedState {
readonly loaded: number
readonly loadedTime: number
}
export default function loadedMix(art: LoadedHost): asserts art is LoadedHost & LoadedState {
const { $video } = art.template
const target = art as LoadedHost & LoadedState
def(art, 'loaded', { get: () => target.loadedTime / $video.duration })
def(art, 'loadedTime', {
get: () => $video.buffered.length ? $video.buffered.end($video.buffered.length - 1) : 0,
})
}
@@ -1,26 +0,0 @@
import { def } from '../utils'
export default function playbackRateMix(art) {
const {
template: { $video },
i18n,
notice,
} = art
def(art, 'playbackRate', {
get() {
return $video.playbackRate
},
set(rate) {
if (rate) {
if (rate === $video.playbackRate)
return
$video.playbackRate = rate
notice.show = `${i18n.get('Rate')}: ${rate === 1.0 ? i18n.get('Normal') : `${rate}x`}`
}
else {
art.playbackRate = 1
}
},
})
}
@@ -0,0 +1,26 @@
import type { MediaHost } from '../media/hosts'
import type { PlaybackNotice } from '../media/playback'
import { def } from '../utils'
type RateHost = MediaHost<Pick<HTMLMediaElement, 'playbackRate'>> & PlaybackNotice
export default function playbackRateMix(art: RateHost): asserts art is RateHost & { playbackRate: number } {
const { template: { $video }, i18n, notice } = art
const target = art as RateHost & { playbackRate: number }
def(art, 'playbackRate', {
get() {
return $video.playbackRate
},
set(rate: number) {
if (rate) {
if (rate === $video.playbackRate)
return
$video.playbackRate = rate
notice.show = `${i18n.get('Rate')}: ${rate === 1.0 ? i18n.get('Normal') : `${rate}x`}`
}
else {
target.playbackRate = 1
}
},
})
}
@@ -1,7 +0,0 @@
import { def } from '../utils'
export default function playedMix(art) {
def(art, 'played', {
get: () => art.currentTime / art.duration,
})
}
@@ -0,0 +1,6 @@
import type { Position } from '../media/playback'
import { def } from '../utils'
export default function playedMix(art: Position): asserts art is Position & { readonly played: number } {
def(art, 'played', { get: () => art.currentTime / art.duration })
}
-27
View File
@@ -1,27 +0,0 @@
import { def, secondToTime } from '../utils'
export default function seekMix(art) {
const { notice } = art
def(art, 'seek', {
set(time) {
art.currentTime = time
if (art.duration) {
notice.show = `${secondToTime(art.currentTime)} / ${secondToTime(art.duration)}`
}
art.emit('seek', art.currentTime, time)
},
})
def(art, 'forward', {
set(time) {
art.seek = art.currentTime + time
},
})
def(art, 'backward', {
set(time) {
art.seek = art.currentTime - time
},
})
}
+26
View File
@@ -0,0 +1,26 @@
import type { SeekHost, SeekMethods } from '../media/playback'
import { def, secondToTime } from '../utils'
export default function seekMix(art: SeekHost): asserts art is SeekHost & SeekMethods {
const { notice } = art
def(art, 'seek', {
set(time: number | string) {
art.currentTime = time
if (art.duration)
notice.show = `${secondToTime(art.currentTime)} / ${secondToTime(art.duration)}`
art.emit('seek', art.currentTime, time)
},
})
// The seek descriptor is installed above; preserve forwarding through it.
const target = art as SeekHost & SeekMethods
def(art, 'forward', {
set(time: number) {
target.seek = art.currentTime + time
},
})
def(art, 'backward', {
set(time: number) {
target.seek = art.currentTime - time
},
})
}
-16
View File
@@ -1,16 +0,0 @@
import { def } from '../utils'
export default function stateMix(art) {
const states = ['mini', 'pip', 'fullscreen', 'fullscreenWeb']
def(art, 'state', {
get: () => states.find(name => art[name]) || 'standard',
set(name) {
for (let index = 0; index < states.length; index++) {
const prop = states[index]
if (prop !== name && art[prop]) {
art[prop] = false
}
}
},
})
}
+16
View File
@@ -0,0 +1,16 @@
import type { DisplayStates, StateProperty } from '../media/playback'
import { def } from '../utils'
export default function stateMix(art: DisplayStates): asserts art is DisplayStates & StateProperty {
const states = ['mini', 'pip', 'fullscreen', 'fullscreenWeb'] as const
def(art, 'state', {
get: () => states.find(name => art[name]) || 'standard',
set(name: string) {
for (let index = 0; index < states.length; index++) {
const prop = states[index]!
if (prop !== name && art[prop])
art[prop] = false
}
},
})
}
@@ -1,14 +0,0 @@
import { def } from '../utils'
export default function toggleMix(art) {
def(art, 'toggle', {
value() {
if (art.playing) {
return art.pause()
}
else {
return art.play()
}
},
})
}
@@ -0,0 +1,13 @@
import type { PlaybackMethods } from '../media/types'
import { def } from '../utils'
export default function toggleMix<PlayResult, PauseResult>(art: PlaybackMethods<PlayResult, PauseResult> & { readonly playing: boolean }): asserts art is typeof art & { toggle: () => PlayResult | PauseResult } {
def(art, 'toggle', {
value() {
if (art.playing)
return art.pause()
else
return art.play()
},
})
}
@@ -1,29 +0,0 @@
import { clamp, def } from '../utils'
export default function volumeMix(art) {
const {
template: { $video },
i18n,
notice,
storage,
} = art
def(art, 'volume', {
get: () => $video.volume || 0,
set: (percentage) => {
$video.volume = clamp(percentage, 0, 1)
notice.show = `${i18n.get('Volume')}: ${Number.parseInt($video.volume * 100, 10)}`
if ($video.volume !== 0) {
storage.set('volume', $video.volume)
}
},
})
def(art, 'muted', {
get: () => $video.muted,
set: (muted) => {
$video.muted = muted
art.emit('muted', muted)
},
})
}
@@ -0,0 +1,22 @@
import type { VolumeHost } from '../media/playback'
import { clamp, def } from '../utils'
export default function volumeMix(art: VolumeHost): asserts art is VolumeHost & Pick<HTMLMediaElement, 'volume' | 'muted'> {
const { template: { $video }, i18n, notice, storage } = art
def(art, 'volume', {
get: () => $video.volume || 0,
set: (percentage: number) => {
$video.volume = clamp(percentage, 0, 1)
notice.show = `${i18n.get('Volume')}: ${Number.parseInt(String($video.volume * 100), 10)}`
if ($video.volume !== 0)
storage.set('volume', $video.volume)
},
})
def(art, 'muted', {
get: () => $video.muted,
set: (muted: boolean) => {
$video.muted = muted
art.emit('muted', muted)
},
})
}
+1
View File
@@ -40,5 +40,6 @@ export interface SwitchHost extends UrlTarget, SourceEvents {
export interface SwitchMethods { export interface SwitchMethods {
switchUrl: (url: string) => Promise<void> switchUrl: (url: string) => Promise<void>
switchQuality: (url: string) => Promise<void> switchQuality: (url: string) => Promise<void>
get switch(): undefined
set switch(url: string) set switch(url: string)
} }
+32 -10
View File
@@ -33,16 +33,33 @@ art.on('subtitleBeforeUpdate', (cues: VTTCue[]) => {
## Retained conflicts and owners ## Retained conflicts and owners
| Surface | Actual runtime | Retained historical declaration | Follow-up | CORE-10 adds an accurate method view without wrapping or replacing the player:
| ------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------ |
| plugins.add | Registry for a synchronous factory; Promise of registry for a Promise factory | Always Promise | CORE-08/21, BASE-TYPE-04 | ```ts
| toggle | Pause result or original play Promise, including rejection | void | CORE-10/21, BASE-TYPE-04 | import type { PlaybackControls } from 'artplayer'
| debounce/throttle | Wrapper returns undefined; debounce ignores its old context argument | Callback return type | CORE-21, BASE-TYPE-05 |
| def with string key | Native defineProperty returns the target | void, arbitrary descriptor input accepted | CORE-21, BASE-TYPE-05 | const playback: PlaybackControls = art
| setting.find | Item or null | Item or undefined | CORE-14/21, BASE-TYPE-07 | const pending = playback.toggle()
| setting.add/update/remove | Item/item/undefined | Setting registry | CORE-14/21, BASE-TYPE-07 | if (pending)
| subtitle update events | VTTCue array | Scalar contextual inference unless explicitly annotated | CORE-15/21, BASE-TYPE-07 | pending.catch(error => console.error(error))
| notice.show getter | Boolean visibility | Historical message/false union | CORE-18/21, BASE-TYPE-07 | ```
The original `art.toggle(): void` declaration remains accepted by historical
consumers. The optional view exposes `Promise<void> | void`; play rejection and
the original object identity remain unchanged. BASE-TYPE-04 stays open for the
remaining plugins.add conflict and CORE-21's full facade review.
| Surface | Actual runtime | Retained historical declaration | Follow-up |
| ------------------------------------------ | ----------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------ |
| plugins.add | Registry for a synchronous factory; Promise of registry for a Promise factory | Always Promise | CORE-08/21, BASE-TYPE-04 |
| toggle | Pause result or original play Promise, including rejection | void | CORE-10/21, BASE-TYPE-04 |
| debounce/throttle | Wrapper returns undefined; debounce ignores its old context argument | Callback return type | CORE-21, BASE-TYPE-05 |
| def with string key | Native defineProperty returns the target | void, arbitrary descriptor input accepted | CORE-21, BASE-TYPE-05 |
| setting.find | Item or null | Item or undefined | CORE-14/21, BASE-TYPE-07 |
| setting.add/update/remove | Item/item/undefined | Setting registry | CORE-14/21, BASE-TYPE-07 |
| subtitle update events | VTTCue array | Scalar contextual inference unless explicitly annotated | CORE-15/21, BASE-TYPE-07 |
| notice.show getter | Boolean visibility | Historical message/false union | CORE-18/21, BASE-TYPE-07 |
| seek/forward/backward/switch/quality reads | Undefined: these descriptors only have setters | Numeric/string/quality-array getters | CORE-21, BASE-TYPE-08 |
These remain open declaration work, not release waivers. CORE-21 must resolve the These remain open declaration work, not release waivers. CORE-21 must resolve the
public/generated facade strategy against both actual returns and historical source public/generated facade strategy against both actual returns and historical source
@@ -52,6 +69,11 @@ and Promise results; ignore timer results, guard setting.find with a truthiness
check, and explicitly annotate cue-array listeners. Do not rely on the inaccurate check, and explicitly annotate cue-array listeners. Do not rely on the inaccurate
return types to call Promise methods on synchronous results or chain settings. return types to call Promise methods on synchronous results or chain settings.
CORE-10 records BASE-TYPE-08 from real published/candidate descriptors. Reading
these command properties returns undefined; use currentTime, option and media
state for observations. Source interfaces model undefined reads independently
of setter inputs. Historical public getter declarations are not removed here.
## Evidence and maintenance ## Evidence and maintenance
declaration-legacy.ts is a compile-only regression fixture, not recommended runtime declaration-legacy.ts is a compile-only regression fixture, not recommended runtime
+6
View File
@@ -47,6 +47,12 @@ signature. CORE-21 remains responsible for the generated/public declaration stra
## Internal media migration ## Internal media migration
`PlaybackControls` is an optional accurate view of play/pause/toggle. Assign the
existing player directly (`const playback: PlaybackControls = art`); it preserves
object identity and reports toggle's Promise-or-void branches. It is exported from
the root and shared type entry in all five supported consumer modes. Historical
`art.toggle(): void` remains available; see COMPATIBILITY.md for the distinction.
CORE-06 uses src/media/types.ts and src/media/hosts.ts for native media, canvas CORE-06 uses src/media/types.ts and src/media/hosts.ts for native media, canvas
shims and minimal playback/layout dependencies. These types are not exported by shims and minimal playback/layout dependencies. These types are not exported by
the package. Public art.video keeps its historical declaration and runtime identity; the package. Public art.video keeps its historical declaration and runtime identity;
+1
View File
@@ -14,6 +14,7 @@ declare namespace Artplayer {
type Option = Definition.Option type Option = Definition.Option
type OptionInput = Definition.OptionInput type OptionInput = Definition.OptionInput
type Player = Definition.Player type Player = Definition.Player
type PlaybackControls = Definition.PlaybackControls
type PluginFactory<Host = Artplayer, Result = unknown> = Definition.PluginFactory<Host, Result> type PluginFactory<Host = Artplayer, Result = unknown> = Definition.PluginFactory<Host, Result>
type Plugins = Definition.Plugins type Plugins = Definition.Plugins
type Setting = Definition.Setting type Setting = Definition.Setting
+1 -1
View File
@@ -1,2 +1,2 @@
export { default } from './artplayer.cjs' export { default } from './artplayer.cjs'
export type { Config, Emitter, Events, I18n, Icons, Option, OptionInput, Player, PluginFactory, Plugins, Setting, SettingOption, Subtitle, SubtitleUpdateEvents, Template, Utils } from './artplayer.js' export type { Config, Emitter, Events, I18n, Icons, Option, OptionInput, PlaybackControls, Player, PluginFactory, Plugins, Setting, SettingOption, Subtitle, SubtitleUpdateEvents, Template, Utils } from './artplayer.js'
+2
View File
@@ -5,6 +5,7 @@ import type { Events, SubtitleUpdateEvents } from './events'
import type { I18n } from './i18n' import type { I18n } from './i18n'
import type { Icons } from './icons' import type { Icons } from './icons'
import type { Option, OptionInput } from './option' import type { Option, OptionInput } from './option'
import type { PlaybackControls } from './playback'
import type { Player } from './player' import type { Player } from './player'
import type { PluginFactory, Plugins } from './plugin' import type { PluginFactory, Plugins } from './plugin'
import type { Setting, SettingOption } from './setting' import type { Setting, SettingOption } from './setting'
@@ -20,6 +21,7 @@ export type {
Icons, Icons,
Option, Option,
OptionInput, OptionInput,
PlaybackControls,
Player, Player,
PluginFactory, PluginFactory,
Plugins, Plugins,
+6
View File
@@ -0,0 +1,6 @@
/** Accurate playback method view; assign an existing player without a runtime wrapper. */
export interface PlaybackControls {
play: () => Promise<void>
pause: () => void
toggle: () => Promise<void> | void
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,40 @@
# CORE-10 播放与时间/状态属性
状态:完成;本地源码、产物、安装消费和三浏览器检查通过。
## 范围与兼容决策
迁移 toggle/currentTime/seek/volume/playbackRate/played/loaded/state 八个 mixin,增加 media/playback.ts 最小宿主类型;play/pause/playing/duration 已在 CORE-06/09 迁移,本任务继续复验。显示模式实现归 CORE-16,媒体转发归 CORE-11,剩余通用 attr/type 门面归 CORE-20。
保留 API-02/03/04/07:toggle 精确返回当前 play/pause 分支结果;currentTime 保持 parseFloat/NaN 忽略/范围限制;seek 保留原输入事件参数;forward 的 JS 加法与 backward 的减法不互换;音量按原 parseInt 百分比格式且非零才持久化;速率假值重置 1、相同值不刷新通知。played/loaded 在未加载或零时长时可能 NaN/Infinity,不能为了测试方便改成 0。保留捕获媒体引用和 duration 动态读取的差别。
state getter 优先 mini、pip、fullscreen、fullscreenWeb;setter 仅退出其他状态,不主动进入指定状态。未把它变成新的显示模式状态机。
## 类型策略
新增纯类型 PlaybackControls,现有 art 可直接赋给该视图,无转换/代理对象;toggle 返回 Promise<void> | void。原 art.toggle(): void 声明保留,使旧合法赋值继续编译。此为 BASE-TYPE-04 的 toggle 分支兼容方案,plugins.add 及总体公开门面仍由 CORE-21 收口。
发现 BASE-TYPE-08:seek/forward/backward/switch/quality 的运行时描述符只有 setter,读取 undefined;旧公共声明却有数字、字符串或数组 getter。源接口按实际读取值建模,旧公开读取声明暂保留并交 CORE-21,不能凭添加 getter 来伪造兼容。
## 验证与回退
完整证据见 [playback-properties-validation.json](../baselines/playback-properties-validation.json),含源码、声明、测试、生成编辑器声明和安装包指纹,以及逐项浏览器结果。
| 检查 | 结果 |
| --- | --- |
| yarn ci:check | 145 项:116 单元、4 工程、25 基线;47 个生产 TS 文件严格编译 |
| 属性专项 | 新增 8 项 Node,覆盖转换、通知、存储、范围、描述符、状态和引用 |
| 安装包消费 | 核心/chapter 的 27 项运行时通过,五组类型模式零诊断 |
| UMD 三浏览器 | 195 项通过,无跳过、重试或 flaky |
| legacy 三浏览器 | 195 项通过,无跳过、重试或 flaky |
| 生成声明 | build:ts 重新生成编辑器声明;PlaybackControls 命名导出进入 CJS/ESM/共享类型入口 |
测试入口:test/playback-properties.test.js、test/types/playback-properties.ts、test/types/playback-public.ts、test/browser/playback-properties.spec.js;既有 play/source/canvas 及发布基线全部继续通过。新旧类型消费同时包含旧 toggle:void、命令属性读取赋值和新 PlaybackControls 分支。
没有执行实体设备、远端 CI、完整 fullscreen/PiP 或全部生态包发布验收。本任务不关闭 BASE-TYPE-04/08 的剩余公共门面工作。
专项浏览器首轮 WebKit 的旧/新版本均在 seek 后出现空 buffered 区间,说明“此时必须大于 0”的测试假设不成立。改为与同一媒体的原始 TimeRanges 最后一个区间精确对照,保留空区间检查;非空/多区间公式由专项单元测试固定验证。未修改实现或添加等待来隐藏差异。
完整检查另发现风险台账的负向测试依赖“第一项风险尚未关闭”。CORE-09 关闭该项后,重复设置 resolved 已不是非法操作。测试现在显式移除关闭证据,继续验证无证据关闭必定失败,不再依赖台账顺序或当前完成程度。
未新增依赖或改变锁文件/版本。任务结束单独本地 commit。需要回退时整体回退源码、声明、生成产物和文档;不单独删除类型桥接或源宿主文件。
+4 -3
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 214 项,范围 22 个包及工作区/示例。 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 214 项,范围 22 个包及工作区/示例。
状态:todo 169 / doing 0 / blocked 0 / done 45 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 状态:todo 168 / doing 0 / blocked 0 / done 46 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -113,7 +113,7 @@
| CORE-07 | artplayer<br>逐项协调公开声明差异 | CORE-02, CORE-06, BASE-07 | plugins/toggle/cue/setting/static 的旧新类型对照 | 每项有兼容重载/扩展或待决结论,不能删除旧类型样例;处理 BASE-TYPE-01/04,分别保留旧合法声明消费并验证真实返回;协调 BASE-TYPE-05 的 Utils 导出、descriptor 和 timer 旧声明差异 | H | done | | CORE-07 | artplayer<br>逐项协调公开声明差异 | CORE-02, CORE-06, BASE-07 | plugins/toggle/cue/setting/static 的旧新类型对照 | 每项有兼容重载/扩展或待决结论,不能删除旧类型样例;处理 BASE-TYPE-01/04,分别保留旧合法声明消费并验证真实返回;协调 BASE-TYPE-05 的 Utils 导出、descriptor 和 timer 旧声明差异 | H | done |
| CORE-08 | artplayer<br>整理插件管理器与扩展类型 | CORE-03, CORE-07 | 注册流程、同步/异步结果、插件/事件扩展接口 | 旧插件同步可见性、重名与命名、销毁中异步插件保持明确语义;处理 baselines/lifecycle-coverage.md 的 BASE-LIFE-03 并增加候选正向回归;接续 CORE-07 / BASE-TYPE-04 的同步/异步注册返回声明冲突,内部返回类型精确,保留历史消费者 | H | done | | CORE-08 | artplayer<br>整理插件管理器与扩展类型 | CORE-03, CORE-07 | 注册流程、同步/异步结果、插件/事件扩展接口 | 旧插件同步可见性、重名与命名、销毁中异步插件保持明确语义;处理 baselines/lifecycle-coverage.md 的 BASE-LIFE-03 并增加候选正向回归;接续 CORE-07 / BASE-TYPE-04 的同步/异步注册返回声明冲突,内部返回类型精确,保留历史消费者 | H | done |
| CORE-09 | artplayer<br>整理 URL 与切源操作 | CORE-04, CORE-06, CORE-08 | url/switch/customType 的操作身份及取消方案 | 并发切源、错误、同 URL、销毁和 Promise 结算有契约;处理 baselines/lifecycle-coverage.md 的 BASE-LIFE-01/02/06 并增加候选正向回归 | H | done | | CORE-09 | artplayer<br>整理 URL 与切源操作 | CORE-04, CORE-06, CORE-08 | url/switch/customType 的操作身份及取消方案 | 并发切源、错误、同 URL、销毁和 Promise 结算有契约;处理 baselines/lifecycle-coverage.md 的 BASE-LIFE-01/02/06 并增加候选正向回归 | H | done |
| CORE-10 | artplayer<br>迁移播放与时间/状态属性 | CORE-09 | play/pause/toggle/seek/volume/rate/duration 等模块 | 旧返回值、拒绝、mutex、位置恢复、属性描述符与事件通过;接续 CORE-07 / BASE-TYPE-04 的 toggle void 与实际 Promise 分支冲突,不吞拒绝 | H | todo | | CORE-10 | artplayer<br>迁移播放与时间/状态属性 | CORE-09 | play/pause/toggle/seek/volume/rate/duration 等模块 | 旧返回值、拒绝、mutex、位置恢复、属性描述符与事件通过;接续 CORE-07 / BASE-TYPE-04 的 toggle void 与实际 Promise 分支冲突,不吞拒绝 | H | done |
| CORE-11 | artplayer<br>整理媒体事件、ready 与重连 | CORE-10 | 媒体转发、重连、UI 响应职责分离 | readiness 次数/顺序、过期重连、原错误参数有回归 | H | todo | | CORE-11 | artplayer<br>整理媒体事件、ready 与重连 | CORE-10 | 媒体转发、重连、UI 响应职责分离 | readiness 次数/顺序、过期重连、原错误参数有回归 | H | todo |
| CORE-12 | artplayer<br>迁移模板及公开资源入口 | CORE-06, CORE-04, BASE-04 | template/icons/i18n/style、SSR 与 proxy 挂载 | DOM/CSS/global/样式注入、i18n 子路径及 SSR import 保持;不把 SSR import/useSSR 保持扩大为服务端构造播放器;复跑 BASE-05 的 SSR import/模板、浏览器 useSSR 节点复用和非浏览器构造错误;接续 VENDOR-01/02,核对 screenfull/hint 复制版本、修改与分发通知,保留 DOM/CSS/fullscreen 行为 | H | todo | | CORE-12 | artplayer<br>迁移模板及公开资源入口 | CORE-06, CORE-04, BASE-04 | template/icons/i18n/style、SSR 与 proxy 挂载 | DOM/CSS/global/样式注入、i18n 子路径及 SSR import 保持;不把 SSR import/useSSR 保持扩大为服务端构造播放器;复跑 BASE-05 的 SSR import/模板、浏览器 useSSR 节点复用和非浏览器构造错误;接续 VENDOR-01/02,核对 screenfull/hint 复制版本、修改与分发通知,保留 DOM/CSS/fullscreen 行为 | H | todo |
| CORE-13 | artplayer<br>迁移组件注册与控件 | CORE-12, CORE-08 | Component/control/layer/contextmenu 的资源与 TS 边界 | add/update/remove、name、selector、mounted/beforeUnmount 通过;处理 BASE-DOM-02 窄容器控件裁切,保留 controls.update 的旧 undefined 返回值 | H | todo | | CORE-13 | artplayer<br>迁移组件注册与控件 | CORE-12, CORE-08 | Component/control/layer/contextmenu 的资源与 TS 边界 | add/update/remove、name、selector、mounted/beforeUnmount 通过;处理 BASE-DOM-02 窄容器控件裁切,保留 controls.update 的旧 undefined 返回值 | H | todo |
@@ -124,7 +124,7 @@
| CORE-18 | artplayer<br>迁移内置插件和提示模块 | CORE-14, CORE-16, CORE-17 | autoPlayback/autoOrientation/fastForward/lock/miniProgressBar 及 info/notice/loading/mask | 默认启用条件、storage 格式、定时器/动画清理通过;结合 BASE-PERF-01 防止销毁后重新安排 notice timer;接续 BASE-TYPE-07 的 notice.show boolean getter 与历史读类型冲突 | M | todo | | CORE-18 | artplayer<br>迁移内置插件和提示模块 | CORE-14, CORE-16, CORE-17 | autoPlayback/autoOrientation/fastForward/lock/miniProgressBar 及 info/notice/loading/mask | 默认启用条件、storage 格式、定时器/动画清理通过;结合 BASE-PERF-01 防止销毁后重新安排 notice timer;接续 BASE-TYPE-07 的 notice.show boolean getter 与历史读类型冲突 | M | todo |
| CORE-19 | artplayer<br>整理进度、质量、缩略图和截图 | CORE-10, CORE-13, CORE-16 | 相关 player/control 功能与纯计算分离 | 截图跨域失败、缩略图布局、quality/进度边界保持 | M | todo | | CORE-19 | artplayer<br>整理进度、质量、缩略图和截图 | CORE-10, CORE-13, CORE-16 | 相关 player/control 功能与纯计算分离 | 截图跨域失败、缩略图布局、quality/进度边界保持 | M | todo |
| CORE-20 | artplayer<br>收敛核心入口与依赖方向 | CORE-14, CORE-15, CORE-18, CORE-19 | Artplayer 门面、初始化依赖与最终目录 | 所有核心自有源码迁移,公开描述符/静态接口/事件差分通过 | H | todo | | CORE-20 | artplayer<br>收敛核心入口与依赖方向 | CORE-14, CORE-15, CORE-18, CORE-19 | Artplayer 门面、初始化依赖与最终目录 | 所有核心自有源码迁移,公开描述符/静态接口/事件差分通过 | H | todo |
| CORE-21 | artplayer<br>生成核心声明并校验包内容 | CORE-20, ENG-07 | 由 TS 生成的既有入口声明与三种发布产物 | 内部类型不泄漏,旧 JS/TS 消费者与 i18n 通过;逐项完成 CORE-07 留下的 BASE-TYPE-04/05/07 声明方案,旧合法消费者与实际返回分别留证;不能以虚假的交叉返回类型或改变运行时来掩盖冲突,未决项阻止核心声明出口 | H | todo | | CORE-21 | artplayer<br>生成核心声明并校验包内容 | CORE-20, ENG-07 | 由 TS 生成的既有入口声明与三种发布产物 | 内部类型不泄漏,旧 JS/TS 消费者与 i18n 通过;逐项完成 CORE-07 留下的 BASE-TYPE-04/05/07 声明方案,旧合法消费者与实际返回分别留证;不能以虚假的交叉返回类型或改变运行时来掩盖冲突,未决项阻止核心声明出口;处理 CORE-10 / BASE-TYPE-08 的 setter-only 属性虚构 getter 声明,并复核 PlaybackControls 精确视图与旧 toggle 声明共存策略 | H | todo |
| CORE-23 | artplayer<br>检查并补齐键盘、焦点与可访问名称 | CORE-13, CORE-14, CORE-17, ENG-05, BASE-04 | 主要控件/设置/模式退出的键盘与焦点回归、名称和字幕可用性检查及必要兼容修正 | 保持旧快捷键和 DOM/CSS 钩子;真实浏览器验证,不以静态属性检查代替交互;处理 BASE-DOM-01 的主要控件 Tab 不可达,保留既有名称与快捷键 | H | todo | | CORE-23 | artplayer<br>检查并补齐键盘、焦点与可访问名称 | CORE-13, CORE-14, CORE-17, ENG-05, BASE-04 | 主要控件/设置/模式退出的键盘与焦点回归、名称和字幕可用性检查及必要兼容修正 | 保持旧快捷键和 DOM/CSS 钩子;真实浏览器验证,不以静态属性检查代替交互;处理 BASE-DOM-01 的主要控件 Tab 不可达,保留既有名称与快捷键 | H | todo |
| CORE-22 | artplayer<br>核心阶段完整验收 | CORE-21, CORE-23, PILOT-01, ENG-08, ENG-10 | 核心与旧插件的可自动化完整回归、资源/性能及明确外部验证缺口 | 核心自动化和公开差异处置通过;真实环境缺口链接包集成/REL 门槛,阶段完成不代表可公开发布 | H | todo | | CORE-22 | artplayer<br>核心阶段完整验收 | CORE-21, CORE-23, PILOT-01, ENG-08, ENG-10 | 核心与旧插件的可自动化完整回归、资源/性能及明确外部验证缺口 | 核心自动化和公开差异处置通过;真实环境缺口链接包集成/REL 门槛,阶段完成不代表可公开发布 | H | todo |
@@ -444,6 +444,7 @@
- CORE-07: [记录](changes/2026-09-11-CORE-07-declarations.md) [记录](baselines/declarations-validation.json) - CORE-07: [记录](changes/2026-09-11-CORE-07-declarations.md) [记录](baselines/declarations-validation.json)
- CORE-08: [记录](changes/2026-09-11-CORE-08-plugin-manager.md) [记录](baselines/plugins-validation.json) - CORE-08: [记录](changes/2026-09-11-CORE-08-plugin-manager.md) [记录](baselines/plugins-validation.json)
- CORE-09: [记录](changes/2026-09-11-CORE-09-source-operations.md) [记录](baselines/source-operations-validation.json) - CORE-09: [记录](changes/2026-09-11-CORE-09-source-operations.md) [记录](baselines/source-operations-validation.json)
- CORE-10: [记录](changes/2026-09-11-CORE-10-playback-properties.md) [记录](baselines/playback-properties-validation.json)
- PKG-CHAPTER-01: [记录](changes/2026-09-10-PKG-CHAPTER-01-contract.md) [记录](baselines/chapter-contract.md) - PKG-CHAPTER-01: [记录](changes/2026-09-10-PKG-CHAPTER-01-contract.md) [记录](baselines/chapter-contract.md)
- PKG-CHAPTER-02: [记录](changes/2026-09-10-PKG-CHAPTER-02-tests.md) [记录](baselines/chapter-validation.json) - PKG-CHAPTER-02: [记录](changes/2026-09-10-PKG-CHAPTER-02-tests.md) [记录](baselines/chapter-validation.json)
- PKG-CHAPTER-03: [记录](changes/2026-09-10-PKG-CHAPTER-03-typescript-modules.md) [记录](baselines/chapter-migration-validation.json) - PKG-CHAPTER-03: [记录](changes/2026-09-10-PKG-CHAPTER-03-typescript-modules.md) [记录](baselines/chapter-migration-validation.json)
+4
View File
@@ -1,5 +1,9 @@
# 进度与证据 # 进度与证据
## 当前实施:CORE-10 播放与时间/状态属性完成
八个 mixin 迁移 TS,新增最小播放属性宿主;保持转换、通知、存储、进度和状态优先级。新增 PlaybackControls 精确视图,旧 toggle 声明仍兼容。145 项 Node/基线、安装 UMD 与 legacy 各 195 项三浏览器、打包 27 项运行时和五组类型检查通过。见 [交付](changes/2026-09-11-CORE-10-playback-properties.md)。发现 BASE-TYPE-08 的只写属性虚构 getter 声明,已交 CORE-21;没有添加运行时 getter 来掩盖它。当前 214 项,46 完成、168 待办;核心 42 个 TS 文件、chapter 5 个。下一项 CORE-11 媒体事件、ready 与重连。
## 当前实施:CORE-09 URL 与切源操作完成 ## 当前实施:CORE-09 URL 与切源操作完成
URL/switch 门面迁移 TS,新增四个 source 模块,统一切源取消、事件清理、同步代理事件和 Promise 结算;真实 playMix 同步接入源身份,避免晚到播放覆盖新状态。137 项 Node/基线、安装 UMD 与 legacy 各 183 项三浏览器检查、打包 27 项运行时和五组类型检查通过。见 [交付](changes/2026-09-11-CORE-09-source-operations.md)。关闭 BASE-LIFE-01/02/06;customType 外部 SDK 清理、对象 URL 归属和过期重连仍由对应后续任务处理。当前 214 项,45 完成、169 待办;核心 33 个 TS 文件、chapter 5 个。下一项 CORE-10 播放与时间/状态属性。 URL/switch 门面迁移 TS,新增四个 source 模块,统一切源取消、事件清理、同步代理事件和 Promise 结算;真实 playMix 同步接入源身份,避免晚到播放覆盖新状态。137 项 Node/基线、安装 UMD 与 legacy 各 183 项三浏览器检查、打包 27 项运行时和五组类型检查通过。见 [交付](changes/2026-09-11-CORE-09-source-operations.md)。关闭 BASE-LIFE-01/02/06;customType 外部 SDK 清理、对象 URL 归属和过期重连仍由对应后续任务处理。当前 214 项,45 完成、169 待办;核心 33 个 TS 文件、chapter 5 个。下一项 CORE-10 播放与时间/状态属性。
+1
View File
@@ -51,3 +51,4 @@
| EVENT-ONCE-01 | resolved / 已复现 | Nested dispatch snapshots invoke the same once registration twice | CORE-02 | | EVENT-ONCE-01 | resolved / 已复现 | Nested dispatch snapshots invoke the same once registration twice | CORE-02 |
| BASE-TYPE-06 | resolved / 已复现 | Public Option requires url and excludes numeric control HTML accepted by existing JS | CORE-07 | | BASE-TYPE-06 | resolved / 已复现 | Public Option requires url and excludes numeric control HTML accepted by existing JS | CORE-07 |
| BASE-TYPE-07 | open / 已复现 | Setting returns, subtitle contextual payloads and notice getter conflict with historical declarations | CORE-07, CORE-14, CORE-15, CORE-18, CORE-21 | | BASE-TYPE-07 | open / 已复现 | Setting returns, subtitle contextual payloads and notice getter conflict with historical declarations | CORE-07, CORE-14, CORE-15, CORE-18, CORE-21 |
| BASE-TYPE-08 | open / 已复现 | Write-only player commands have fictitious public getter declarations | CORE-21 |
+21 -1
View File
@@ -281,7 +281,8 @@
], ],
"compatibleResolution": "保留旧声明接受范围并协调精确重载/扩展,不静默把同步注册改成 async。", "compatibleResolution": "保留旧声明接受范围并协调精确重载/扩展,不静默把同步注册改成 async。",
"closureCriteria": "返回值类型与运行时差异逐项说明,旧合法消费和拒绝传播回归通过。", "closureCriteria": "返回值类型与运行时差异逐项说明,旧合法消费和拒绝传播回归通过。",
"scopeNote": "CORE-07 retains historical legal return assignments and records actual runtime separately; incompatible return tightening remains open through CORE-21. No release waiver." "scopeNote": "CORE-07 retains historical legal return assignments and records actual runtime separately; incompatible return tightening remains open through CORE-21. No release waiver.",
"playbackResolution": "CORE-10 adds assignable PlaybackControls with Promise<void> | void toggle; legacy root void remains accepted. All five installed type modes pass. The overall facade and plugins.add conflict stay open through CORE-21."
}, },
{ {
"id": "BASE-DIST-01", "id": "BASE-DIST-01",
@@ -871,6 +872,25 @@
"compatibleResolution": "Keep legacy consumers; add explicit cue-array listener overloads, type internal implementations accurately, and resolve remaining generated/public facade contracts at CORE-21 without false intersections.", "compatibleResolution": "Keep legacy consumers; add explicit cue-array listener overloads, type internal implementations accurately, and resolve remaining generated/public facade contracts at CORE-21 without false intersections.",
"closureCriteria": "Actual setting/null/void and notice boolean returns, cue array callbacks and old legal type consumers all have an explicit compatible declaration strategy and package tests.", "closureCriteria": "Actual setting/null/void and notice boolean returns, cue array callbacks and old legal type consumers all have an explicit compatible declaration strategy and package tests.",
"scopeNote": "CORE-07 provides runtime evidence and explicit cue-array overloads; old inferred scalar event callbacks and return signatures are not claimed corrected." "scopeNote": "CORE-07 provides runtime evidence and explicit cue-array overloads; old inferred scalar event callbacks and return signatures are not claimed corrected."
},
{
"id": "BASE-TYPE-08",
"title": "Write-only player commands have fictitious public getter declarations",
"confirmation": "reproduced",
"status": "open",
"owners": [
"CORE-21"
],
"evidence": [
"packages/artplayer/types/player.d.ts",
"test/playback-properties.test.js",
"test/browser/playback-properties.spec.js",
"refactor/changes/2026-09-11-CORE-10-playback-properties.md",
"refactor/baselines/playback-properties-validation.json",
"test/types/declaration-legacy.ts"
],
"compatibleResolution": "Keep historical consumer acceptance, model source command reads as undefined, and provide an accurate public facade without inventing runtime getters.",
"closureCriteria": "seek/forward/backward/switch/quality reads and writes have an explicit accurate compatibility strategy validated against old TypeScript consumers and real package descriptors."
} }
] ]
} }
+4 -1
View File
@@ -13,7 +13,10 @@ test('Risk register rejects omitted findings, missing owners and unsupported clo
for (const mutate of [ for (const mutate of [
value => value.items = value.items.filter(item => item.id !== 'BASE-LIFE-04'), value => value.items = value.items.filter(item => item.id !== 'BASE-LIFE-04'),
value => value.items[0].owners = ['NO-SUCH-TASK'], value => value.items[0].owners = ['NO-SUCH-TASK'],
value => value.items[0].status = 'resolved', (value) => {
value.items[0].status = 'resolved'
delete value.items[0].resolutionEvidence
},
value => value.items[0].evidence = ['../outside-workspace'], value => value.items[0].evidence = ['../outside-workspace'],
]) { ]) {
const changed = structuredClone(register) const changed = structuredClone(register)
+6 -3
View File
@@ -930,11 +930,14 @@
"dependsOn": [ "dependsOn": [
"CORE-09" "CORE-09"
], ],
"status": "todo", "status": "done",
"risk": "H", "risk": "H",
"deliverable": "play/pause/toggle/seek/volume/rate/duration 等模块", "deliverable": "play/pause/toggle/seek/volume/rate/duration 等模块",
"acceptance": "旧返回值、拒绝、mutex、位置恢复、属性描述符与事件通过;接续 CORE-07 / BASE-TYPE-04 的 toggle void 与实际 Promise 分支冲突,不吞拒绝", "acceptance": "旧返回值、拒绝、mutex、位置恢复、属性描述符与事件通过;接续 CORE-07 / BASE-TYPE-04 的 toggle void 与实际 Promise 分支冲突,不吞拒绝",
"evidence": [] "evidence": [
"changes/2026-09-11-CORE-10-playback-properties.md",
"baselines/playback-properties-validation.json"
]
}, },
{ {
"id": "CORE-11", "id": "CORE-11",
@@ -1123,7 +1126,7 @@
"status": "todo", "status": "todo",
"risk": "H", "risk": "H",
"deliverable": "由 TS 生成的既有入口声明与三种发布产物", "deliverable": "由 TS 生成的既有入口声明与三种发布产物",
"acceptance": "内部类型不泄漏,旧 JS/TS 消费者与 i18n 通过;逐项完成 CORE-07 留下的 BASE-TYPE-04/05/07 声明方案,旧合法消费者与实际返回分别留证;不能以虚假的交叉返回类型或改变运行时来掩盖冲突,未决项阻止核心声明出口", "acceptance": "内部类型不泄漏,旧 JS/TS 消费者与 i18n 通过;逐项完成 CORE-07 留下的 BASE-TYPE-04/05/07 声明方案,旧合法消费者与实际返回分别留证;不能以虚假的交叉返回类型或改变运行时来掩盖冲突,未决项阻止核心声明出口;处理 CORE-10 / BASE-TYPE-08 的 setter-only 属性虚构 getter 声明,并复核 PlaybackControls 精确视图与旧 toggle 声明共存策略",
"evidence": [] "evidence": []
}, },
{ {
+1 -1
View File
@@ -38,7 +38,7 @@ export function typeConsumers(dir) {
for (const [compiler, mode] of [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'bundler-esm'], [compat, 'node10-commonjs'], [ts, 'nodenext-esm']]) { for (const [compiler, mode] of [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'bundler-esm'], [compat, 'node10-commonjs'], [ts, 'nodenext-esm']]) {
const next = mode.startsWith('nodenext') const next = mode.startsWith('nodenext')
const extension = next ? (mode.endsWith('-cjs') ? 'cts' : 'mts') : 'ts' const extension = next ? (mode.endsWith('-cjs') ? 'cts' : 'mts') : 'ts'
const inputs = ['public', 'declaration-inputs', 'declaration-legacy', 'plugins-public', 'chapter-options', 'chapter-exports', 'language-value', 'language', 'legacy-plugin'] const inputs = ['public', 'declaration-inputs', 'declaration-legacy', 'plugins-public', 'playback-public', 'chapter-options', 'chapter-exports', 'language-value', 'language', 'legacy-plugin']
if (mode === 'nodenext-cjs') if (mode === 'nodenext-cjs')
inputs.push('commonjs') inputs.push('commonjs')
const files = inputs.map((name) => { const files = inputs.map((name) => {
+1 -1
View File
@@ -71,7 +71,7 @@ export function runTypechecks() {
console.log(`Strict project passed: ${relative(config)} (${result.files.length} root files)`) console.log(`Strict project passed: ${relative(config)} (${result.files.length} root files)`)
} }
for (const [compiler, mode] of [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'bundler-esm'], [ts, 'nodenext-esm'], [compat, 'node10-commonjs']]) { for (const [compiler, mode] of [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'bundler-esm'], [ts, 'nodenext-esm'], [compat, 'node10-commonjs']]) {
for (const fixture of ['test/types/public.ts', 'test/types/declaration-inputs.ts', 'test/types/declaration-legacy.ts', 'test/types/plugins-public.ts', 'test/types/chapter-options.ts', 'test/types/chapter-exports.ts', 'test/types/language-value.ts', 'refactor/fixtures/consumers/language.ts', 'refactor/fixtures/consumers/legacy-plugin.ts']) { for (const fixture of ['test/types/public.ts', 'test/types/declaration-inputs.ts', 'test/types/declaration-legacy.ts', 'test/types/plugins-public.ts', 'test/types/playback-public.ts', 'test/types/chapter-options.ts', 'test/types/chapter-exports.ts', 'test/types/language-value.ts', 'refactor/fixtures/consumers/language.ts', 'refactor/fixtures/consumers/legacy-plugin.ts']) {
assert.deepEqual(checkConsumer(compiler, mode, fs.readFileSync(path.join(root, fixture), 'utf8')), [], `Consumer failed: TS ${compiler.version} ${mode} ${fixture}`) assert.deepEqual(checkConsumer(compiler, mode, fs.readFileSync(path.join(root, fixture), 'utf8')), [], `Consumer failed: TS ${compiler.version} ${mode} ${fixture}`)
} }
if (mode === 'nodenext-cjs') if (mode === 'nodenext-cjs')
+82
View File
@@ -0,0 +1,82 @@
import { expect, test } from './fixtures.js'
for (const core of ['published', 'candidate']) {
test(`${core}: real media preserves timeline, volume, rate and property descriptors`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
await page.evaluate(() => window.createPlayer('/test/pattern.mp4?properties=media'))
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
const result = await page.evaluate(() => {
const { art } = window
const seeks = []
const muted = []
art.on('seek', (...args) => seeks.push(args))
art.on('muted', value => muted.push(value))
art.seek = '2.5 seconds'
const seekNotice = art.template.$noticeInner.textContent
art.forward = 1
art.backward = 0.5
art.volume = 0.42
const volumeNotice = art.template.$noticeInner.textContent
art.muted = 'truthy'
art.playbackRate = 1.5
const rateNotice = art.template.$noticeInner.textContent
art.playbackRate = 0
const resetNotice = art.template.$noticeInner.textContent
const descriptors = Object.fromEntries(['currentTime', 'seek', 'forward', 'backward', 'volume', 'muted', 'playbackRate', 'loaded', 'loadedTime', 'played', 'state'].map((name) => {
const descriptor = Object.getOwnPropertyDescriptor(art, name)
return [name, { getter: typeof descriptor.get === 'function', setter: typeof descriptor.set === 'function', enumerable: descriptor.enumerable, configurable: descriptor.configurable }]
}))
const unreadable = Object.fromEntries(['seek', 'forward', 'backward', 'switch', 'quality'].map(name => [name, art[name] === undefined]))
const buffered = Array.from({ length: art.video.buffered.length }, (_, index) => art.video.buffered.end(index))
const result = { seeks, muted, time: art.currentTime, volume: art.volume, mediaMuted: art.video.muted, rate: art.playbackRate, seekNotice, volumeNotice, rateNotice, resetNotice, descriptors, unreadable, played: art.played, duration: art.duration, buffered, loadedTime: art.loadedTime, loaded: art.loaded, error: art.video.error?.code || 0 }
art.destroy()
return result
})
expect(result.seeks).toEqual([[2.5, '2.5 seconds'], [3.5, 3.5], [3, 3]])
expect(result.muted).toEqual(['truthy'])
expect(result.time).toBeCloseTo(3, 2)
expect(result.volume).toBeCloseTo(0.42, 6)
expect(result.mediaMuted).toBe(true)
expect(result.rate).toBe(1)
expect(result.seekNotice).toBe('00:02 / 00:08')
expect(result.volumeNotice).toBe('Volume: 42')
expect(result.rateNotice).toBe('Rate: 1.5x')
expect(result.resetNotice).toBe('Rate: Normal')
expect(result.played).toBeCloseTo(3 / result.duration, 6)
// WebKit may empty its buffered ranges during seek; compare the actual media surface.
expect(result.loadedTime).toBe(result.buffered.at(-1) || 0)
expect(result.loaded).toBeCloseTo(result.loadedTime / result.duration, 6)
expect(result.error).toBe(0)
expect(Object.values(result.unreadable)).toEqual([true, true, true, true, true])
for (const [name, descriptor] of Object.entries(result.descriptors)) {
expect(descriptor).toEqual({
getter: !['seek', 'forward', 'backward'].includes(name),
setter: !['loaded', 'loadedTime', 'played'].includes(name),
enumerable: false,
configurable: false,
})
}
})
test(`${core}: invalid time inputs and rate no-op retain native behavior`, async ({ page }) => {
await page.goto(`/test/player.html?core=${core}&chapter=published`)
await page.evaluate(() => window.createPlayer('/test/pattern.mp4?properties=invalid'))
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
const result = await page.evaluate(() => {
const { art } = window
art.currentTime = 2
const times = []
for (const input of [Number.NaN, undefined, null, '', 'invalid']) {
art.currentTime = input
times.push(art.currentTime)
}
art.notice.show = 'keep'
art.playbackRate = 1
art.playbackRate = Number.NaN
const result = { times, rate: art.playbackRate, notice: art.template.$noticeInner.textContent, noRuntimeType: window.Artplayer.PlaybackControls === undefined }
art.destroy()
return result
})
expect(result).toEqual({ times: [2, 2, 2, 2, 2], rate: 1, notice: 'keep', noRuntimeType: true })
})
}
+175
View File
@@ -0,0 +1,175 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Use the repository Node test runner.
import { test } from 'node:test'
import { loadModules } from './helpers/load.js'
const names = ['currentTimeMix', 'durationMix', 'seekMix', 'volumeMix', 'playbackRateMix', 'playedMix', 'loadedMix', 'stateMix', 'toggleMix']
const mixins = await loadModules(Object.fromEntries(names.map(name => [name, `packages/artplayer/src/player/${name}`])))
function createArt() {
const stored = []
const emitted = []
const video = { currentTime: 0, duration: 100, volume: 0.5, muted: false, playbackRate: 1, buffered: { length: 0, end() {
throw new Error('empty')
} } }
const art = {
template: { $video: video },
notice: { show: '' },
i18n: { get: key => key },
storage: { set: (...args) => stored.push(args) },
emit: (...args) => emitted.push(args),
mini: false,
pip: false,
fullscreen: false,
fullscreenWeb: false,
}
for (const name of names.filter(name => name !== 'toggleMix'))
mixins[name](art)
return { art, video, stored, emitted }
}
test('currentTime keeps parseFloat coercion, clamps and ignores NaN', () => {
const { art, video } = createArt()
for (const [input, expected] of [['12.5 seconds', 12.5], [-2, 0], [200, 100], [Infinity, 100], [-Infinity, 0]]) {
art.currentTime = input
assert.equal(video.currentTime, expected)
}
art.currentTime = 7
for (const input of [Number.NaN, undefined, null, '', 'invalid']) {
art.currentTime = input
assert.equal(art.currentTime, 7)
}
let coerced = 0
art.currentTime = { toString() {
coerced++
return '22s'
} }
assert.equal(art.currentTime, 22)
assert.equal(coerced, 1)
assert.throws(() => art.currentTime = Symbol('time'), TypeError)
})
test('seek preserves original payload, notification order and forward/backward coercion', () => {
const { art, emitted } = createArt()
art.seek = '12.5 seconds'
assert.equal(art.currentTime, 12.5)
assert.equal(art.notice.show, '00:12 / 01:40')
assert.deepEqual(emitted.at(-1), ['seek', 12.5, '12.5 seconds'])
art.currentTime = 10
art.forward = '2'
assert.deepEqual(emitted.at(-1), ['seek', 100, '102'])
art.backward = '2'
assert.deepEqual(emitted.at(-1), ['seek', 98, 98])
for (const name of ['seek', 'forward', 'backward']) {
assert.equal(art[name], undefined)
const descriptor = Object.getOwnPropertyDescriptor(art, name)
assert.equal(descriptor.get, undefined)
assert.equal(descriptor.enumerable, false)
assert.equal(descriptor.configurable, false)
}
})
test('volume uses actual assigned media volume, historical percentage formatting and nonzero persistence', () => {
const { art, video, stored, emitted } = createArt()
art.volume = 2
assert.equal(video.volume, 1)
assert.equal(art.notice.show, 'Volume: 100')
art.volume = -1
assert.equal(video.volume, 0)
assert.deepEqual(stored, [['volume', 1]])
art.volume = 1e-9
assert.equal(art.notice.show, 'Volume: 1')
assert.deepEqual(stored.at(-1), ['volume', 1e-9])
art.muted = 'legacy-truthy'
assert.equal(video.muted, 'legacy-truthy')
assert.deepEqual(emitted.at(-1), ['muted', 'legacy-truthy'])
})
test('playbackRate preserves equal-value no-op, falsy normalization and native errors', () => {
const { art, video } = createArt()
art.notice.show = 'keep'
art.playbackRate = 1
assert.equal(art.notice.show, 'keep')
art.playbackRate = 1.5
assert.equal(art.notice.show, 'Rate: 1.5x')
art.playbackRate = 0
assert.equal(video.playbackRate, 1)
assert.equal(art.notice.show, 'Rate: Normal')
art.notice.show = 'keep again'
art.playbackRate = Number.NaN
assert.equal(art.notice.show, 'keep again')
const error = new Error('native rate rejected')
Object.defineProperty(video, 'playbackRate', { get: () => 1, set() {
throw error
} })
assert.throws(() => art.playbackRate = -1, actual => actual === error)
})
test('progress getters keep raw ratios and the last buffered range', () => {
const { art, video } = createArt()
assert.equal(art.loadedTime, 0)
video.buffered = { length: 2, end: index => [3, 40][index] }
assert.equal(art.loadedTime, 40)
assert.equal(art.loaded, 0.4)
art.currentTime = 25
assert.equal(art.played, 0.25)
video.duration = Infinity
assert.equal(art.duration, 0)
assert.equal(art.played, Infinity)
assert.equal(art.loaded, 0)
video.currentTime = 0
video.duration = 0
video.buffered = { length: 0 }
assert(Number.isNaN(art.played))
assert(Number.isNaN(art.loaded))
})
test('state prioritizes mini/pip/fullscreen/web and only disables other states', () => {
const { art } = createArt()
assert.equal(art.state, 'standard')
art.mini = art.pip = art.fullscreen = art.fullscreenWeb = true
assert.equal(art.state, 'mini')
art.state = 'fullscreen'
assert.deepEqual([art.mini, art.pip, art.fullscreen, art.fullscreenWeb], [false, false, true, false])
assert.equal(art.state, 'fullscreen')
art.state = 'unknown'
assert.equal(art.state, 'standard')
art.state = 'pip'
assert.equal(art.pip, false)
})
test('captured media attributes remain captured while duration follows the current template', () => {
const { art, video } = createArt()
art.template.$video = { currentTime: 88, duration: 200, volume: 0.7, muted: true, playbackRate: 2, buffered: { length: 0 } }
assert.equal(art.duration, 200)
art.currentTime = 150
assert.equal(video.currentTime, 150)
assert.equal(art.template.$video.currentTime, 88)
assert.equal(art.volume, 0.5)
assert.equal(art.muted, false)
assert.equal(art.playbackRate, 1)
video.buffered = { length: 1, end: () => 50 }
assert.equal(art.loaded, 0.5)
assert.equal(art.played, 0.75)
})
test('toggle returns exact dynamic branch results and preserves extracted function binding', async () => {
const pending = Promise.resolve(42)
const calls = []
const art = { playing: false, play() {
calls.push(this)
return pending
}, pause() {
calls.push(this)
return 'paused'
} }
mixins.toggleMix(art)
const toggle = art.toggle
assert.equal(toggle(), pending)
art.playing = true
assert.equal(toggle.call({}), 'paused')
assert.deepEqual(calls, [art, art])
assert.equal(await pending, 42)
const descriptor = Object.getOwnPropertyDescriptor(art, 'toggle')
assert.deepEqual([descriptor.writable, descriptor.configurable, descriptor.enumerable], [false, false, false])
})
+6 -1
View File
@@ -13,8 +13,13 @@ const throttled: number = Artplayer.utils.throttle(() => 1, 10)()
const descriptor: void = Artplayer.utils.def({}, 'value', { value: 1 }) const descriptor: void = Artplayer.utils.def({}, 'value', { value: 1 })
const updated: Artplayer['setting'] = art.setting.update({ name: 'entry', html: 'Entry' }) const updated: Artplayer['setting'] = art.setting.update({ name: 'entry', html: 'Entry' })
const removed: Artplayer['setting'] = art.setting.remove('entry') const removed: Artplayer['setting'] = art.setting.remove('entry')
const seekRead: number = art.seek
const forwardRead: number = art.forward
const backwardRead: number = art.backward
const switchRead: string = art.switch
const qualityRead: Artplayer['quality'] = art.quality
art.on('subtitleBeforeUpdate', (cue) => { art.on('subtitleBeforeUpdate', (cue) => {
const historicalCue: VTTCue = cue const historicalCue: VTTCue = cue
void historicalCue void historicalCue
}) })
void [requiredUrl, registration, toggle, debounced, throttled, descriptor, updated, removed] void [requiredUrl, registration, toggle, debounced, throttled, descriptor, updated, removed, seekRead, forwardRead, backwardRead, switchRead, qualityRead]
+33
View File
@@ -0,0 +1,33 @@
import currentTimeMix from '../../packages/artplayer/src/player/currentTimeMix'
import loadedMix from '../../packages/artplayer/src/player/loadedMix'
import seekMix from '../../packages/artplayer/src/player/seekMix'
import toggleMix from '../../packages/artplayer/src/player/toggleMix'
declare const timing: {
template: { $video: Pick<HTMLMediaElement, 'currentTime' | 'duration' | 'buffered'> }
duration: number
notice: { show: string }
emit: (name: 'seek', actual: number, requested: number | string) => void
}
currentTimeMix(timing)
timing.currentTime = '12.5 seconds'
const time: number = timing.currentTime
seekMix(timing)
timing.seek = 12
const noSeekGetter: undefined = timing.seek
const noForwardGetter: undefined = timing.forward
loadedMix(timing)
const loaded: number = timing.loadedTime
void [time, loaded, noSeekGetter, noForwardGetter]
// @ts-expect-error Buffered state is computed, not writable.
timing.loaded = 0.5
// @ts-expect-error Time inputs do not advertise booleans.
timing.currentTime = true
declare const playback: { playing: boolean, play: () => Promise<42>, pause: () => 'paused' }
toggleMix(playback)
const result: Promise<42> | 'paused' = playback.toggle()
void result
// @ts-expect-error A toggle preserves either branch, not only the play branch.
const invalid: Promise<42> = playback.toggle()
void invalid
+19
View File
@@ -0,0 +1,19 @@
import type Artplayer from 'artplayer'
import type { PlaybackControls } from 'artplayer'
import type { PlaybackControls as SharedControls } from 'artplayer/types'
declare const art: Artplayer
const controls: PlaybackControls = art
const shared: SharedControls = controls
const toggled: Promise<void> | void = shared.toggle()
if (toggled)
toggled.catch(() => {})
const played: Promise<void> = controls.play()
const paused: void = controls.pause()
const historical: void = art.toggle()
void [played, paused, historical]
// @ts-expect-error A paused toggle does not return a Promise.
const alwaysAsync: Promise<void> = controls.toggle()
// @ts-expect-error Toggle does not report playback as a boolean.
const boolean: boolean = controls.toggle()
void [alwaysAsync, boolean]