refactor(dash): [PKG-DASH-04] migrate strict TypeScript and compatible declarations

This commit is contained in:
Harvey Zhao committed 2026-09-12 05:18:47 +08:00
1 parent 7de04573b6
commit 6840544e0e
38 files changed
+1744 -102

No files matched your search

+2 -1
View File
@@ -240,7 +240,8 @@ When editing proxy packages:
The reference implementation for adaptive selector UI is:
- `packages/artplayer-plugin-hls-control/src/index.js`
- `packages/artplayer-plugin-hls-control/src/index.ts`
- `packages/artplayer-plugin-dash-control/src/index.ts` (SDK 4.x/5.x capability adapter)
If adding HLS-like quality/audio selection elsewhere:
+42 -13
View File
@@ -1,17 +1,46 @@
interface Config {
control?: boolean
setting?: boolean
title?: string
auto?: string
getName?: (level: object) => string
// Generated from the package public declaration by yarn build:ts. Do not edit.
/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */
declare namespace artplayerPluginDashControlDefinitions {
export interface QualityLevel {
height: number
width?: number
id?: string | number
qualityIndex?: number
bitrate?: number
bitrateInKbit?: number
}
export interface AudioTrack {
id?: string | number
index?: number
lang?: string
}
export interface Config<Item extends object = object> {
control?: boolean
setting?: boolean
title?: string
auto?: string
/** Called without a receiver or index, with the original SDK object. */
getName?: (item: Item) => string
}
export interface Option<Level extends object = QualityLevel, Track extends object = AudioTrack> {
quality?: Config<Level>
audio?: Config<Track>
}
export interface Result {
name: 'artplayerPluginDashControl'
update: () => void
}
export function artplayerPluginDashControl<Level extends object = QualityLevel, Track extends object = AudioTrack>(option?: Option<Level, Track>): (art: Artplayer) => Result
// Preserve the required last signature for historical Parameters<typeof factory>[0] consumers.
export function artplayerPluginDashControl<Level extends object = QualityLevel, Track extends object = AudioTrack>(option: Option<Level, Track>): (art: Artplayer) => Result
}
declare const artplayerPluginDashControl: (option: { quality?: Config, audio?: Config }) => (art: Artplayer) => {
name: 'artplayerPluginDashControl'
update: () => void
declare const artplayerPluginDashControl: typeof artplayerPluginDashControlDefinitions.artplayerPluginDashControl
declare namespace artplayerPluginDashControl {
export type QualityLevel = artplayerPluginDashControlDefinitions.QualityLevel
export type AudioTrack = artplayerPluginDashControlDefinitions.AudioTrack
export type Config<Item extends object = object> = artplayerPluginDashControlDefinitions.Config<Item>
export type Option<Level extends object = QualityLevel, Track extends object = AudioTrack> = artplayerPluginDashControlDefinitions.Option<Level, Track>
export type Result = artplayerPluginDashControlDefinitions.Result
}
export default artplayerPluginDashControl
export = artplayerPluginDashControl
export as namespace artplayerPluginDashControl;
@@ -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.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function e(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){if(!o||r.get(n)!==o)return;r.delete(n);const l=t[n];"controls"===n&&l.cache?.get&&l.cache.get(e)?.option?.onSelect!==o||"setting"===n&&l.find&&l.find(e)?.onSelect!==o||l.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,s,u){if(!s)return void i();const a={};o=a;const f=()=>a===o&&u(),d=e=>f()?(s.select(e,f),f()?(t.notice.show=`${s.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:s.html,style:{padding:"0 10px"},selector:s.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:s.html,html:s.title,icon:n,width:200,selector:s.selector,onSelect:d})):l("setting"))},clear:i}}return function(n={}){return o=>{const{$video:r}=o.template,{errorHandle:l}=o.constructor.utils;let i=!1,c=0;const s=e(o,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),u=e(o,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),a=[];function f(t=()=>!0){let e;for(const o of[s.clear,u.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function d(){if(i||o.isDestroy)return;const e=++c,a=o.dash,d=()=>!i&&!o.isDestroy&&e===c&&o.dash===a,h=()=>d()&&a.getVideoElement()===r&&d();try{if(l(a.getVideoElement()===r,'Cannot find instance of DASH from "art.dash"'),!d())return;const e=n.quality||{},o=function(e,n,o){const r=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(e),l=r.levels();if(!o()||!l?.length)return null;const i=n.auto||"Auto",c=n.getName||(t=>`${t.height}p`),s=r.current(l);if(!o())return null;const u=e.getSettings().streaming.abr.autoSwitchBitrate.video;if(!o())return null;const a=!u&&s?c(s):i;if(!o())return null;const f=[];for(const[t,h]of l.entries()){const e=c(h);if(!o())return null;f.push({html:e,...r.item(h,t,s,u)})}const d=t(f).sort((t,e)=>e.value-t.value);return d.push({html:i,value:"auto",default:u}),{html:a,title:n.title||"Quality",selector:d,select:r.select}}(a,e,h);if(!h())return;if(s.update(e,o,h),!h())return;const i=n.audio||{},c=function(e,n,o){const r=e.getTracksFor("audio");if(!o()||!r?.length)return null;const l=n.auto||"Auto",i=n.getName||(t=>t.lang||t.id),c=e.getCurrentTrackFor("audio")||r[0];if(!o())return null;const s=c?i(c):l;if(!o())return null;const u=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=t.filter(t=>["id","index","lang"].every(n=>null==e[n]||t[n]===e[n]));return 1===n.length?n[0]:void 0}(r,c),a=[];for(const t of r){const e=i(t);if(!o())return null;a.push({html:e,value:t,default:t===u})}const f=t(a);return{html:s,title:n.title||"Audio",selector:f,select:t=>e.setCurrentTrack(t.value)}}(a,i,h);h()&&u.update(i,c,h)}catch(g){if(d()){const t=++c;try{f(()=>c===t)}catch(p){console.warn("ArtPlayer DASH cleanup failed:",p)}}throw g}}function h(){if(i)return;let t;i=!0,c++;const e=[f,...a.splice(0).map(([t,e])=>()=>o.off(t,e))Line truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function e(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){if(!o||r.get(n)!==o)return;r.delete(n);const l=t[n];"controls"===n&&l.cache?.get&&l.cache.get(e)?.option?.onSelect!==o||"setting"===n&&l.find&&l.find(e)?.onSelect!==o||l.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,s,u){if(!s)return void i();const a={};o=a;const f=()=>a===o&&u(),d=e=>f()?(s.select(e,f),f()?(t.notice.show=`${s.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:s.html,style:{padding:"0 10px"},selector:s.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:s.html,html:s.title,icon:n,width:200,selector:s.selector,onSelect:d})):l("setting"))},clear:i}}return function(n={}){return o=>{const r=o,{$video:l}=r.template,{errorHandle:i}=r.constructor.utils;let c=!1,s=0;const u=e(r,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),a=e(r,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),f=[];function d(t=()=>!0){let e;for(const o of[u.clear,a.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function h(){if(c||r.isDestroy)return;const e=++s,o=r.dash,f=()=>!c&&!r.isDestroy&&e===s&&r.dash===o,h=()=>f()&&o.getVideoElement()===l&&f();try{if(i(o.getVideoElement()===l,'Cannot find instance of DASH from "art.dash"'),!f())return;const e=n.quality||{},r=function(e,n,o){const r=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(e),l=r.levels();if(!o()||!l?.length)return null;const i=n.auto||"Auto",c=n.getName||(t=>`${t.height}p`),s=r.current(l);if(!o())return null;const u=e.getSettings().streaming.abr.autoSwitchBitrate.video;if(!o())return null;const a=!u&&s?c(s):i;if(!o())return null;const f=[];for(const[t,h]of l.entries()){const e=c(h);if(!o())return null;f.push({html:e,...r.item(h,t,s,u)})}const d=t(f).sort((t,e)=>e.value-t.value);return d.push({html:i,value:"auto",default:u}),{html:a,title:n.title||"Quality",selector:d,select:r.select}}(o,e,h);if(!h())return;if(u.update(e,r,h),!h())return;const c=n.audio||{},s=function(e,n,o){const r=e.getTracksFor("audio");if(!o()||!r?.length)return null;const l=n.auto||"Auto",i=n.getName||(t=>t.lang||t.id),c=e.getCurrentTrackFor("audio")||r[0];if(!o())return null;const s=c?i(c):l;if(!o())return null;const u=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=["id","index","lang"],o=t.filter(t=>n.every(n=>null==e[n]||t[n]===e[n]));return 1===o.length?o[0]:void 0}(r,c),a=[];for(const t of r){const e=i(t);if(!o())return null;a.push({html:e,value:t,default:t===u})}const f=t(a);return{html:s,title:n.title||"Audio",selector:f,select:t=>e.setCurrentTrack(t.value)}}(o,c,h);h()&&a.update(c,s,h)}catch(g){if(f()){const t=++s;try{d(()=>s===t)}catch(p){console.warn("ArtPlayer DASH cleanup failed:",p)}}throw g}}function g(){if(c)return;let t;c=!0,s++;const e=[d,...f.splice(0).map(([t,e])=>()=>r.Line truncated
@@ -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.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,r=(e,n,o)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[n]=o,l=(t,l)=>{for(var i in l||(l={}))n.call(l,i)&&r(t,i,l[i]);if(e)for(var i of e(l))o.call(l,i)&&r(t,i,l[i]);return t};function i(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function c(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){var l,i,c,u;if(!o||r.get(n)!==o)return;r.delete(n);const s=t[n];"controls"===n&&(null==(l=s.cache)?void 0:l.get)&&(null==(c=null==(i=s.cache.get(e))?void 0:i.option)?void 0:c.onSelect)!==o||"setting"===n&&s.find&&(null==(u=s.find(e))?void 0:u.onSelect)!==o||s.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,u,s){if(!u)return void i();const a={};o=a;const f=()=>a===o&&s(),d=e=>f()?(u.select(e,f),f()?(t.notice.show=`${u.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:u.html,style:{padding:"0 10px"},selector:u.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:u.html,html:u.title,icon:n,width:200,selector:u.selector,onSelect:d})):l("setting"))},clear:i}}return function(t={}){return e=>{const{$video:n}=e.template,{errorHandle:o}=e.constructor.utils;let r=!1,u=0;const s=c(e,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),a=c(e,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),f=[];function d(t=()=>!0){let e;for(const o of[s.clear,a.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function h(){if(r||e.isDestroy)return;const c=++u,f=e.dash,h=()=>!r&&!e.isDestroy&&c===u&&e.dash===f,p=()=>h()&&f.getVideoElement()===n&&h();try{if(o(f.getVideoElement()===n,'Cannot find instance of DASH from "art.dash"'),!h())return;const e=t.quality||{},r=function(t,e,n){const o=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(t),r=o.levels();if(!n()||!(null==r?void 0:r.length))return null;const c=e.auto||"Auto",u=e.getName||(t=>`${t.height}p`),s=o.current(r);if(!n())return null;const a=t.getSettings().streaming.abr.autoSwitchBitrate.video;if(!n())return null;const f=!a&&s?u(s):c;if(!n())return null;const d=[];for(const[i,p]of r.entries()){const t=u(p);if(!n())return null;d.push(l({html:t},o.item(p,i,s,a)))}const h=i(d).sort((t,e)=>e.value-t.value);return h.push({html:c,value:"auto",default:a}),{html:f,title:e.title||"Quality",selector:h,select:o.select}}(f,e,p);if(!p())return;if(s.update(e,r,p),!p())return;const c=t.audio||{},u=function(t,e,n){const o=t.getTracksFor("audio");if(!n()||!(null==o?void 0:o.length))return null;const r=e.auto||"Auto",l=e.getName||(t=>t.lang||t.id),c=t.getCurrentTrackFor("audio")||o[0];if(!n())return null;const u=c?l(c):r;if(!n())return null;const s=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=t.filter(t=>["id","index","lang"].every(n=>null==e[n]||t[n]===e[n]));return 1===n.lLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,r=(e,n,o)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[n]=o,l=(t,l)=>{for(var i in l||(l={}))n.call(l,i)&&r(t,i,l[i]);if(e)for(var i of e(l))o.call(l,i)&&r(t,i,l[i]);return t};function i(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function c(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){var l,i,c,u;if(!o||r.get(n)!==o)return;r.delete(n);const s=t[n];"controls"===n&&(null==(l=s.cache)?void 0:l.get)&&(null==(c=null==(i=s.cache.get(e))?void 0:i.option)?void 0:c.onSelect)!==o||"setting"===n&&s.find&&(null==(u=s.find(e))?void 0:u.onSelect)!==o||s.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,u,s){if(!u)return void i();const a={};o=a;const f=()=>a===o&&s(),d=e=>f()?(u.select(e,f),f()?(t.notice.show=`${u.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:u.html,style:{padding:"0 10px"},selector:u.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:u.html,html:u.title,icon:n,width:200,selector:u.selector,onSelect:d})):l("setting"))},clear:i}}return function(t={}){return e=>{const n=e,{$video:o}=n.template,{errorHandle:r}=n.constructor.utils;let u=!1,s=0;const a=c(n,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),f=c(n,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),d=[];function h(t=()=>!0){let e;for(const o of[a.clear,f.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function p(){if(u||n.isDestroy)return;const e=++s,c=n.dash,d=()=>!u&&!n.isDestroy&&e===s&&n.dash===c,p=()=>d()&&c.getVideoElement()===o&&d();try{if(r(c.getVideoElement()===o,'Cannot find instance of DASH from "art.dash"'),!d())return;const e=t.quality||{},n=function(t,e,n){const o=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(t),r=o.levels();if(!n()||!(null==r?void 0:r.length))return null;const c=e.auto||"Auto",u=e.getName||(t=>`${t.height}p`),s=o.current(r);if(!n())return null;const a=t.getSettings().streaming.abr.autoSwitchBitrate.video;if(!n())return null;const f=!a&&s?u(s):c;if(!n())return null;const d=[];for(const[i,p]of r.entries()){const t=u(p);if(!n())return null;d.push(l({html:t},o.item(p,i,s,a)))}const h=i(d).sort((t,e)=>e.value-t.value);return h.push({html:c,value:"auto",default:a}),{html:f,title:e.title||"Quality",selector:h,select:o.select}}(c,e,p);if(!p())return;if(a.update(e,n,p),!p())return;const u=t.audio||{},s=function(t,e,n){const o=t.getTracksFor("audio");if(!n()||!(null==o?void 0:o.length))return null;const r=e.auto||"Auto",l=e.getName||(t=>t.lang||t.id),c=t.getCurrentTrackFor("audio")||o[0];if(!n())return null;const u=c?l(c):r;if(!n())return null;const s=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=["id","index","lang"],o=t.filter(t=>n.every(n=>null==e[n]||t[n]===e[n]));returLine truncated
@@ -77,7 +77,8 @@ function selectedTrack(tracks, current) {
return current;
if (current.id == null && current.index == null)
return void 0;
const matching = tracks.filter((track) => ["id", "index", "lang"].every((key) => current[key] == null || track[key] === current[key]));
const fields = ["id", "index", "lang"];
const matching = tracks.filter((track) => fields.every((key) => current[key] == null || track[key] === current[key]));
return matching.length === 1 ? matching[0] : void 0;
}
function audioModel(dash, config, active) {
@@ -170,7 +171,8 @@ function createMenu(art, name, icon) {
}
const $quality = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
function artplayerPluginDashControl(option = {}) {
return (art) => {
return (player) => {
const art = player;
const { $video } = art.template;
const { errorHandle } = art.constructor.utils;
let closed = false;
@@ -244,7 +246,8 @@ function artplayerPluginDashControl(option = {}) {
throw failure;
}
try {
for (const entry of [["ready", update], ["restart", update], ["destroy", destroy]]) {
const entries = [["ready", update], ["restart", update], ["destroy", destroy]];
for (const entry of entries) {
if (closed)
break;
subscriptions.push(entry);
+2 -1
View File
@@ -60,7 +60,8 @@
"test:package": "node scripts/package-check.mjs",
"test:package:release": "node scripts/package-check.mjs --release",
"build:types": "node scripts/build-types.mjs --write",
"check:types": "node scripts/build-types.mjs --check"
"check:types": "node scripts/build-types.mjs --check",
"test:dash-types-package": "node refactor/scripts/dash-package-types.mjs"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
@@ -2,4 +2,5 @@ node_modules
.DS_Store
yarn.lock
package-lock.json
src
src
tsconfig.json
@@ -8,18 +8,20 @@ destroys it, changes its source, or adds a runtime dependency on dash.js.
## Module map
| File | Responsibility |
| --- | --- |
| `src/index.js` | Deferred installation, media identity, update revisions, event subscription and cleanup |
| `src/sdk.js` | 4.x qualityIndex vs 5.x representation ID access and manual/Auto selection |
| `src/mapping.js` | Names, current item matching, duplicate labels and selector ordering |
| `src/menu.js` | Existing control/setting registries, menu ownership and guarded selection callbacks |
| `src/audio.svg`, `src/quality.svg` | Existing setting icons |
| `types/artplayer-plugin-dash-control.d.ts` | Existing public declarations; precise TS migration follows in PKG-DASH-04 |
| File | Responsibility |
| ------------------------------------------ | --------------------------------------------------------------------------------------- |
| `src/index.ts` | Deferred installation, media identity, update revisions, event subscription and cleanup |
| `src/sdk.ts` | 4.x qualityIndex vs 5.x representation ID access and manual/Auto selection |
| `src/mapping.ts` | Names, current item matching, duplicate labels and selector ordering |
| `src/menu.ts` | Existing control/setting registries, menu ownership and guarded selection callbacks |
| `src/audio.svg`, `src/quality.svg` | Existing setting icons |
| `types/artplayer-plugin-dash-control.d.ts` | Public options, generic SDK inputs and callable result |
Dependencies flow from the entry into mapping/menu, and from mapping into the SDK
adapter. Mapping has no DOM dependency. Menu receives a model and validity callback;
it does not choose SDK versions. This is the structural step, still in JavaScript.
it does not choose SDK versions. All five owned modules are checked with strict
TypeScript, noUncheckedIndexedAccess, and skipLibCheck=false; there are no remaining
owned JavaScript modules in this package.
## Selection and lifetime
@@ -54,6 +56,36 @@ Keep `dash-quality`/`dash-audio`, selector shape, right control placement/paddin
setting width/icons, defaults and unbound getName calls compatible. Do not use a
filtered representation array index or absoluteIndex as the SDK 5 selection key.
## Types and compatibility
`src/types.ts` describes the narrow SDK and registry surfaces, menu models, event
names, labels, and cleanup callbacks. SDK keys may be absent in malformed external
data; the internal types retain that possibility instead of asserting every value
exists. SDK methods are optional by generation. Non-null assertions in `sdk.ts`
preserve the selected method family's historical synchronous failure when a caller
provides an incomplete SDK. They do not validate the whole SDK at runtime.
The entry casts only the externally augmented ArtPlayer boundary to the narrow host.
The non-empty track check protects the fallback item. Quality sorting casts apply
before the synthetic Auto row is appended, and the manual selection key cast follows
the Auto branch check. These assertions do not generate runtime conversions or mask
the controlled error/identity tests. Changes to SDK capability detection belong in
`sdk.ts`, not in a global `any` or ambient module escape.
Public `Config`, `Option`, `QualityLevel`, `AudioTrack`, and `Result` are named types.
Default callbacks infer known SDK fields; explicit generic parameters or callback
annotations accept a caller's more detailed SDK interfaces. Original object-based
formatters remain valid. The optional overload supports omitted/undefined options;
the last required overload preserves historical `Parameters<typeof factory>[0]`.
Formatters still receive exactly one argument and return a string; update returns
void synchronously. No global augmentation claims every ArtPlayer has a DASH SDK.
The historical `.d.ts` path stays available. `.d.cts` exports a callable CommonJS
value with a type namespace; `.d.mts` bridges ESM default/named types. Runtime root
and legacy paths are unchanged. `typesVersions` supports old Node10 type resolution
for the legacy subpath. The online editor declaration is generated by `yarn build:ts`
and checked standalone against both TS 4.3.5 and 5.9.3, then consumed by real Monaco.
## Validation and next work
Run at the workspace root with pinned Node and Yarn:
@@ -62,6 +94,9 @@ Run at the workspace root with pinned Node and Yarn:
yarn test:dash-control
yarn build artplayer-plugin-dash-control
yarn test:browser test/browser/dash-control.spec.js --trace on
yarn test:browser test/browser/dash-editor-types.spec.js --trace on
yarn build:ts
yarn test:dash-types-package
yarn ci:check
```
@@ -71,6 +106,14 @@ contracts and lifecycle tests; use the operating system's path delimiter.
and result/trace directory before running another suite because the output is shared.
Tests use frozen npm 1.1.0, a frozen pre-refactor 5.x implementation, and candidates.
`test/types/dash-source.ts` checks source/public factory compatibility;
`test/types/dash-control.ts` covers module modes and rejects invalid calls. The
legacy fixture is checked against the actual published declaration and candidate.
`test:dash-types-package` packs the current core and DASH with Yarn, installs the
tarballs offline outside the workspace, repeats a frozen install and checks all
public type modes without resolving any workspace source. It also verifies that
implementation files/config do not leak into the archive. This is type acceptance;
the full installed runtime and SDK release matrix still belongs to PKG-DASH-06/05.
Browser tests exercise real old/new core DOM, menu clicks, native MP4 and cleanup,
with controlled SDK methods. They do not load dash.js or test MPD/ABR decoding.
PKG-DASH-05 must verify pinned dash.js 4.5.2 and 5.2.1 with real adaptive media and
@@ -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.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function e(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){if(!o||r.get(n)!==o)return;r.delete(n);const l=t[n];"controls"===n&&l.cache?.get&&l.cache.get(e)?.option?.onSelect!==o||"setting"===n&&l.find&&l.find(e)?.onSelect!==o||l.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,s,u){if(!s)return void i();const a={};o=a;const f=()=>a===o&&u(),d=e=>f()?(s.select(e,f),f()?(t.notice.show=`${s.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:s.html,style:{padding:"0 10px"},selector:s.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:s.html,html:s.title,icon:n,width:200,selector:s.selector,onSelect:d})):l("setting"))},clear:i}}return function(n={}){return o=>{const{$video:r}=o.template,{errorHandle:l}=o.constructor.utils;let i=!1,c=0;const s=e(o,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),u=e(o,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),a=[];function f(t=()=>!0){let e;for(const o of[s.clear,u.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function d(){if(i||o.isDestroy)return;const e=++c,a=o.dash,d=()=>!i&&!o.isDestroy&&e===c&&o.dash===a,h=()=>d()&&a.getVideoElement()===r&&d();try{if(l(a.getVideoElement()===r,'Cannot find instance of DASH from "art.dash"'),!d())return;const e=n.quality||{},o=function(e,n,o){const r=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(e),l=r.levels();if(!o()||!l?.length)return null;const i=n.auto||"Auto",c=n.getName||(t=>`${t.height}p`),s=r.current(l);if(!o())return null;const u=e.getSettings().streaming.abr.autoSwitchBitrate.video;if(!o())return null;const a=!u&&s?c(s):i;if(!o())return null;const f=[];for(const[t,h]of l.entries()){const e=c(h);if(!o())return null;f.push({html:e,...r.item(h,t,s,u)})}const d=t(f).sort((t,e)=>e.value-t.value);return d.push({html:i,value:"auto",default:u}),{html:a,title:n.title||"Quality",selector:d,select:r.select}}(a,e,h);if(!h())return;if(s.update(e,o,h),!h())return;const i=n.audio||{},c=function(e,n,o){const r=e.getTracksFor("audio");if(!o()||!r?.length)return null;const l=n.auto||"Auto",i=n.getName||(t=>t.lang||t.id),c=e.getCurrentTrackFor("audio")||r[0];if(!o())return null;const s=c?i(c):l;if(!o())return null;const u=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=t.filter(t=>["id","index","lang"].every(n=>null==e[n]||t[n]===e[n]));return 1===n.length?n[0]:void 0}(r,c),a=[];for(const t of r){const e=i(t);if(!o())return null;a.push({html:e,value:t,default:t===u})}const f=t(a);return{html:s,title:n.title||"Audio",selector:f,select:t=>e.setCurrentTrack(t.value)}}(a,i,h);h()&&u.update(i,c,h)}catch(g){if(d()){const t=++c;try{f(()=>c===t)}catch(p){console.warn("ArtPlayer DASH cleanup failed:",p)}}throw g}}function h(){if(i)return;let t;i=!0,c++;const e=[f,...a.splice(0).map(([t,e])=>()=>o.off(t,e))Line truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function e(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){if(!o||r.get(n)!==o)return;r.delete(n);const l=t[n];"controls"===n&&l.cache?.get&&l.cache.get(e)?.option?.onSelect!==o||"setting"===n&&l.find&&l.find(e)?.onSelect!==o||l.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,s,u){if(!s)return void i();const a={};o=a;const f=()=>a===o&&u(),d=e=>f()?(s.select(e,f),f()?(t.notice.show=`${s.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:s.html,style:{padding:"0 10px"},selector:s.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:s.html,html:s.title,icon:n,width:200,selector:s.selector,onSelect:d})):l("setting"))},clear:i}}return function(n={}){return o=>{const r=o,{$video:l}=r.template,{errorHandle:i}=r.constructor.utils;let c=!1,s=0;const u=e(r,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),a=e(r,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),f=[];function d(t=()=>!0){let e;for(const o of[u.clear,a.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function h(){if(c||r.isDestroy)return;const e=++s,o=r.dash,f=()=>!c&&!r.isDestroy&&e===s&&r.dash===o,h=()=>f()&&o.getVideoElement()===l&&f();try{if(i(o.getVideoElement()===l,'Cannot find instance of DASH from "art.dash"'),!f())return;const e=n.quality||{},r=function(e,n,o){const r=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(e),l=r.levels();if(!o()||!l?.length)return null;const i=n.auto||"Auto",c=n.getName||(t=>`${t.height}p`),s=r.current(l);if(!o())return null;const u=e.getSettings().streaming.abr.autoSwitchBitrate.video;if(!o())return null;const a=!u&&s?c(s):i;if(!o())return null;const f=[];for(const[t,h]of l.entries()){const e=c(h);if(!o())return null;f.push({html:e,...r.item(h,t,s,u)})}const d=t(f).sort((t,e)=>e.value-t.value);return d.push({html:i,value:"auto",default:u}),{html:a,title:n.title||"Quality",selector:d,select:r.select}}(o,e,h);if(!h())return;if(u.update(e,r,h),!h())return;const c=n.audio||{},s=function(e,n,o){const r=e.getTracksFor("audio");if(!o()||!r?.length)return null;const l=n.auto||"Auto",i=n.getName||(t=>t.lang||t.id),c=e.getCurrentTrackFor("audio")||r[0];if(!o())return null;const s=c?i(c):l;if(!o())return null;const u=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=["id","index","lang"],o=t.filter(t=>n.every(n=>null==e[n]||t[n]===e[n]));return 1===o.length?o[0]:void 0}(r,c),a=[];for(const t of r){const e=i(t);if(!o())return null;a.push({html:e,value:t,default:t===u})}const f=t(a);return{html:s,title:n.title||"Audio",selector:f,select:t=>e.setCurrentTrack(t.value)}}(o,c,h);h()&&a.update(c,s,h)}catch(g){if(f()){const t=++s;try{d(()=>s===t)}catch(p){console.warn("ArtPlayer DASH cleanup failed:",p)}}throw g}}function g(){if(c)return;let t;c=!0,s++;const e=[d,...f.splice(0).map(([t,e])=>()=>r.Line truncated
@@ -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.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,r=(e,n,o)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[n]=o,l=(t,l)=>{for(var i in l||(l={}))n.call(l,i)&&r(t,i,l[i]);if(e)for(var i of e(l))o.call(l,i)&&r(t,i,l[i]);return t};function i(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function c(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){var l,i,c,u;if(!o||r.get(n)!==o)return;r.delete(n);const s=t[n];"controls"===n&&(null==(l=s.cache)?void 0:l.get)&&(null==(c=null==(i=s.cache.get(e))?void 0:i.option)?void 0:c.onSelect)!==o||"setting"===n&&s.find&&(null==(u=s.find(e))?void 0:u.onSelect)!==o||s.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,u,s){if(!u)return void i();const a={};o=a;const f=()=>a===o&&s(),d=e=>f()?(u.select(e,f),f()?(t.notice.show=`${u.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:u.html,style:{padding:"0 10px"},selector:u.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:u.html,html:u.title,icon:n,width:200,selector:u.selector,onSelect:d})):l("setting"))},clear:i}}return function(t={}){return e=>{const{$video:n}=e.template,{errorHandle:o}=e.constructor.utils;let r=!1,u=0;const s=c(e,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),a=c(e,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),f=[];function d(t=()=>!0){let e;for(const o of[s.clear,a.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function h(){if(r||e.isDestroy)return;const c=++u,f=e.dash,h=()=>!r&&!e.isDestroy&&c===u&&e.dash===f,p=()=>h()&&f.getVideoElement()===n&&h();try{if(o(f.getVideoElement()===n,'Cannot find instance of DASH from "art.dash"'),!h())return;const e=t.quality||{},r=function(t,e,n){const o=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(t),r=o.levels();if(!n()||!(null==r?void 0:r.length))return null;const c=e.auto||"Auto",u=e.getName||(t=>`${t.height}p`),s=o.current(r);if(!n())return null;const a=t.getSettings().streaming.abr.autoSwitchBitrate.video;if(!n())return null;const f=!a&&s?u(s):c;if(!n())return null;const d=[];for(const[i,p]of r.entries()){const t=u(p);if(!n())return null;d.push(l({html:t},o.item(p,i,s,a)))}const h=i(d).sort((t,e)=>e.value-t.value);return h.push({html:c,value:"auto",default:a}),{html:f,title:e.title||"Quality",selector:h,select:o.select}}(f,e,p);if(!p())return;if(s.update(e,r,p),!p())return;const c=t.audio||{},u=function(t,e,n){const o=t.getTracksFor("audio");if(!n()||!(null==o?void 0:o.length))return null;const r=e.auto||"Auto",l=e.getName||(t=>t.lang||t.id),c=t.getCurrentTrackFor("audio")||o[0];if(!n())return null;const u=c?l(c):r;if(!n())return null;const s=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=t.filter(t=>["id","index","lang"].every(n=>null==e[n]||t[n]===e[n]));return 1===n.lLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,r=(e,n,o)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[n]=o,l=(t,l)=>{for(var i in l||(l={}))n.call(l,i)&&r(t,i,l[i]);if(e)for(var i of e(l))o.call(l,i)&&r(t,i,l[i]);return t};function i(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function c(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){var l,i,c,u;if(!o||r.get(n)!==o)return;r.delete(n);const s=t[n];"controls"===n&&(null==(l=s.cache)?void 0:l.get)&&(null==(c=null==(i=s.cache.get(e))?void 0:i.option)?void 0:c.onSelect)!==o||"setting"===n&&s.find&&(null==(u=s.find(e))?void 0:u.onSelect)!==o||s.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,u,s){if(!u)return void i();const a={};o=a;const f=()=>a===o&&s(),d=e=>f()?(u.select(e,f),f()?(t.notice.show=`${u.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:u.html,style:{padding:"0 10px"},selector:u.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:u.html,html:u.title,icon:n,width:200,selector:u.selector,onSelect:d})):l("setting"))},clear:i}}return function(t={}){return e=>{const n=e,{$video:o}=n.template,{errorHandle:r}=n.constructor.utils;let u=!1,s=0;const a=c(n,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),f=c(n,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),d=[];function h(t=()=>!0){let e;for(const o of[a.clear,f.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function p(){if(u||n.isDestroy)return;const e=++s,c=n.dash,d=()=>!u&&!n.isDestroy&&e===s&&n.dash===c,p=()=>d()&&c.getVideoElement()===o&&d();try{if(r(c.getVideoElement()===o,'Cannot find instance of DASH from "art.dash"'),!d())return;const e=t.quality||{},n=function(t,e,n){const o=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(t),r=o.levels();if(!n()||!(null==r?void 0:r.length))return null;const c=e.auto||"Auto",u=e.getName||(t=>`${t.height}p`),s=o.current(r);if(!n())return null;const a=t.getSettings().streaming.abr.autoSwitchBitrate.video;if(!n())return null;const f=!a&&s?u(s):c;if(!n())return null;const d=[];for(const[i,p]of r.entries()){const t=u(p);if(!n())return null;d.push(l({html:t},o.item(p,i,s,a)))}const h=i(d).sort((t,e)=>e.value-t.value);return h.push({html:c,value:"auto",default:a}),{html:f,title:e.title||"Quality",selector:h,select:o.select}}(c,e,p);if(!p())return;if(a.update(e,n,p),!p())return;const u=t.audio||{},s=function(t,e,n){const o=t.getTracksFor("audio");if(!n()||!(null==o?void 0:o.length))return null;const r=e.auto||"Auto",l=e.getName||(t=>t.lang||t.id),c=t.getCurrentTrackFor("audio")||o[0];if(!n())return null;const u=c?l(c):r;if(!n())return null;const s=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=["id","index","lang"],o=t.filter(t=>n.every(n=>null==e[n]||t[n]===e[n]));returLine truncated
@@ -77,7 +77,8 @@ function selectedTrack(tracks, current) {
return current;
if (current.id == null && current.index == null)
return void 0;
const matching = tracks.filter((track) => ["id", "index", "lang"].every((key) => current[key] == null || track[key] === current[key]));
const fields = ["id", "index", "lang"];
const matching = tracks.filter((track) => fields.every((key) => current[key] == null || track[key] === current[key]));
return matching.length === 1 ? matching[0] : void 0;
}
function audioModel(dash, config, active) {
@@ -170,7 +171,8 @@ function createMenu(art, name, icon) {
}
const $quality = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
function artplayerPluginDashControl(option = {}) {
return (art) => {
return (player) => {
const art = player;
const { $video } = art.template;
const { errorHandle } = art.constructor.utils;
let closed = false;
@@ -244,7 +246,8 @@ function artplayerPluginDashControl(option = {}) {
throw failure;
}
try {
for (const entry of [["ready", update], ["restart", update], ["destroy", destroy]]) {
const entries = [["ready", update], ["restart", update], ["destroy", destroy]];
for (const entry of entries) {
if (closed)
break;
subscriptions.push(entry);
@@ -19,19 +19,36 @@
],
"exports": {
".": {
"types": "./types/artplayer-plugin-dash-control.d.ts",
"import": "./dist/artplayer-plugin-dash-control.mjs",
"require": "./dist/artplayer-plugin-dash-control.js"
"import": {
"types": "./types/artplayer-plugin-dash-control.d.mts",
"default": "./dist/artplayer-plugin-dash-control.mjs"
},
"require": {
"types": "./types/artplayer-plugin-dash-control.d.cts",
"default": "./dist/artplayer-plugin-dash-control.js"
}
},
"./legacy": {
"types": "./types/artplayer-plugin-dash-control.d.ts",
"import": "./dist/artplayer-plugin-dash-control.legacy.js",
"require": "./dist/artplayer-plugin-dash-control.legacy.js"
"import": {
"types": "./types/artplayer-plugin-dash-control.d.cts",
"default": "./dist/artplayer-plugin-dash-control.legacy.js"
},
"require": {
"types": "./types/artplayer-plugin-dash-control.d.cts",
"default": "./dist/artplayer-plugin-dash-control.legacy.js"
}
}
},
"main": "./dist/artplayer-plugin-dash-control.js",
"module": "./dist/artplayer-plugin-dash-control.mjs",
"types": "./types/artplayer-plugin-dash-control.d.ts",
"legacy": "./dist/artplayer-plugin-dash-control.legacy.js",
"browserslist": "last 1 Chrome version"
"browserslist": "last 1 Chrome version",
"typesVersions": {
"*": {
"legacy": [
"types/artplayer-plugin-dash-control.d.ts"
]
}
}
}
@@ -1,20 +1,25 @@
import type Artplayer from 'artplayer'
import type { AudioTrack, Option, QualityLevel, Result } from '../types/artplayer-plugin-dash-control'
import type { AudioItem, Cleanup, EventName, Host, QualityItem, Valid } from './types'
import $audio from './audio.svg?raw'
import { audioModel, qualityModel } from './mapping'
import { createMenu } from './menu'
import $quality from './quality.svg?raw'
export default function artplayerPluginDashControl(option = {}) {
return (art) => {
export default function artplayerPluginDashControl<Level extends object = QualityLevel, Track extends object = AudioTrack>(option: Option<Level, Track> = {}) {
return (player: Artplayer): Result => {
// art.dash is caller-owned; update keeps the historical SDK/media validation boundary.
const art = player as unknown as Host<Level, Track>
const { $video } = art.template
const { errorHandle } = art.constructor.utils
let closed = false
let revision = 0
const quality = createMenu(art, 'dash-quality', $quality)
const audio = createMenu(art, 'dash-audio', $audio)
const subscriptions = []
const quality = createMenu<QualityItem>(art, 'dash-quality', $quality)
const audio = createMenu<AudioItem<Track>>(art, 'dash-audio', $audio)
const subscriptions: [EventName, Cleanup][] = []
function clear(current = () => true) {
let failure
function clear(current: Valid = () => true): void {
let failure: unknown
for (const cleanup of [quality.clear, audio.clear]) {
if (!current())
break
@@ -29,11 +34,11 @@ export default function artplayerPluginDashControl(option = {}) {
throw failure
}
function update() {
function update(): void {
if (closed || art.isDestroy)
return
const version = ++revision
const dash = art.dash
const dash = art.dash!
const current = () => !closed && !art.isDestroy && version === revision && art.dash === dash
const valid = () => current() && dash.getVideoElement() === $video && current()
try {
@@ -66,12 +71,12 @@ export default function artplayerPluginDashControl(option = {}) {
}
}
function destroy() {
function destroy(): void {
if (closed)
return
closed = true
revision++
let failure
let failure: unknown
const actions = [clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))]
for (const action of actions) {
try {
@@ -86,7 +91,8 @@ export default function artplayerPluginDashControl(option = {}) {
}
try {
for (const entry of [['ready', update], ['restart', update], ['destroy', destroy]]) {
const entries: [EventName, Cleanup][] = [['ready', update], ['restart', update], ['destroy', destroy]]
for (const entry of entries) {
if (closed)
break
subscriptions.push(entry)
@@ -1,7 +1,9 @@
import type { Config } from '../types/artplayer-plugin-dash-control'
import type { AudioFields, AudioItem, Dash, Label, MenuModel, QualityFields, QualityItem, SelectorItem, Valid } from './types'
import { qualityAdapter } from './sdk'
function uniqueLabels(items) {
const seen = new Map()
function uniqueLabels<Item extends SelectorItem>(items: Item[]): Item[] {
const seen = new Map<Label, Item>()
return items.filter((item) => {
if (item.html === undefined)
return true
@@ -16,13 +18,13 @@ function uniqueLabels(items) {
})
}
export function qualityModel(dash, config, active) {
export function qualityModel<Level extends object, Track extends object>(dash: Dash<Level, Track>, config: Config<Level>, active: Valid): MenuModel<QualityItem> | null {
const adapter = qualityAdapter(dash)
const levels = adapter.levels()
if (!active() || !levels?.length)
return null
const auto = config.auto || 'Auto'
const getName = config.getName || (level => `${level.height}p`)
const getName = config.getName || ((level: QualityFields) => `${level.height}p`)
const selected = adapter.current(levels)
if (!active())
return null
@@ -32,7 +34,7 @@ export function qualityModel(dash, config, active) {
const html = !automatic && selected ? getName(selected) : auto
if (!active())
return null
const items = []
const items: QualityItem[] = []
for (const [index, level] of levels.entries()) {
const label = getName(level)
if (!active())
@@ -40,34 +42,37 @@ export function qualityModel(dash, config, active) {
items.push({ html: label, ...adapter.item(level, index, selected, automatic) })
}
const selector = uniqueLabels(items)
.sort((left, right) => right.value - left.value)
// The synthetic Auto item is appended after sorting SDK numeric keys.
.sort((left, right) => (right.value as number) - (left.value as number))
selector.push({ html: auto, value: 'auto', default: automatic })
return { html, title: config.title || 'Quality', selector, select: adapter.select }
}
function selectedTrack(tracks, current) {
function selectedTrack<Track extends AudioFields>(tracks: Track[], current: Track): Track | undefined {
if (tracks.includes(current))
return current
if (current.id == null && current.index == null)
return undefined
const matching = tracks.filter(track => ['id', 'index', 'lang'].every(key => current[key] == null || track[key] === current[key]))
const fields = ['id', 'index', 'lang'] as const
const matching = tracks.filter(track => fields.every(key => current[key] == null || track[key] === current[key]))
return matching.length === 1 ? matching[0] : undefined
}
export function audioModel(dash, config, active) {
export function audioModel<Level extends object, Track extends object>(dash: Dash<Level, Track>, config: Config<Track>, active: Valid): MenuModel<AudioItem<Track>> | null {
const tracks = dash.getTracksFor('audio')
if (!active() || !tracks?.length)
return null
const auto = config.auto || 'Auto'
const getName = config.getName || (track => track.lang || track.id)
const current = dash.getCurrentTrackFor('audio') || tracks[0]
const getName = config.getName || ((track: AudioFields) => track.lang || track.id)
// The non-empty list check above protects the fallback item.
const current = dash.getCurrentTrackFor('audio') || tracks[0]!
if (!active())
return null
const html = current ? getName(current) : auto
if (!active())
return null
const selected = selectedTrack(tracks, current)
const items = []
const items: AudioItem<Track>[] = []
for (const track of tracks) {
const label = getName(track)
if (!active())
@@ -1,8 +1,10 @@
export function createMenu(art, name, icon) {
let current
const owned = new Map()
import type { DisplayConfig, Label, MenuHost, MenuModel, SelectorItem, Valid } from './types'
function remove(surface, callback = owned.get(surface)) {
export function createMenu<Item extends SelectorItem>(art: MenuHost, name: string, icon: string) {
let current: object | undefined
const owned = new Map<'controls' | 'setting', (item: Item) => Label>()
function remove(surface: 'controls' | 'setting', callback = owned.get(surface)): void {
if (!callback || owned.get(surface) !== callback)
return
owned.delete(surface)
@@ -14,9 +16,9 @@ export function createMenu(art, name, icon) {
registry.remove(name)
}
function clear() {
function clear(): void {
current = undefined
let failure
let failure: unknown
for (const [surface, callback] of [...owned]) {
try {
remove(surface, callback)
@@ -29,7 +31,7 @@ export function createMenu(art, name, icon) {
throw failure
}
function update(config, model, active) {
function update(config: DisplayConfig, model: MenuModel<Item> | null, active: Valid): void {
if (!model) {
clear()
return
@@ -37,7 +39,7 @@ export function createMenu(art, name, icon) {
const state = {}
current = state
const valid = () => state === current && active()
const onSelect = (item) => {
const onSelect = (item: Item): Label => {
if (!valid())
return item.html
model.select(item, valid)
@@ -1,11 +1,14 @@
export function qualityAdapter(dash) {
import type { Dash, QualityAdapter } from './types'
export function qualityAdapter<Level extends object, Track extends object>(dash: Dash<Level, Track>): QualityAdapter<Level> {
// Select one SDK method family. Invalid/incomplete SDKs keep synchronous method errors.
const modern = typeof dash.getRepresentationsByType === 'function'
return {
levels: () => modern ? dash.getRepresentationsByType('video') : dash.getBitrateInfoListFor('video'),
levels: () => modern ? dash.getRepresentationsByType!('video') : dash.getBitrateInfoListFor!('video'),
current(levels) {
if (modern)
return dash.getCurrentRepresentationForType('video')
const quality = dash.getQualityFor('video')
return dash.getCurrentRepresentationForType!('video')
const quality = dash.getQualityFor!('video')
return levels.find(level => level.qualityIndex === quality)
},
item(level, index, selected, automatic) {
@@ -18,9 +21,9 @@ export function qualityAdapter(dash) {
dash.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: automatic } } } })
if (!automatic && valid()) {
if (modern)
dash.setRepresentationForTypeById('video', item.id)
dash.setRepresentationForTypeById!('video', item.id)
else
dash.setQualityFor('video', item.value)
dash.setQualityFor!('video', item.value as number | undefined)
}
},
}
@@ -0,0 +1,99 @@
import type { Config } from '../types/artplayer-plugin-dash-control'
export type Label = string | number | undefined
export type Identifier = string | number
export type Valid = () => boolean
export type Cleanup = () => void
export type EventName = 'ready' | 'restart' | 'destroy'
export interface QualityFields {
height?: number
id?: Identifier
qualityIndex?: number
}
export interface AudioFields {
id?: Identifier
index?: number
lang?: string
}
export interface Dash<Level extends object = object, Track extends object = object> {
getVideoElement: () => object | null
getSettings: () => { streaming: { abr: { autoSwitchBitrate: { video: boolean } } } }
updateSettings: (settings: { streaming: { abr: { autoSwitchBitrate: { video: boolean } } } }) => unknown
getRepresentationsByType?: (type: 'video') => (Level & QualityFields)[] | null | undefined
getCurrentRepresentationForType?: (type: 'video') => (Level & QualityFields) | null | undefined
setRepresentationForTypeById?: (type: 'video', id: Identifier | undefined) => unknown
getBitrateInfoListFor?: (type: 'video') => (Level & QualityFields)[] | null | undefined
getQualityFor?: (type: 'video') => number
setQualityFor?: (type: 'video', index: number | undefined) => unknown
getTracksFor: (type: 'audio') => (Track & AudioFields)[] | null | undefined
getCurrentTrackFor: (type: 'audio') => (Track & AudioFields) | null | undefined
setCurrentTrack: (track: Track & AudioFields) => unknown
}
export interface SelectorItem {
html: Label
default: boolean
}
export interface QualityItem extends SelectorItem {
value: number | 'auto' | undefined
id?: Identifier
}
export interface AudioItem<Track extends object> extends SelectorItem {
value: Track & AudioFields
}
export interface QualityAdapter<Level extends object> {
levels: () => (Level & QualityFields)[] | null | undefined
current: (levels: (Level & QualityFields)[]) => (Level & QualityFields) | null | undefined
item: (level: Level & QualityFields, index: number, selected: (Level & QualityFields) | null | undefined, automatic: boolean) => Omit<QualityItem, 'html'>
select: (item: QualityItem, valid: Valid) => void
}
export interface MenuModel<Item extends SelectorItem> {
html: Label
title: string
selector: Item[]
select: (item: Item, valid: Valid) => unknown
}
export interface MenuOption<Item extends SelectorItem> {
name: string
html: Label
selector: Item[]
onSelect: (item: Item) => Label
position?: 'right'
style?: { padding: string }
tooltip?: Label
icon?: string
width?: number
}
export interface Registry {
update: <Item extends SelectorItem>(option: MenuOption<Item>) => unknown
remove: (name: string) => unknown
check: (item: SelectorItem) => unknown
cache?: { get?: (name: string) => { option?: { onSelect?: unknown } } | undefined }
find?: (name: string) => { onSelect?: unknown } | undefined
}
export interface MenuHost {
controls: Registry
setting: Registry
notice: { show: string }
}
export interface Host<Level extends object, Track extends object> extends MenuHost {
template: { $video: object }
constructor: { utils: { errorHandle: (condition: unknown, message: string) => void } }
isDestroy?: boolean
dash?: Dash<Level, Track>
on: (name: EventName, callback: Cleanup) => unknown
off: (name: EventName, callback: Cleanup) => unknown
}
export type DisplayConfig = Pick<Config, 'control' | 'setting'>
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "allowJs": false },
"include": ["src/**/*.ts", "types/**/*.d.ts", "types/**/*.d.mts", "types/**/*.d.cts", "../../types/assets.d.ts"]
}
@@ -0,0 +1,14 @@
/* eslint-disable ts/no-redeclare -- CommonJS callable export and named-type namespace intentionally merge. */
import type * as Definition from './artplayer-plugin-dash-control.js'
declare const artplayerPluginDashControl: typeof Definition.default
declare namespace artplayerPluginDashControl {
type QualityLevel = Definition.QualityLevel
type AudioTrack = Definition.AudioTrack
type Config<Item extends object = object> = Definition.Config<Item>
type Option<Level extends object = QualityLevel, Track extends object = AudioTrack> = Definition.Option<Level, Track>
type Result = Definition.Result
}
export = artplayerPluginDashControl
@@ -0,0 +1,2 @@
export { default } from './artplayer-plugin-dash-control.cjs'
export type { AudioTrack, Config, Option, QualityLevel, Result } from './artplayer-plugin-dash-control.js'
@@ -1,16 +1,41 @@
import type Artplayer from 'artplayer'
interface Config {
export interface QualityLevel {
height: number
width?: number
id?: string | number
qualityIndex?: number
bitrate?: number
bitrateInKbit?: number
}
export interface AudioTrack {
id?: string | number
index?: number
lang?: string
}
export interface Config<Item extends object = object> {
control?: boolean
setting?: boolean
title?: string
auto?: string
getName?: (level: object) => string
/** Called without a receiver or index, with the original SDK object. */
getName?: (item: Item) => string
}
declare const artplayerPluginDashControl: (option: { quality?: Config, audio?: Config }) => (art: Artplayer) => {
export interface Option<Level extends object = QualityLevel, Track extends object = AudioTrack> {
quality?: Config<Level>
audio?: Config<Track>
}
export interface Result {
name: 'artplayerPluginDashControl'
update: () => void
}
declare function artplayerPluginDashControl<Level extends object = QualityLevel, Track extends object = AudioTrack>(option?: Option<Level, Track>): (art: Artplayer) => Result
// Preserve the required last signature for historical Parameters<typeof factory>[0] consumers.
declare function artplayerPluginDashControl<Level extends object = QualityLevel, Track extends object = AudioTrack>(option: Option<Level, Track>): (art: Artplayer) => Result
export default artplayerPluginDashControl
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,49 @@
# PKG-DASH-04:DASH 严格 TS 与兼容公开类型
起点 `7de04573`。四个已拆分 JS 模块迁移为 TS,新增 `src/types.ts` 描述窄宿主、两代 SDK
方法、selector/menu、事件和资源类型。纳入 strict/noUncheckedIndexedAccess,未加入 any、
通配模块声明或 dash.js 依赖。源码工厂直接使用公开 Option/Result,并有双向可赋值夹具。
## 类型兼容
保留原 .d.ts 入口、工厂与字面量插件名、同步 update、单参数无接收者的 getName。
新增 QualityLevel/AudioTrack/Config/Option/Result 命名类型及自定义 SDK 泛型;旧 object
回调依然可用。可选重载允许运行时原本支持的省略/undefined,最后保留必填重载以保护
`Parameters<typeof factory>[0]['quality']`。不要求消费者改变现有调用。
增加 ESM/CommonJS 类型桥和 legacy 的旧解析路径,所有 JS 分发文件路径不变。
原始 npm 声明的 legacy 消费夹具在 TS 4.3.5/5.9.3 通过,候选运行同样夹具也通过。
五种 TS/模块模式的新消费者零诊断,移除 expect-error 后八条非法调用全部被拒绝。
有限断言位于外部 art.dash 宿主、选定 SDK 方法族、非空轨道回退、Auto 与数字键边界。
内部类型保留 SDK 字段缺失可能;断言不引入运行时修正,不把不完整 SDK 静默变成成功。
源码与前一步运行代码相比只有局部别名和类型擦除,已知行为修正仍由 03 的契约测试保护。
详情见 [包内架构](../../packages/artplayer-plugin-dash-control/ARCHITECTURE.md)。
## 编辑器与验证
build:ts 将 DASH 加入已存在的 AST 生成器,生成可独立编译的 callable/global 类型桥,
避免旧 default export 与 export= 混用。实际生成文件有可重复生成、TS 4.3/5.9 正反例验证。
新增 Monaco worker 用例,加载真实声明、检查回调/旧类型提取、拒绝错误返回并执行编译输出。
没有手改 docs/assets/ts,产物由标准脚本生成。实际 SDK 的类型/媒体组合仍由 05 核验。
新增 `yarn test:dash-types-package`,使用现有打包检查和隔离目录工具,在工作区外离线安装
核心与 DASH tarball,冻结锁重装,逐文件核对实际安装字节,编译时禁止回工作区解析类型。
首次打包发现新增 tsconfig.json 泄露,先保留失败再补 .npmignore;最终五种模式及每组八条
非法调用通过。此脚本只验收隔离类型,不替代 06 的完整运行时分发验收。
最终证据见 [冻结记录](../baselines/dash-types-validation.json):
| 验证 | 结果 |
| --- | --- |
| 严格源码 | DASH 5 个自有 TS 模块,全仓 261 个生产 TS 文件通过 |
| 声明与隔离类型 | 新增 3 项类型基线通过;工作区与隔离 tarball 各 5 种模式,8 条非法用法均拒绝;旧实际 npm 声明对照通过 |
| 运行时 | 源码 116 项、带三格式的 Node 266 项通过 |
| 浏览器 | main 90 项(含核心/DASH Monaco 6 项)、legacy 84 项通过,无重试/跳过 |
| 完整 CI | 715 项:661 单元、14 工程、40 基线;隔离打包脚本随后独立执行并通过 |
| 构建 | 标准 build / build:ts 通过,三份 dist 与 docs/compiled 字节一致 |
DASH-TYPE-01 关闭;DASH-SDK-01/DASH-STATE-01、实际 SDK 类型/媒体及设备仍由 05 接续。
当前 217 项:74 done、3 doing、140 todo。没有增加依赖、改变版本或发布。
本地独立提交,回退同时恢复前一步 JS、声明和生成方式;保留 01~03 的基线与修复。
下一步 05 完成真实 dash.js 4.5.2/5.2.1 媒体、组合和示例验证,06 接续隔离安装/分发。
+9 -2
View File
@@ -1,7 +1,8 @@
# DASH Control 验证与维护入口
测试保护真实 npm 1.1.0 的既有调用契约,以及重构前工作区的 SDK 5.x 稳定 ID 修正。
版本来源见 [发布契约](baselines/dash-control-contract.md)。02 建立基线,03 已改造生产实现;
版本来源见 [发布契约](baselines/dash-control-contract.md)。02 建立基线,03 改造运行行为,
04 已把五个自有模块迁移为严格 TS 并补齐公开类型与编辑器生成;
实际模块地图与生命周期规则见 [包内架构](../packages/artplayer-plugin-dash-control/ARCHITECTURE.md)。
## 可重跑的测试
@@ -12,6 +13,12 @@
候选插件 SDK 4/5、两类菜单、两种选择及候选清理、Chromium/Firefox/WebKit。
浏览器套件串行运行,避免共享端口和报告覆盖。
- `yarn ci:check`:工具链、计划、lint、声明漂移、严格类型和 Node/基线测试。
- `yarn test:browser test/browser/dash-editor-types.spec.js --trace on`:三引擎 Monaco 实际
worker 编译全局声明、验证回调/旧类型提取及错误诊断,并执行发出的 JS 工厂。
- `yarn build:ts`:重新生成编辑器声明;`refactor/scripts/dash-types.test.mjs` 校验可复现、
TS 4.3.5/5.9.3、五种模块模式、八条非法用法及实际 npm 旧声明的消费者对照。
- `yarn test:dash-types-package`:Yarn 打包核心/DASH、工作区外离线安装与冻结锁重装,
检查五种模式、CommonJS callable/命名空间、每模式八条非法调用,并拒绝解析回工作区。
Node helper 分别加载当前源码、固定 Git 提交的旧工作区 UMD、实际 npm main/legacy/module。
Git 内容以 LF 指纹核对,发布归档与成员以原始字节指纹核对。`ARTPLAYER_TEST_DASH` 可指定
@@ -45,5 +52,5 @@ Git 内容以 LF 指纹核对,发布归档与成员以原始字节指纹核对
- DASH-LIFE-01:空列表/禁用后 UI、旧回调、销毁后 update/订阅已修正,补充重入和错误回归。
- DASH-STATE-01:唯一克隆当前轨道、重复标签、数字零 ID 已修正;实际 SDK 轨道身份仍由 05 核验。
状态以 risks.json 为准;受控修复不替代真实 SDK 验收。04 完成严格 TS/公开类型兼容,05 完成
状态以 risks.json 为准;受控修复不替代真实 SDK 验收。严格 TS/公开类型已有独立验证,05 完成
真实 SDK/媒体/组合验证,06 完成包分发验收。物理设备和 npm 发布准入仍待完成。
@@ -0,0 +1,12 @@
import Artplayer from 'artplayer'
import dashControl from 'artplayer-plugin-dash-control'
type OldOption = Parameters<typeof dashControl>[0]
type OldQuality = NonNullable<OldOption['quality']>
const options: OldOption = { quality: { control: true, getName: (level: object) => String(level) }, audio: { setting: true } }
const quality: OldQuality = { title: 'Quality', auto: 'Auto' }
const formatter: NonNullable<OldQuality['getName']> = value => String(value)
const art = new Artplayer({ container: '#player', url: 'video.mpd', plugins: [dashControl(options)] })
const plugin: { name: 'artplayerPluginDashControl', update: () => void } = dashControl({ quality })(art)
const done: void = plugin.update()
void [formatter, done]
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 217 项,范围 22 个包及工作区/示例。
状态:todo 141 / doing 3 / blocked 0 / done 73 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 140 / doing 3 / blocked 0 / done 74 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -204,7 +204,7 @@
| PKG-DASH-01 | artplayer-plugin-dash-control<br>核对包契约与历史用法 | BASE-05 | quality/audio、representation ID/Auto、getName 和 update | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-DASH-02 | artplayer-plugin-dash-control<br>建立特有行为与错误测试 | PKG-DASH-01, ENG-03, ENG-05 | 保留已有 5 项稳定 ID 回归,补发布版 SDK 4.x/当前 5.x、音轨/空列表/过滤/换源与关闭引用 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-DASH-03 | artplayer-plugin-dash-control<br>整理内部职责与资源 | PKG-DASH-02, CORE-11, CORE-14 | 能力适配保留 SDK 4.x 与 5.x,稳定 ID 映射、ABR 状态、UI 清理和生命周期职责分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-DASH-04 | artplayer-plugin-dash-control<br>迁移自有源码和公开类型 | PKG-DASH-03, ENG-04, ENG-06, CORE-07 | dash.js adapter、selector 和回调类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| PKG-DASH-04 | artplayer-plugin-dash-control<br>迁移自有源码和公开类型 | PKG-DASH-03, ENG-04, ENG-06, CORE-07 | dash.js adapter、selector 和回调类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done |
| PKG-DASH-05 | artplayer-plugin-dash-control<br>验证新旧核心和组合 | PKG-DASH-04, CORE-22 | 固定 dash.js 4.5.2/5.2.1 与本地 DASH 实际清晰度/音轨、高亮、Auto 和换源组合 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-DASH-06 | artplayer-plugin-dash-control<br>验证分发并同步文档 | PKG-DASH-05, ENG-07 | dash.control.js、支持的 dash.js 版本与产物 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
@@ -482,3 +482,4 @@
- PKG-DASH-01: [记录](changes/2026-09-12-PKG-DASH-01-contract.md) [记录](baselines/dash-control-contract.md) [记录](baselines/dash-control-release.json)
- PKG-DASH-02: [记录](changes/2026-09-12-PKG-DASH-02-tests.md) [记录](dash-validation.md) [记录](baselines/dash-validation.json)
- PKG-DASH-03: [记录](changes/2026-09-12-PKG-DASH-03-runtime.md) [记录](baselines/dash-runtime.json) [记录](dash-validation.md)
- PKG-DASH-04: [记录](changes/2026-09-12-PKG-DASH-04-types.md) [记录](baselines/dash-types-validation.json) [记录](dash-validation.md)
+16
View File
@@ -1,5 +1,21 @@
# 进度与证据
## 最新完成:DASH-04 严格 TS、公开类型和编辑器迁移
DASH 五个自有模块全部 TS 化,源码/公开工厂可互相赋值;默认字段和自定义 SDK 泛型准确,
旧 object 回调、Parameters 配置提取、同步 update 与所有 JS 分发路径保持。
补齐 CJS/ESM/legacy 类型桥和 Monaco 生成声明;实际 npm 旧声明与候选消费者对照通过。
全仓 261 个生产 TS 严格检查,完整 CI 715 项通过;源码 Node 116、三格式 Node 266 项通过。
正式 main 三引擎 90 项(含核心/DASH Monaco 6 项)、legacy 84 项通过,无重试/跳过。
工作区外 Yarn tarball 离线安装/冻结重装后五种类型模式通过,每模式拒绝八条非法用法。
首次发现 tsconfig.json 泄露到包,已保留失败并修正 .npmignore,未手改生成产物。
DASH-TYPE-01 关闭;真实 dash.js/MPD/ABR、SDK 类型与设备/完整分发仍待验证。
当前 217 项:74 done、3 doing、140 todo。下一步 DASH-05 进入真实 SDK 与媒体组合。
详见 [本次变更](changes/2026-09-12-PKG-DASH-04-types.md) 和
[执行证据](baselines/dash-types-validation.json)。独立本地提交,不推送/发布,全项目目标未完成。
## 最新完成:DASH-03 SDK 适配与生命周期源码改造
DASH 源码已拆为入口生命周期、SDK 适配、selector 模型、菜单所有权四个 JS 模块。
+1 -1
View File
@@ -138,5 +138,5 @@
| DASH-SDK-01 | open / 已复现 | npm 发布版 4.x 与工作区仅 5.x 的 SDK 接口不兼容 | PKG-DASH-02, PKG-DASH-03, PKG-DASH-05 |
| DASH-LIFE-01 | resolved / 已复现 | DASH 空拓扑残留 UI,监听和旧选择回调缺乏关闭守卫 | PKG-DASH-02, PKG-DASH-03 |
| DASH-STATE-01 | open / 已复现 | DASH 当前项高亮使用真值 ID、音轨身份和标签去重,边界待核实 | PKG-DASH-02, PKG-DASH-03, PKG-DASH-05 |
| DASH-TYPE-01 | open / 源码/产物事实 | DASH 类型未表达默认工厂参数、SDK 回调对象和现代模块入口 | PKG-DASH-04 |
| DASH-TYPE-01 | resolved / 源码/产物事实 | DASH 类型未表达默认工厂参数、SDK 回调对象和现代模块入口 | PKG-DASH-04 |
| DASH-DEMO-01 | open / 源码/产物事实 | DASH 示例仅新 SDK 且重复安装会累积销毁回调 | PKG-DASH-06 |
+15 -8
View File
@@ -628,7 +628,7 @@
"PKG-DASH-05"
],
"evidence": [
"packages/artplayer-plugin-dash-control/src/index.js",
"packages/artplayer-plugin-dash-control/src/index.ts",
"docs/assets/example/dash.control.js",
"refactor/baselines/dash-control-contract.md"
],
@@ -2844,7 +2844,7 @@
"evidence": [
"refactor/baselines/dash-control-contract.md",
"refactor/baselines/dash-control-release.json",
"packages/artplayer-plugin-dash-control/src/index.js",
"packages/artplayer-plugin-dash-control/src/index.ts",
"packages/artplayer-plugin-dash-control/types/artplayer-plugin-dash-control.d.ts",
"docs/assets/example/dash.control.js",
"test/dash-contract.test.js",
@@ -2867,7 +2867,7 @@
"evidence": [
"refactor/baselines/dash-control-contract.md",
"refactor/baselines/dash-control-release.json",
"packages/artplayer-plugin-dash-control/src/index.js",
"packages/artplayer-plugin-dash-control/src/index.ts",
"packages/artplayer-plugin-dash-control/types/artplayer-plugin-dash-control.d.ts",
"docs/assets/example/dash.control.js",
"test/dash-contract.test.js",
@@ -2897,7 +2897,7 @@
"evidence": [
"refactor/baselines/dash-control-contract.md",
"refactor/baselines/dash-control-release.json",
"packages/artplayer-plugin-dash-control/src/index.js",
"packages/artplayer-plugin-dash-control/src/index.ts",
"packages/artplayer-plugin-dash-control/types/artplayer-plugin-dash-control.d.ts",
"docs/assets/example/dash.control.js",
"test/dash-contract.test.js",
@@ -2912,19 +2912,26 @@
"id": "DASH-TYPE-01",
"title": "DASH 类型未表达默认工厂参数、SDK 回调对象和现代模块入口",
"confirmation": "source-observed",
"status": "open",
"status": "resolved",
"owners": [
"PKG-DASH-04"
],
"evidence": [
"refactor/baselines/dash-control-contract.md",
"refactor/baselines/dash-control-release.json",
"packages/artplayer-plugin-dash-control/src/index.js",
"packages/artplayer-plugin-dash-control/src/index.ts",
"packages/artplayer-plugin-dash-control/types/artplayer-plugin-dash-control.d.ts",
"docs/assets/example/dash.control.js"
],
"compatibleResolution": "保留旧类型推断,提供准确的 SDK 适配/回调契约和兼容格式入口,不以 any 掩盖源码。",
"closureCriteria": "严格源码、旧/新编译器消费者与隔离产物类型验证通过。"
"closureCriteria": "严格源码、旧/新编译器消费者与隔离产物类型验证通过。",
"resolutionEvidence": [
"refactor/baselines/dash-types-validation.json",
"refactor/changes/2026-09-12-PKG-DASH-04-types.md",
"test/types/dash-control.ts",
"refactor/scripts/dash-package-types.mjs"
],
"resolutionRationale": "五个自有 TS 模块严格检查通过,真实旧 npm 声明/候选消费者在 TS 4.3/5.9 兼容,五种模块模式及非法用法验证、真实 Monaco、工作区外 Yarn tarball 安装与类型检查通过。只关闭插件自身声明和格式解析问题,实际 SDK 类型/媒体矩阵仍由 05 接续。"
},
{
"id": "DASH-DEMO-01",
@@ -2937,7 +2944,7 @@
"evidence": [
"refactor/baselines/dash-control-contract.md",
"refactor/baselines/dash-control-release.json",
"packages/artplayer-plugin-dash-control/src/index.js",
"packages/artplayer-plugin-dash-control/src/index.ts",
"packages/artplayer-plugin-dash-control/types/artplayer-plugin-dash-control.d.ts",
"docs/assets/example/dash.control.js"
],
+85
View File
@@ -0,0 +1,85 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import ts from 'typescript'
import compat from 'typescript-compat'
import { checkFiles, packedFiles } from '../../scripts/package-check.mjs'
import { consumerDirectory, removeConsumer, run, workspace, writeJson } from '../../scripts/package-consumer.mjs'
import { hash, readMember } from './releases.mjs'
async function main() {
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Use yarn test:dash-types-package')
const yarn = process.env.npm_execpath
assert(yarn && fs.existsSync(yarn))
const output = fs.mkdtempSync(path.join(workspace, 'refactor/.cache/dash-package-types-'))
console.log(`DASH isolated package type evidence: ${output}`)
const consumer = consumerDirectory()
try {
const packages = []
const published = JSON.parse(fs.readFileSync(path.join(workspace, 'refactor/baselines/dash-control-release.json'))).release
for (const name of ['artplayer', 'artplayer-plugin-dash-control']) {
const archive = path.join(output, `${name}.tgz`)
fs.writeFileSync(path.join(output, `${name}-pack.log`), run([yarn, 'pack', '--filename', archive], path.join(workspace, 'packages', name)))
const files = packedFiles(archive)
const manifest = JSON.parse(readMember(archive, 'package/package.json'))
checkFiles(manifest, files, name === published.name ? Object.keys(published.files) : [])
packages.push({ name, archive, sha256: hash(fs.readFileSync(archive)), files: Object.fromEntries(files.map(member => [member, hash(readMember(archive, member))])) })
}
writeJson(path.join(consumer, 'package.json'), { name: 'dash-isolated-type-consumer', private: true, dependencies: Object.fromEntries(packages.map(pkg => [pkg.name, `file:${pkg.archive.replaceAll('\\', '/')}`])) })
fs.copyFileSync(path.join(workspace, 'yarn.lock'), path.join(consumer, 'yarn.lock'))
fs.writeFileSync(path.join(output, 'install.log'), run([yarn, 'install', '--offline', '--ignore-scripts', '--non-interactive'], consumer))
const lock = fs.readFileSync(path.join(consumer, 'yarn.lock'))
fs.writeFileSync(path.join(output, 'frozen-install.log'), run([yarn, 'install', '--offline', '--frozen-lockfile', '--ignore-scripts', '--non-interactive'], consumer))
assert.deepEqual(fs.readFileSync(path.join(consumer, 'yarn.lock')), lock)
fs.writeFileSync(path.join(output, 'yarn.lock'), lock)
for (const pkg of packages) {
const root = path.join(consumer, 'node_modules', pkg.name)
assert.equal(fs.realpathSync(root), root, 'Installed package is a workspace link')
for (const [member, expected] of Object.entries(pkg.files))
assert.equal(hash(fs.readFileSync(path.join(root, member.slice(8)))), expected, `Installed bytes differ: ${member}`)
}
const matrix = []
for (const [compiler, mode] of [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]) {
const next = mode.startsWith('nodenext')
const extension = next ? mode.endsWith('-cjs') ? 'cts' : 'mts' : 'ts'
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: true, target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], module: next ? compiler.ModuleKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleKind.ESNext : compiler.ModuleKind.CommonJS, moduleResolution: next ? compiler.ModuleResolutionKind.NodeNext : mode === 'bundler-esm' ? compiler.ModuleResolutionKind.Bundler : compiler.ModuleResolutionKind.NodeJs }
const source = fs.readFileSync(path.join(workspace, 'test/types/dash-control.ts'), 'utf8')
const filename = path.join(consumer, `current.${extension}`)
const legacy = path.join(consumer, `legacy.${extension}`)
fs.copyFileSync(path.join(workspace, 'refactor/fixtures/consumers/dash-control-legacy.ts'), legacy)
const files = [filename, legacy]
if (mode === 'nodenext-cjs') {
const commonjs = path.join(consumer, 'commonjs.cts')
fs.writeFileSync(commonjs, `import dash = require('artplayer-plugin-dash-control'); import legacy = require('artplayer-plugin-dash-control/legacy');
const option: dash.Option = { quality: { getName: level => String(level.height) } }; dash(option); legacy();`)
files.push(commonjs)
}
function compile(code) {
fs.writeFileSync(filename, code)
const program = compiler.createProgram(files, options)
for (const file of program.getSourceFiles()) {
const actual = fs.realpathSync(file.fileName)
assert(actual.startsWith(fs.realpathSync(consumer) + path.sep) || (program.isSourceFileDefaultLibrary(file) && path.dirname(actual) === fs.realpathSync(path.dirname(compiler.sys.getExecutingFilePath()))), `Types escaped isolated consumer: ${actual}`)
}
return compiler.getPreEmitDiagnostics(program).map(item => ({ code: item.code, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n') }))
}
const diagnostics = compile(source)
assert.deepEqual(diagnostics, [], `${compiler.version} ${mode}`)
const invalid = compile(source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''))
assert.equal(invalid.length, 8, 'Installed package must reject all invalid uses')
matrix.push({ compiler: compiler.version, mode, diagnostics, invalid })
}
const report = { task: 'PKG-DASH-04', scope: 'Packed current core and DASH, offline Yarn install and frozen reinstall outside workspace; isolated public type acceptance only, not complete package runtime/SDK/media release validation.', packages, matrix }
writeJson(path.join(output, 'report.json'), report)
console.log(`DASH installed type matrix passed: ${matrix.length} modes, eight invalid uses rejected in each; ${output}`)
}
finally {
removeConsumer(consumer)
}
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
+78
View File
@@ -0,0 +1,78 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'
// eslint-disable-next-line test/no-import-node-test -- Run exact compiler and editor compatibility contracts.
import test from 'node:test'
import { ESLint } from 'eslint'
import ts from 'typescript'
import compat from 'typescript-compat'
import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from '../../scripts/plugin-editor-types.mjs'
import { checkConsumer } from '../../scripts/typecheck.mjs'
import { ensureArchive, hash, readMember, refactorDir } from './releases.mjs'
test('DASH public factories preserve root/legacy imports, inferred SDK fields and historical extraction', () => {
const source = fs.readFileSync('test/types/dash-control.ts', 'utf8')
for (const [compiler, mode] of [[ts, 'node10-commonjs'], [ts, 'nodenext-cjs'], [ts, 'nodenext-esm'], [ts, 'bundler-esm'], [compat, 'node10-commonjs']]) {
assert.deepEqual(checkConsumer(compiler, mode, source), [], `${compiler.version} ${mode}`)
const invalid = source.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, '')
assert.equal(checkConsumer(compiler, mode, invalid).length, 8, 'Every invalid use must remain invalid')
}
const commonjs = `import dash = require('artplayer-plugin-dash-control'); import legacy = require('artplayer-plugin-dash-control/legacy');
const option: dash.Option = { quality: { getName: level => String(level.height) } }; dash(option); legacy();`
assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', commonjs), [])
})
test('DASH legacy consumer compiles against the actual published declaration and candidate', async () => {
const { release } = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/dash-control-release.json')))
const member = 'package/types/artplayer-plugin-dash-control.d.ts'
const bytes = readMember(await ensureArchive(release), member)
assert.equal(hash(bytes), release.files[member])
const source = fs.readFileSync(path.join(refactorDir, 'fixtures/consumers/dash-control-legacy.ts'), 'utf8')
const directory = fs.mkdtempSync(path.join(refactorDir, '.cache/dash-legacy-types-'))
try {
const filename = path.join(directory, 'consumer.ts')
fs.writeFileSync(path.join(directory, 'legacy.d.ts'), bytes)
fs.writeFileSync(filename, source.replace('from \'artplayer-plugin-dash-control\'', 'from \'./legacy\''))
for (const compiler of [ts, compat]) {
const program = compiler.createProgram([filename], { strict: true, noEmit: true, skipLibCheck: false, types: [], esModuleInterop: true, module: compiler.ModuleKind.CommonJS, moduleResolution: compiler.ModuleResolutionKind.NodeJs, target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'] })
const diagnostics = compiler.getPreEmitDiagnostics(program).map(item => ({ code: item.code, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n') }))
assert.deepEqual(diagnostics, [], `Published / TS ${compiler.version}`)
assert(program.getSourceFiles().some(file => path.resolve(file.fileName) === path.join(directory, 'legacy.d.ts')))
assert.deepEqual(checkConsumer(compiler, 'node10-commonjs', source), [], `Candidate / TS ${compiler.version}`)
}
}
finally {
const resolved = fs.realpathSync(directory)
assert.equal(path.dirname(resolved), fs.realpathSync(path.join(refactorDir, '.cache')))
assert(path.basename(resolved).startsWith('dash-legacy-types-'))
fs.rmSync(resolved, { recursive: true, force: true })
}
})
test('DASH editor declarations are reproducible, standalone, precise and compatible with old TS', async () => {
const source = fs.readFileSync('packages/artplayer-plugin-dash-control/types/artplayer-plugin-dash-control.d.ts', 'utf8')
const generated = generatePluginEditorDeclaration(source, 'artplayerPluginDashControl')
const file = 'docs/assets/ts/artplayer-plugin-dash-control.d.ts'
const [formatted] = await new ESLint({ fix: true, fixTypes: ['layout'] }).lintText(generated, { filePath: file })
assert.equal(formatted.errorCount, 0)
assert.equal(fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n'), formatted.output || generated, 'Run yarn build:ts')
const core = fs.readFileSync('docs/assets/ts/artplayer.d.ts', 'utf8')
const consumer = `
const option: artplayerPluginDashControl.Option = { quality: { getName: level => String(level.height) }, audio: { getName: track => track.lang || String(track.id) } }
const oldQuality: Parameters<typeof artplayerPluginDashControl>[0]['quality'] = { getName: (level: object) => String(level) }
const factory: (art: Artplayer) => artplayerPluginDashControl.Result = artplayerPluginDashControl(option)
artplayerPluginDashControl()
artplayerPluginDashControl(undefined)
// @ts-expect-error A formatter must return a string.
artplayerPluginDashControl({ quality: { getName: level => level.height } })
// @ts-expect-error SDK fields cannot become any.
artplayerPluginDashControl({ audio: { getName: track => track.unknownProperty } })
// @ts-expect-error Private definition namespace must not leak globally.
const hidden: artplayerPluginDashControlDefinitions.Option = {}
void [oldQuality, factory, hidden]
`
for (const compiler of [ts, compat]) {
assert.deepEqual(checkPluginEditorDeclaration(generated, core, consumer, compiler), [], compiler.version)
assert.equal(checkPluginEditorDeclaration(generated, core, consumer.replaceAll(/\/\/ @ts-expect-error[^\n]*\n/g, ''), compiler).length, 3)
}
})
+6 -2
View File
@@ -2037,11 +2037,15 @@
"ENG-06",
"CORE-07"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "dash.js adapter、selector 和回调类型",
"acceptance": "严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容",
"evidence": []
"evidence": [
"changes/2026-09-12-PKG-DASH-04-types.md",
"baselines/dash-types-validation.json",
"dash-validation.md"
]
},
{
"id": "PKG-DASH-05",
+3 -2
View File
@@ -1042,13 +1042,14 @@
"riskId": "SDK-02",
"owners": [
"PKG-DASH-01",
"PKG-DASH-05",
"EX-03"
],
"sourceFiles": [
"packages/artplayer-plugin-dash-control/src/index.js",
"packages/artplayer-plugin-dash-control/src/index.ts",
"docs/assets/example/dash.control.js"
],
"contract": "Caller owns art.dash. Published plugin 1.1.0 uses dash.js 4.x (demo 4.5.2); workspace uses 5.x (demo 5.2.1). Preserve both through capability adapters and v5 stable representation IDs; actual SDK playback remains PKG-DASH-05.",
"contract": "Caller owns art.dash. Candidate adapts the released 4.x quality method surface and workspace 5.x stable representation ID surface. Controlled contracts and DOM validation pass; actual pinned 4.5.2/5.2.1 SDK media remains PKG-DASH-05.",
"validationStatus": "Source integration observed; complete real SDK/service/device matrix not yet verified.",
"demoPathsSource": "refactor/baselines/demo-inventory.json"
},
+1 -1
View File
@@ -43,7 +43,7 @@ console.log(`✨ Built ${artplayerTSoutput}`);
const type = pluginsTS[index]
const { name, file } = parsePluginInfo(type)
const source = String(fs.readFileSync(type))
const semanticPlugin = ['artplayerPluginHlsControl', 'artplayerPluginAudioTrack'].includes(name)
const semanticPlugin = ['artplayerPluginHlsControl', 'artplayerPluginAudioTrack', 'artplayerPluginDashControl'].includes(name)
const code = semanticPlugin
? generatePluginEditorDeclaration(source, name)
: `${source.replace(reg, '')}\nexport = ${name};\nexport as namespace ${name};\n`
+1 -1
View File
@@ -73,7 +73,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/playback-public.ts', 'test/types/chapter-options.ts', 'test/types/chapter-exports.ts', 'test/types/hls-control.ts', 'test/types/audio-track.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/hls-control.ts', 'test/types/dash-control.ts', 'refactor/fixtures/consumers/dash-control-legacy.ts', 'test/types/audio-track.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')
+67
View File
@@ -0,0 +1,67 @@
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { compilePackage } from '../helpers/load.js'
import { expect, test } from './fixtures.js'
test('Monaco consumes DASH global types and executes the emitted plugin factory', async ({ page }, testInfo) => {
const code = process.env.ARTPLAYER_DASH_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_DASH_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-dash-control', 'umd')
const declarations = Object.fromEntries(['artplayer', 'artplayer-plugin-dash-control'].map(name => [name, hash(fs.readFileSync(new URL(`../../docs/assets/ts/${name}.d.ts`, import.meta.url)))]))
const inputs = { candidate: process.env.ARTPLAYER_DASH_ARTIFACT || 'workspace source build', sha256: hash(code), declarations }
await testInfo.attach('dash-editor-inputs', { contentType: 'application/json', body: JSON.stringify(inputs) })
await page.goto('/test/player.html?core=candidate&chapter=published')
await page.addScriptTag({ content: code })
await page.addScriptTag({ url: '/assets/js/vs/loader.js' })
const result = await page.evaluate(async () => {
window.require.config({ paths: { vs: '/assets/js/vs' } })
await new Promise((resolve, reject) => window.require(['vs/editor/editor.main'], resolve, reject))
const api = window.monaco
const types = api.languages.typescript
types.typescriptDefaults.setCompilerOptions({ target: types.ScriptTarget.ES2020, strict: true, skipLibCheck: false, noEmit: false, types: [], allowNonTsExtensions: true })
const names = ['artplayer', 'artplayer-plugin-dash-control']
const libraries = await Promise.all(names.map(async (name) => {
const uri = `file:///${name}.d.ts`
const source = await (await fetch(`/assets/ts/${name}.d.ts`)).text()
return { uri, handle: types.typescriptDefaults.addExtraLib(source, uri) }
}))
const model = api.editor.createModel(`
const dashOption: artplayerPluginDashControl.Option = {
quality: { getName: level => String(level.height) },
audio: { getName: track => track.lang || String(track.id) },
}
const oldQuality: NonNullable<Parameters<typeof artplayerPluginDashControl>[0]['quality']> = { getName: (value: object) => String(value) }
const typedFactory: (art: Artplayer) => artplayerPluginDashControl.Result = artplayerPluginDashControl(dashOption)
const defaultFactory = artplayerPluginDashControl()
`, 'typescript', api.Uri.parse('file:///dash-consumer.ts'))
const invalid = api.editor.createModel('artplayerPluginDashControl({ quality: { getName: level => level.height } })', 'typescript', api.Uri.parse('file:///dash-invalid.ts'))
try {
const worker = await (await types.getTypeScriptWorker())(model.uri, invalid.uri)
const syntax = await worker.getSyntacticDiagnostics(model.uri.toString())
const semantic = await worker.getSemanticDiagnostics(model.uri.toString())
const declarations = (await Promise.all(libraries.map(lib => worker.getSemanticDiagnostics(lib.uri)))).flat()
const bad = await worker.getSemanticDiagnostics(invalid.uri.toString())
if (syntax.length || semantic.length || declarations.length)
return { syntax, semantic, declarations, invalid: bad.map(item => item.code), executed: false }
const output = await worker.getEmitOutput(model.uri.toString())
const script = output.outputFiles.find(file => file.name.endsWith('.js'))
if (!script)
throw new Error('Monaco did not emit the DASH fixture')
// eslint-disable-next-line no-new-func -- Exercise the local editor Run action with its real emitted code.
const executed = new Function(`${script.text}\nreturn typeof typedFactory === 'function' && typeof defaultFactory === 'function'`)()
return { syntax, semantic, declarations, invalid: bad.map(item => item.code), executed }
}
finally {
model.dispose()
invalid.dispose()
for (const library of libraries)
library.handle.dispose()
}
})
expect(result.syntax).toEqual([])
expect(result.semantic).toEqual([])
expect(result.declarations).toEqual([])
expect(result.invalid).toEqual([2769])
expect(result.executed).toBe(true)
})
+43
View File
@@ -0,0 +1,43 @@
import type { AudioTrack, Config, Option, QualityLevel, Result } from 'artplayer-plugin-dash-control'
import Artplayer from 'artplayer'
import dashControl from 'artplayer-plugin-dash-control'
import legacy from 'artplayer-plugin-dash-control/legacy'
const option: Option = {
quality: { control: true, getName: level => `${level.height}p` },
audio: { setting: true, getName: track => track.lang || String(track.id) },
}
const art = new Artplayer({ container: '#player', url: 'video.mpd', plugins: [dashControl(), dashControl(option), legacy()] })
const result: Result = dashControl(option)(art)
type HistoricalOption = Parameters<typeof dashControl>[0]
const oldQuality: HistoricalOption['quality'] = { getName: (item: object) => String(item) }
dashControl({ quality: oldQuality })
dashControl(undefined)
const done: void = result.update()
const objectConfig: Config = { getName: (item: object) => String(item) }
dashControl({ quality: objectConfig, audio: objectConfig })
const level: QualityLevel = { height: 720, id: 0 }
const track: AudioTrack = { id: 'en', lang: 'en' }
interface CustomLevel { height: number, bandwidth: number }
interface CustomTrack { id: number, lang: string, roles: string[] }
dashControl({ quality: { getName: (value: CustomLevel) => String(value.bandwidth) }, audio: { getName: (value: CustomTrack) => value.roles.join(',') } })
dashControl<CustomLevel, CustomTrack>({ audio: { getName: track => track.lang } })
// @ts-expect-error Update remains synchronous.
const promise: Promise<void> = result.update()
// @ts-expect-error Update has no options parameter.
result.update({})
// @ts-expect-error Plugin name is the original literal.
const invalidName: Result['name'] = 'dash'
// @ts-expect-error DASH formatters receive only one argument.
dashControl({ quality: { getName: (value: QualityLevel, index: number) => String(index) } })
// @ts-expect-error Custom formatters must return a string.
dashControl({ audio: { getName: () => 123 } })
// @ts-expect-error Visibility options stay boolean.
legacy({ quality: { control: 'yes' } })
// @ts-expect-error Null was never a valid options object.
dashControl(null)
// @ts-expect-error Default SDK fields cannot silently become any.
dashControl({ quality: { getName: level => level.unknownProperty } })
void [done, level, track, promise, invalidName]
+9
View File
@@ -0,0 +1,9 @@
import type PublicFactory from 'artplayer-plugin-dash-control'
import factory from '../../packages/artplayer-plugin-dash-control/src/index'
const compatible: typeof PublicFactory = factory
const implementation: typeof factory = compatible
implementation({ quality: { getName: level => `${level.height}p` }, audio: { getName: track => track.lang || String(track.id) } })
compatible()
// @ts-expect-error Source and public declaration reject invalid formatter results.
factory({ quality: { getName: () => 1 } })