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
}
/** 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
= | 'flv'
| 'm3u8'
@@ -1167,6 +1174,7 @@ export type {
Icons,
Option,
OptionInput,
PlaybackControls,
Player,
PluginFactory,
Plugins,
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),r.exports);const a=t(o),n="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),r.exports);const a=t(o),n="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,n)=>{var a=t=>{try{s(r.next(t))}catch(e){n(e)}},i=t=>{try{s(r.throw(t))}catch(e){n(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,n)=>{var a=t=>{try{s(r.next(t))}catch(e){n(e)}},i=t=>{try{s(r.throw(t))}catch(e){n(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
+20 -36
View File
@@ -2753,10 +2753,10 @@ function currentTimeMix(art) {
def(art, "currentTime", {
get: () => $video.currentTime || 0,
set: (time2) => {
time2 = Number.parseFloat(time2);
if (Number.isNaN(time2))
const parsed = Number.parseFloat(time2);
if (Number.isNaN(parsed))
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) {
const { $video } = art.template;
def(art, "loaded", {
get: () => art.loadedTime / $video.duration
});
const target = art;
def(art, "loaded", { get: () => target.loadedTime / $video.duration });
def(art, "loadedTime", {
get: () => {
return $video.buffered.length ? $video.buffered.end($video.buffered.length - 1) : 0;
}
get: () => $video.buffered.length ? $video.buffered.end($video.buffered.length - 1) : 0
});
}
function miniMix(art) {
@@ -3384,11 +3381,8 @@ function pipMix(art) {
}
}
function playbackRateMix(art) {
const {
template: { $video },
i18n,
notice
} = art;
const { template: { $video }, i18n, notice } = art;
const target = art;
def(art, "playbackRate", {
get() {
return $video.playbackRate;
@@ -3400,15 +3394,13 @@ function playbackRateMix(art) {
$video.playbackRate = rate;
notice.show = `${i18n.get("Rate")}: ${rate === 1 ? i18n.get("Normal") : `${rate}x`}`;
} else {
art.playbackRate = 1;
target.playbackRate = 1;
}
}
});
}
function playedMix(art) {
def(art, "played", {
get: () => art.currentTime / art.duration
});
def(art, "played", { get: () => art.currentTime / art.duration });
}
function playingMix(art) {
const { $video } = art.template;
@@ -3629,20 +3621,20 @@ function seekMix(art) {
def(art, "seek", {
set(time2) {
art.currentTime = time2;
if (art.duration) {
if (art.duration)
notice.show = `${secondToTime(art.currentTime)} / ${secondToTime(art.duration)}`;
}
art.emit("seek", art.currentTime, time2);
}
});
const target = art;
def(art, "forward", {
set(time2) {
art.seek = art.currentTime + time2;
target.seek = art.currentTime + time2;
}
});
def(art, "backward", {
set(time2) {
art.seek = art.currentTime - time2;
target.seek = art.currentTime - time2;
}
});
}
@@ -3653,9 +3645,8 @@ function stateMix(art) {
set(name) {
for (let index = 0; index < states2.length; index++) {
const prop = states2[index];
if (prop !== name && art[prop]) {
if (prop !== name && art[prop])
art[prop] = false;
}
}
}
});
@@ -3902,11 +3893,10 @@ function thumbnailsMix(art) {
function toggleMix(art) {
def(art, "toggle", {
value() {
if (art.playing) {
if (art.playing)
return art.pause();
} else {
else
return art.play();
}
}
});
}
@@ -3995,20 +3985,14 @@ function urlMix(art) {
});
}
function volumeMix(art) {
const {
template: { $video },
i18n,
notice,
storage
} = 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) {
notice.show = `${i18n.get("Volume")}: ${Number.parseInt(String($video.volume * 100), 10)}`;
if ($video.volume !== 0)
storage.set("volume", $video.volume);
}
}
});
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",
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
"typecheck": "node scripts/typecheck.mjs",
"test:unit": "node --test test/playback.test.js test/dash-control.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js",
"test: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:browser": "playwright test",
"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
### 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)
`player/urlMix.ts` and `player/switchMix.ts` preserve the public descriptors.
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),r.exports);const a=t(o),n="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";function t(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var e,r={exports:{}};var o=(e||(e=1,r.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var a=t(r);if("boolean"===a)return"boolean";if("string"===a)return"string";if("number"===a)return"number";if("symbol"===a)return"symbol";if("function"===a)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(a=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return a.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function a(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return n(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),n(s,l,c),i(s,l,c),a(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),n(l,c,d),i(l,c,d),a(l,c,d)})}}(t,e,o),t}function n(t,e,o){if("string"===r(e)){var a=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return a===t}):e.toLowerCase().trim()===a))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(a,"'"))}}function i(t,e,o){if("function"===r(e)){var a=e(t,r(t),o);if(!0!==a){var n=r(a);throw"string"===n?new Error(a):"error"===n?a:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(a,"'"))}}}return a.kindOf=r,a}()),r.exports);const a=t(o),n="5.4.1",i={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webLine truncated
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,n)=>{var a=t=>{try{s(r.next(t))}catch(e){n(e)}},i=t=>{try{s(r.throw(t))}catch(e){n(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.Artplayer=e(),define(function(){return t.Artplayer})):(t="undefined"!==typeof globalThis?globalThis:t||self).Artplayer=e()}(this,function(){"use strict";var t,e=Object.defineProperty,r=Object.defineProperties,o=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable,s=(t,r,o)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[r]=o,l=(t,e)=>{for(var r in e||(e={}))a.call(e,r)&&s(t,r,e[r]);if(n)for(var r of n(e))i.call(e,r)&&s(t,r,e[r]);return t},c=(t,e)=>r(t,o(e)),d=(t,e,r)=>new Promise((o,n)=>{var a=t=>{try{s(r.next(t))}catch(e){n(e)}},i=t=>{try{s(r.throw(t))}catch(e){n(e)}},s=t=>t.done?o(t.value):Promise.resolve(t.value).then(a,i);s((r=r.apply(t,e)).next())});function p(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var h,u={exports:{}};var m=(h||(h=1,u.exports=function(){function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}var e=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var n=t(r);if("boolean"===n)return"boolean";if("string"===n)return"string";if("number"===n)return"number";if("symbol"===n)return"symbol";if("function"===n)return"GeneratorFunction"===o(r)?"generatorfunction":"function";if(function(t){return Array.isArray?Array.isArray(t):t instanceof Array}(r))return"array";if(function(t){return!(!t.constructor||"function"!=typeof t.constructor.isBuffer)&&t.constructor.isBuffer(t)}(r))return"buffer";if(function(t){try{if("number"==typeof t.length&&"function"==typeof t.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(t){return t instanceof Date||"function"==typeof t.toDateString&&"function"==typeof t.getDate&&"function"==typeof t.setDate}(r))return"date";if(function(t){return t instanceof Error||"string"==typeof t.message&&t.constructor&&"number"==typeof t.constructor.stackTraceLimit}(r))return"error";if(function(t){return t instanceof RegExp||"string"==typeof t.flags&&"boolean"==typeof t.ignoreCase&&"boolean"==typeof t.multiline&&"boolean"==typeof t.global}(r))return"regexp";switch(o(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(t){return"function"==typeof t.throw&&"function"==typeof t.return&&"function"==typeof t.next}(r))return"generator";switch(n=e.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return n.slice(8,-1).toLowerCase().replace(/\s/g,"")};function o(t){return t.constructor?t.constructor.name:null}function n(t,e){var o=2<arguments.length&&void 0!==arguments[2]?arguments[2]:["option"];return a(t,e,o),i(t,e,o),function(t,e,o){var s=r(e),l=r(t);if("object"===s){if("object"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'object' type, but got '").concat(l,"'"));Object.keys(e).forEach(function(r){var s=t[r],l=e[r],c=o.slice();c.push(r),a(s,l,c),i(s,l,c),n(s,l,c)})}if("array"===s){if("array"!==l)throw new Error("[Type Error]: '".concat(o.join("."),"' require 'array' type, but got '").concat(l,"'"));t.forEach(function(r,s){var l=t[s],c=e[s]||e[0],d=o.slice();d.push(s),a(l,c,d),i(l,c,d),n(l,c,d)})}}(t,e,o),t}function a(t,e,o){if("string"===r(e)){var n=r(t);if("?"===e[0]&&(e=e.slice(1)+"|undefined"),!(-1<e.indexOf("|")?e.split("|").map(function(t){return t.toLowerCase().trim()}).filter(Boolean).some(function(t){return n===t}):e.toLowerCase().trim()===n))throw new Error("[Type Error]: '".concat(o.join("."),"' require '").concat(e,"' type, but got '").concat(n,"'"))}}function i(t,e,o){if("function"===r(e)){var n=e(t,r(t),o);if(!0!==n){var a=r(n);throw"string"===a?new Error(n):"error"===a?n:new Error("[Validator Error]: The scheme for '".concat(o.join("."),"' validator require return true, but got '").concat(n,"'"))}}}return n.kindOf=r,n}()),u.exports);const f=p(m),g="5.4.1",v={properties:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","meLine truncated
+20 -36
View File
@@ -2753,10 +2753,10 @@ function currentTimeMix(art) {
def(art, "currentTime", {
get: () => $video.currentTime || 0,
set: (time2) => {
time2 = Number.parseFloat(time2);
if (Number.isNaN(time2))
const parsed = Number.parseFloat(time2);
if (Number.isNaN(parsed))
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) {
const { $video } = art.template;
def(art, "loaded", {
get: () => art.loadedTime / $video.duration
});
const target = art;
def(art, "loaded", { get: () => target.loadedTime / $video.duration });
def(art, "loadedTime", {
get: () => {
return $video.buffered.length ? $video.buffered.end($video.buffered.length - 1) : 0;
}
get: () => $video.buffered.length ? $video.buffered.end($video.buffered.length - 1) : 0
});
}
function miniMix(art) {
@@ -3384,11 +3381,8 @@ function pipMix(art) {
}
}
function playbackRateMix(art) {
const {
template: { $video },
i18n,
notice
} = art;
const { template: { $video }, i18n, notice } = art;
const target = art;
def(art, "playbackRate", {
get() {
return $video.playbackRate;
@@ -3400,15 +3394,13 @@ function playbackRateMix(art) {
$video.playbackRate = rate;
notice.show = `${i18n.get("Rate")}: ${rate === 1 ? i18n.get("Normal") : `${rate}x`}`;
} else {
art.playbackRate = 1;
target.playbackRate = 1;
}
}
});
}
function playedMix(art) {
def(art, "played", {
get: () => art.currentTime / art.duration
});
def(art, "played", { get: () => art.currentTime / art.duration });
}
function playingMix(art) {
const { $video } = art.template;
@@ -3629,20 +3621,20 @@ function seekMix(art) {
def(art, "seek", {
set(time2) {
art.currentTime = time2;
if (art.duration) {
if (art.duration)
notice.show = `${secondToTime(art.currentTime)} / ${secondToTime(art.duration)}`;
}
art.emit("seek", art.currentTime, time2);
}
});
const target = art;
def(art, "forward", {
set(time2) {
art.seek = art.currentTime + time2;
target.seek = art.currentTime + time2;
}
});
def(art, "backward", {
set(time2) {
art.seek = art.currentTime - time2;
target.seek = art.currentTime - time2;
}
});
}
@@ -3653,9 +3645,8 @@ function stateMix(art) {
set(name) {
for (let index = 0; index < states2.length; index++) {
const prop = states2[index];
if (prop !== name && art[prop]) {
if (prop !== name && art[prop])
art[prop] = false;
}
}
}
});
@@ -3902,11 +3893,10 @@ function thumbnailsMix(art) {
function toggleMix(art) {
def(art, "toggle", {
value() {
if (art.playing) {
if (art.playing)
return art.pause();
} else {
else
return art.play();
}
}
});
}
@@ -3995,20 +3985,14 @@ function urlMix(art) {
});
}
function volumeMix(art) {
const {
template: { $video },
i18n,
notice,
storage
} = 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) {
notice.show = `${i18n.get("Volume")}: ${Number.parseInt(String($video.volume * 100), 10)}`;
if ($video.volume !== 0)
storage.set("volume", $video.volume);
}
}
});
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 {
switchUrl: (url: string) => Promise<void>
switchQuality: (url: string) => Promise<void>
get switch(): undefined
set switch(url: string)
}
+32 -10
View File
@@ -33,16 +33,33 @@ art.on('subtitleBeforeUpdate', (cues: VTTCue[]) => {
## Retained conflicts and owners
| 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 |
CORE-10 adds an accurate method view without wrapping or replacing the player:
```ts
import type { PlaybackControls } from 'artplayer'
const playback: PlaybackControls = art
const pending = playback.toggle()
if (pending)
pending.catch(error => console.error(error))
```
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
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
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
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
`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
shims and minimal playback/layout dependencies. These types are not exported by
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 OptionInput = Definition.OptionInput
type Player = Definition.Player
type PlaybackControls = Definition.PlaybackControls
type PluginFactory<Host = Artplayer, Result = unknown> = Definition.PluginFactory<Host, Result>
type Plugins = Definition.Plugins
type Setting = Definition.Setting
+1 -1
View File
@@ -1,2 +1,2 @@
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 { Icons } from './icons'
import type { Option, OptionInput } from './option'
import type { PlaybackControls } from './playback'
import type { Player } from './player'
import type { PluginFactory, Plugins } from './plugin'
import type { Setting, SettingOption } from './setting'
@@ -20,6 +21,7 @@ export type {
Icons,
Option,
OptionInput,
PlaybackControls,
Player,
PluginFactory,
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 个包及工作区/示例。
状态: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。
@@ -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-08 | artplayer<br>整理插件管理器与扩展类型 | CORE-03, CORE-07 | 注册流程、同步/异步结果、插件/事件扩展接口 | 旧插件同步可见性、重名与命名、销毁中异步插件保持明确语义;处理 baselines/lifecycle-coverage.md 的 BASE-LIFE-03 并增加候选正向回归;接续 CORE-07 / BASE-TYPE-04 的同步/异步注册返回声明冲突,内部返回类型精确,保留历史消费者 | H | done |
| CORE-09 | artplayer<br>整理 URL 与切源操作 | CORE-04, CORE-06, CORE-08 | url/switch/customType 的操作身份及取消方案 | 并发切源、错误、同 URL、销毁和 Promise 结算有契约;处理 baselines/lifecycle-coverage.md 的 BASE-LIFE-01/02/06 并增加候选正向回归 | H | done |
| CORE-10 | artplayer<br>迁移播放与时间/状态属性 | CORE-09 | play/pause/toggle/seek/volume/rate/duration 等模块 | 旧返回值、拒绝、mutex、位置恢复、属性描述符与事件通过;接续 CORE-07 / BASE-TYPE-04 的 toggle void 与实际 Promise 分支冲突,不吞拒绝 | H | 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-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 |
@@ -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-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-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-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-08: [记录](changes/2026-09-11-CORE-08-plugin-manager.md) [记录](baselines/plugins-validation.json)
- CORE-09: [记录](changes/2026-09-11-CORE-09-source-operations.md) [记录](baselines/source-operations-validation.json)
- CORE-10: [记录](changes/2026-09-11-CORE-10-playback-properties.md) [记录](baselines/playback-properties-validation.json)
- PKG-CHAPTER-01: [记录](changes/2026-09-10-PKG-CHAPTER-01-contract.md) [记录](baselines/chapter-contract.md)
- PKG-CHAPTER-02: [记录](changes/2026-09-10-PKG-CHAPTER-02-tests.md) [记录](baselines/chapter-validation.json)
- PKG-CHAPTER-03: [记录](changes/2026-09-10-PKG-CHAPTER-03-typescript-modules.md) [记录](baselines/chapter-migration-validation.json)
+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 与切源操作完成
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 |
| BASE-TYPE-06 | resolved / 已复现 | Public Option requires url and excludes numeric control HTML accepted by existing JS | CORE-07 |
| BASE-TYPE-07 | open / 已复现 | Setting returns, subtitle contextual payloads and notice getter conflict with historical declarations | CORE-07, CORE-14, CORE-15, CORE-18, CORE-21 |
| BASE-TYPE-08 | open / 已复现 | Write-only player commands have fictitious public getter declarations | CORE-21 |
+21 -1
View File
@@ -281,7 +281,8 @@
],
"compatibleResolution": "保留旧声明接受范围并协调精确重载/扩展,不静默把同步注册改成 async。",
"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",
@@ -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.",
"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."
},
{
"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 [
value => value.items = value.items.filter(item => item.id !== 'BASE-LIFE-04'),
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'],
]) {
const changed = structuredClone(register)
+6 -3
View File
@@ -930,11 +930,14 @@
"dependsOn": [
"CORE-09"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "play/pause/toggle/seek/volume/rate/duration 等模块",
"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",
@@ -1123,7 +1126,7 @@
"status": "todo",
"risk": "H",
"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": []
},
{
+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']]) {
const next = mode.startsWith('nodenext')
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')
inputs.push('commonjs')
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)`)
}
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}`)
}
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 updated: Artplayer['setting'] = art.setting.update({ name: 'entry', html: '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) => {
const historicalCue: VTTCue = cue
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]