refactor(hls): [PKG-HLS-04] migrate modules and compatible types

This commit is contained in:
Harvey Zhao committed 2026-09-12 00:49:50 +08:00
1 parent ba93b98be5
commit 2f9bee4ee0
34 files changed
+4860 -88

No files matched your search

+39 -13
View File
@@ -1,17 +1,43 @@
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 artplayerPluginHlsControlDefinitions {
export interface QualityLevel {
height: number
name?: string
}
export interface AudioTrack {
id: number
name: string
lang?: string
language?: string
}
export interface Config<Item extends object = object> {
control?: boolean
setting?: boolean
title?: string
auto?: string
/** Plain callback; current-label calls omit index. SDK objects retain their identity. */
getName?: (item: Item, index?: number) => string
}
export interface Option<Level extends object = QualityLevel, Track extends object = AudioTrack> {
quality?: Config<Level>
audio?: Config<Track>
}
export interface Result {
name: 'artplayerPluginHlsControl'
update: () => void
}
export function artplayerPluginHlsControl<Level extends object = QualityLevel, Track extends object = AudioTrack>(option?: Option<Level, Track>): (art: Artplayer) => Result
// Keep the required last signature for historical Parameters<typeof factory>[0] consumers.
export function artplayerPluginHlsControl<Level extends object = QualityLevel, Track extends object = AudioTrack>(option: Option<Level, Track>): (art: Artplayer) => Result
}
declare const artplayerPluginHlsControl: (option: { quality?: Config, audio?: Config }) => (art: Artplayer) => {
name: 'artplayerPluginHlsControl'
update: () => void
declare const artplayerPluginHlsControl: typeof artplayerPluginHlsControlDefinitions.artplayerPluginHlsControl
declare namespace artplayerPluginHlsControl {
export type QualityLevel = artplayerPluginHlsControlDefinitions.QualityLevel
export type AudioTrack = artplayerPluginHlsControlDefinitions.AudioTrack
export type Config<Item extends object = object> = artplayerPluginHlsControlDefinitions.Config<Item>
export type Option<Level extends object = QualityLevel, Track extends object = AudioTrack> = artplayerPluginHlsControlDefinitions.Option<Level, Track>
export type Result = artplayerPluginHlsControlDefinitions.Result
}
export default artplayerPluginHlsControl
export = artplayerPluginHlsControl
export as namespace artplayerPluginHlsControl;
@@ -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.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const o=e.get(t.html);return o?(t.default&&(o.default=!0,o.value=t.value),!1):(e.set(t.html,t),!0)})}function e(t,e,o,n,l,i){let r;const c={control:!1,setting:!1};function s(o){c[o]&&(c[o]=!1,t["control"===o?"controls":"setting"].remove(e))}function u(){r=void 0,s("control"),s("setting")}function a(e,o){for(const t of e.model.selector)t.default=t===o;e.config.control&&t.controls.check(o),e===r&&l(e.hls)&&e.config.setting&&t.setting.check(o)}return{update:function(f,d,h,m){if(!h)return void u();const g=r,v=h.selector.findIndex(t=>t.default);if(!m&&g?.hls===f&&g.model.title===h.title&&(-1!==v||!g.model.selector.some(t=>t.default)&&g.model.html===h.html)&&c.control===Boolean(d.control)&&c.setting===Boolean(d.setting)&&g.model.selector.length===h.selector.length&&g.model.selector.every((t,e)=>t.html===h.selector[e].html&&t.value===h.selector[e].value)){g.config=d;const t=g.model.selector[v];return void(!t||t.default&&g.model.html===h.html||(g.model.html=h.html,a(g,t)))}const p={hls:f,config:d,model:h};r=p;const y=()=>r===p&&l(f),T=e=>i(()=>y()?(f[o]=e.value,y()?(h.html=e.html,t.notice.show=`${h.title}: ${e.html}`,y()&&a(p,e),e.html):e.html):e.html);d.control?(c.control=!0,t.controls.update({name:e,position:"right",html:h.html,style:{padding:"0 10px"},selector:h.selector,onSelect:T})):s("control"),y()&&(d.setting?(c.setting=!0,t.setting.update({name:e,tooltip:h.html,html:h.title,icon:n,width:200,selector:h.selector,onSelect:T})):s("setting"))},clear:u,invalidate:function(){r=void 0}}}return function(o={}){return n=>{const{$video:l}=n.template,{errorHandle:i}=n.constructor.utils;let r,c,s=!1,u=0,a=()=>{},f=0;const d=new WeakSet,h=t=>!s&&!d.has(t)&&n.hls===t&&t.media===l,m=e(n,"hls-quality","currentLevel",'<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>',h,v),g=e(n,"hls-audio","audioTrack",'<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>',h,v);function v(t){f++;let e=!1;try{const o=t();return e=!0,o}finally{if(f--,!f){const t=c;c=void 0,e&&t&&p(t,!1)}}}function p(e,n){if(!h(e))return;const l=++u,i=o.quality||{},r=function(e,o){if(!e.levels.length)return null;const n=o.auto||"Auto",l=o.getName||(t=>t.name||`${t.height}P`),i=function(t){return!0===t.autoLevelEnabled?-1:t.currentLevel}(e),r=e.levels[i],c=r?l(r):n,s=t(e.levels.map((t,e)=>({html:l(t,e),value:e,default:i===e}))).sort((t,e)=>e.value-t.value);return s.push({html:n,value:-1,default:-1===i}),{html:c,title:o.title||"Quality",selector:s}}(e,i);if(!h(e)||l!==u)return;if(m.update(e,i,r,n),!h(e)||l!==u)return;const c=o.audio||{},s=function(e,o){if(!e.audioTracks.length)return null;const n=o.auto||"Auto",l=o.getName||(t=>t.name||t.lang||t.language),i=e.audioTracks[e.audioTrack],r=i?l(i):n,c=t(e.audioTracks.map((t,o)=>({html:l(t,o),value:t.id,default:e.audioTrack===t.id})));return{html:r,title:o.title||"Audio",selector:c}}(e,c);h(e)&&l===u&&g.update(e,c,s,n)}function y(){if(s)return;const t=n.hls;if(i(t?.media===l,'Cannot find instance of HLS from "art.hls"'),!d.has(t)){if(r!==t){a(),r=t;try{a=function(t,e,o){const n=t.constructor?.Events;if(!n||"function"!==typeof t.on||"function"!==typeof t.off)return()=>{};const l=[];let i=!0;function r(){if(i){i=!1;for(const[e,o]of l)t.off(e,o);l.length=0}}try{for(const r of["MANIFEST_PARSED","LEVELS_UPDATED","LEVEL_SWITCHED","AUDIO_TRACKS_UPDATED","AUDIO_TRACK_SWITCHED","DESTROYING"]){const c=n[r];if("string"!==typeof c||l.some(([t])=>t===c))continue;const s=()=>{i&&("DESTROYING"===r?o():e())};l.push([c,s]),t.on(c,s)}}catch(c){throw r(),c}return r}(t,()=>{f?c=t:p(t,!1)},()=>{d.add(t),a(),r===t&&(r=void 0,s||(m.clear(),g.clear()))})}catch(e){throw r=void 0,e}}p(t,!0)}}return n.on("ready",y),n.on("restart",y),n.on("destroy",function t(){s||(s=!0,u++,c=void 0,m.invalidate(),g.invalidate(),a(),n.off("ready",Line truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const o=e.get(t.html);return o?(t.default&&(o.default=!0,o.value=t.value),!1):(e.set(t.html,t),!0)})}function e(t,e,o,n,l,i){let r;const c={control:!1,setting:!1};function s(o){c[o]&&(c[o]=!1,t["control"===o?"controls":"setting"].remove(e))}function u(){r=void 0,s("control"),s("setting")}function a(e,o){for(const t of e.model.selector)t.default=t===o;e.config.control&&t.controls.check(o),e===r&&l(e.hls)&&e.config.setting&&t.setting.check(o)}return{update:function(f,d,h,m){if(!h)return void u();const g=r,v=h.selector.findIndex(t=>t.default);if(!m&&g?.hls===f&&g.model.title===h.title&&(-1!==v||!g.model.selector.some(t=>t.default)&&g.model.html===h.html)&&c.control===Boolean(d.control)&&c.setting===Boolean(d.setting)&&g.model.selector.length===h.selector.length&&g.model.selector.every((t,e)=>t.html===h.selector[e].html&&t.value===h.selector[e].value)){g.config=d;const t=g.model.selector[v];return void(!t||t.default&&g.model.html===h.html||(g.model.html=h.html,a(g,t)))}const p={hls:f,config:d,model:h};r=p;const y=()=>r===p&&l(f),T=e=>i(()=>y()?(f[o]=e.value,y()?(h.html=e.html,t.notice.show=`${h.title}: ${e.html}`,y()&&a(p,e),e.html):e.html):e.html);d.control?(c.control=!0,t.controls.update({name:e,position:"right",html:h.html,style:{padding:"0 10px"},selector:h.selector,onSelect:T})):s("control"),y()&&(d.setting?(c.setting=!0,t.setting.update({name:e,tooltip:h.html,html:h.title,icon:n,width:200,selector:h.selector,onSelect:T})):s("setting"))},clear:u,invalidate:function(){r=void 0}}}return function(o={}){return n=>{const l=n,{$video:i}=l.template,{errorHandle:r}=l.constructor.utils;let c,s,u=!1,a=0,f=()=>{},d=0;const h=new WeakSet,m=t=>!u&&!h.has(t)&&l.hls===t&&t.media===i,g=e(l,"hls-quality","currentLevel",'<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>',m,p),v=e(l,"hls-audio","audioTrack",'<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>',m,p);function p(t){d++;let e=!1;try{const o=t();return e=!0,o}finally{if(d--,!d){const t=s;s=void 0,e&&t&&y(t,!1)}}}function y(e,n){if(!m(e))return;const l=++a,i=o.quality||{},r=function(e,o){if(!e.levels.length)return null;const n=o.auto||"Auto",l=o.getName||(t=>t.name||`${t.height}P`),i=function(t){return!0===t.autoLevelEnabled?-1:t.currentLevel}(e),r=e.levels[i],c=r?l(r):n,s=t(e.levels.map((t,e)=>({html:l(t,e),value:e,default:i===e}))).sort((t,e)=>e.value-t.value);return s.push({html:n,value:-1,default:-1===i}),{html:c,title:o.title||"Quality",selector:s}}(e,i);if(!m(e)||l!==a)return;if(g.update(e,i,r,n),!m(e)||l!==a)return;const c=o.audio||{},s=function(e,o){if(!e.audioTracks.length)return null;const n=o.auto||"Auto",l=o.getName||(t=>t.name||t.lang||t.language),i=e.audioTracks[e.audioTrack],r=i?l(i):n,c=t(e.audioTracks.map((t,o)=>({html:l(t,o),value:t.id,default:e.audioTrack===t.id})));return{html:r,title:o.title||"Audio",selector:c}}(e,c);m(e)&&l===a&&v.update(e,c,s,n)}function T(){if(u)return;const t=l.hls;if(r(t?.media===i,'Cannot find instance of HLS from "art.hls"'),!h.has(t)){if(c!==t){f(),c=t;try{f=function(t,e,o){const n=t.constructor?.Events;if(!n||"function"!==typeof t.on||"function"!==typeof t.off)return()=>{};const l=t,i=[];let r=!0;function c(){if(r){r=!1;for(const[t,e]of i)l.off(t,e);i.length=0}}try{for(const t of["MANIFEST_PARSED","LEVELS_UPDATED","LEVEL_SWITCHED","AUDIO_TRACKS_UPDATED","AUDIO_TRACK_SWITCHED","DESTROYING"]){const c=n[t];if("string"!==typeof c||i.some(([t])=>t===c))continue;const s=()=>{r&&("DESTROYING"===t?o():e())};i.push([c,s]),l.on(c,s)}}catch(s){throw c(),s}return c}(t,()=>{d?s=t:y(t,!1)},()=>{h.add(t),f(),c===t&&(c=void 0,u||(g.clear(),v.clear()))})}catch(e){throw c=void 0,e}}y(t,!0)}}return l.on("ready",T),l.on("restart",T),l.on("destroy",function t(){u||(u=!0,a++,s=void 0,g.invalidate(),v.invalidate(),f(),l.offLine 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.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const o=e.get(t.html);return o?(t.default&&(o.default=!0,o.value=t.value),!1):(e.set(t.html,t),!0)})}function e(t,e,o,l,n,i){let r;const c={control:!1,setting:!1};function s(o){c[o]&&(c[o]=!1,t["control"===o?"controls":"setting"].remove(e))}function u(){r=void 0,s("control"),s("setting")}function a(e,o){for(const t of e.model.selector)t.default=t===o;e.config.control&&t.controls.check(o),e===r&&n(e.hls)&&e.config.setting&&t.setting.check(o)}return{update:function(f,d,h,m){if(!h)return void u();const g=r,v=h.selector.findIndex(t=>t.default);if(!m&&(null==g?void 0:g.hls)===f&&g.model.title===h.title&&(-1!==v||!g.model.selector.some(t=>t.default)&&g.model.html===h.html)&&c.control===Boolean(d.control)&&c.setting===Boolean(d.setting)&&g.model.selector.length===h.selector.length&&g.model.selector.every((t,e)=>t.html===h.selector[e].html&&t.value===h.selector[e].value)){g.config=d;const t=g.model.selector[v];return void(!t||t.default&&g.model.html===h.html||(g.model.html=h.html,a(g,t)))}const p={hls:f,config:d,model:h};r=p;const y=()=>r===p&&n(f),T=e=>i(()=>y()?(f[o]=e.value,y()?(h.html=e.html,t.notice.show=`${h.title}: ${e.html}`,y()&&a(p,e),e.html):e.html):e.html);d.control?(c.control=!0,t.controls.update({name:e,position:"right",html:h.html,style:{padding:"0 10px"},selector:h.selector,onSelect:T})):s("control"),y()&&(d.setting?(c.setting=!0,t.setting.update({name:e,tooltip:h.html,html:h.title,icon:l,width:200,selector:h.selector,onSelect:T})):s("setting"))},clear:u,invalidate:function(){r=void 0}}}return function(o={}){return l=>{const{$video:n}=l.template,{errorHandle:i}=l.constructor.utils;let r,c,s=!1,u=0,a=()=>{},f=0;const d=new WeakSet,h=t=>!s&&!d.has(t)&&l.hls===t&&t.media===n,m=e(l,"hls-quality","currentLevel",'<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>',h,v),g=e(l,"hls-audio","audioTrack",'<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>',h,v);function v(t){f++;let e=!1;try{const o=t();return e=!0,o}finally{if(f--,!f){const t=c;c=void 0,e&&t&&p(t,!1)}}}function p(e,l){if(!h(e))return;const n=++u,i=o.quality||{},r=function(e,o){if(!e.levels.length)return null;const l=o.auto||"Auto",n=o.getName||(t=>t.name||`${t.height}P`),i=function(t){return!0===t.autoLevelEnabled?-1:t.currentLevel}(e),r=e.levels[i],c=r?n(r):l,s=t(e.levels.map((t,e)=>({html:n(t,e),value:e,default:i===e}))).sort((t,e)=>e.value-t.value);return s.push({html:l,value:-1,default:-1===i}),{html:c,title:o.title||"Quality",selector:s}}(e,i);if(!h(e)||n!==u)return;if(m.update(e,i,r,l),!h(e)||n!==u)return;const c=o.audio||{},s=function(e,o){if(!e.audioTracks.length)return null;const l=o.auto||"Auto",n=o.getName||(t=>t.name||t.lang||t.language),i=e.audioTracks[e.audioTrack],r=i?n(i):l,c=t(e.audioTracks.map((t,o)=>({html:n(t,o),value:t.id,default:e.audioTrack===t.id})));return{html:r,title:o.title||"Audio",selector:c}}(e,c);h(e)&&n===u&&g.update(e,c,s,l)}function y(){if(s)return;const t=l.hls;if(i((null==t?void 0:t.media)===n,'Cannot find instance of HLS from "art.hls"'),!d.has(t)){if(r!==t){a(),r=t;try{a=function(t,e,o){var l;const n=null==(l=t.constructor)?void 0:l.Events;if(!n||"function"!==typeof t.on||"function"!==typeof t.off)return()=>{};const i=[];let r=!0;function c(){if(r){r=!1;for(const[e,o]of i)t.off(e,o);i.length=0}}try{for(const l of["MANIFEST_PARSED","LEVELS_UPDATED","LEVEL_SWITCHED","AUDIO_TRACKS_UPDATED","AUDIO_TRACK_SWITCHED","DESTROYING"]){const c=n[l];if("string"!==typeof c||i.some(([t])=>t===c))continue;const s=()=>{r&&("DESTROYING"===l?o():e())};i.push([c,s]),t.on(c,s)}}catch(s){throw c(),s}return c}(t,()=>{f?c=t:p(t,!1)},()=>{d.add(t),a(),r===t&&(r=void 0,s||(m.clear(),g.clear()))})}catch(e){throw r=void 0,e}}p(t,!0)}}return l.on("ready",y),l.on("restart",y),l.on("destroy",function t(){s||(s=!0,u++,cLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const o=e.get(t.html);return o?(t.default&&(o.default=!0,o.value=t.value),!1):(e.set(t.html,t),!0)})}function e(t,e,o,l,n,i){let r;const c={control:!1,setting:!1};function s(o){c[o]&&(c[o]=!1,t["control"===o?"controls":"setting"].remove(e))}function u(){r=void 0,s("control"),s("setting")}function a(e,o){for(const t of e.model.selector)t.default=t===o;e.config.control&&t.controls.check(o),e===r&&n(e.hls)&&e.config.setting&&t.setting.check(o)}return{update:function(f,d,h,m){if(!h)return void u();const g=r,v=h.selector.findIndex(t=>t.default);if(!m&&(null==g?void 0:g.hls)===f&&g.model.title===h.title&&(-1!==v||!g.model.selector.some(t=>t.default)&&g.model.html===h.html)&&c.control===Boolean(d.control)&&c.setting===Boolean(d.setting)&&g.model.selector.length===h.selector.length&&g.model.selector.every((t,e)=>t.html===h.selector[e].html&&t.value===h.selector[e].value)){g.config=d;const t=g.model.selector[v];return void(!t||t.default&&g.model.html===h.html||(g.model.html=h.html,a(g,t)))}const p={hls:f,config:d,model:h};r=p;const y=()=>r===p&&n(f),T=e=>i(()=>y()?(f[o]=e.value,y()?(h.html=e.html,t.notice.show=`${h.title}: ${e.html}`,y()&&a(p,e),e.html):e.html):e.html);d.control?(c.control=!0,t.controls.update({name:e,position:"right",html:h.html,style:{padding:"0 10px"},selector:h.selector,onSelect:T})):s("control"),y()&&(d.setting?(c.setting=!0,t.setting.update({name:e,tooltip:h.html,html:h.title,icon:l,width:200,selector:h.selector,onSelect:T})):s("setting"))},clear:u,invalidate:function(){r=void 0}}}return function(o={}){return l=>{const n=l,{$video:i}=n.template,{errorHandle:r}=n.constructor.utils;let c,s,u=!1,a=0,f=()=>{},d=0;const h=new WeakSet,m=t=>!u&&!h.has(t)&&n.hls===t&&t.media===i,g=e(n,"hls-quality","currentLevel",'<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>',m,p),v=e(n,"hls-audio","audioTrack",'<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>',m,p);function p(t){d++;let e=!1;try{const o=t();return e=!0,o}finally{if(d--,!d){const t=s;s=void 0,e&&t&&y(t,!1)}}}function y(e,l){if(!m(e))return;const n=++a,i=o.quality||{},r=function(e,o){if(!e.levels.length)return null;const l=o.auto||"Auto",n=o.getName||(t=>t.name||`${t.height}P`),i=function(t){return!0===t.autoLevelEnabled?-1:t.currentLevel}(e),r=e.levels[i],c=r?n(r):l,s=t(e.levels.map((t,e)=>({html:n(t,e),value:e,default:i===e}))).sort((t,e)=>e.value-t.value);return s.push({html:l,value:-1,default:-1===i}),{html:c,title:o.title||"Quality",selector:s}}(e,i);if(!m(e)||n!==a)return;if(g.update(e,i,r,l),!m(e)||n!==a)return;const c=o.audio||{},s=function(e,o){if(!e.audioTracks.length)return null;const l=o.auto||"Auto",n=o.getName||(t=>t.name||t.lang||t.language),i=e.audioTracks[e.audioTrack],r=i?n(i):l,c=t(e.audioTracks.map((t,o)=>({html:n(t,o),value:t.id,default:e.audioTrack===t.id})));return{html:r,title:o.title||"Audio",selector:c}}(e,c);m(e)&&n===a&&v.update(e,c,s,l)}function T(){if(u)return;const t=n.hls;if(r((null==t?void 0:t.media)===i,'Cannot find instance of HLS from "art.hls"'),!h.has(t)){if(c!==t){f(),c=t;try{f=function(t,e,o){var l;const n=null==(l=t.constructor)?void 0:l.Events;if(!n||"function"!==typeof t.on||"function"!==typeof t.off)return()=>{};const i=t,r=[];let c=!0;function s(){if(c){c=!1;for(const[t,e]of r)i.off(t,e);r.length=0}}try{for(const t of["MANIFEST_PARSED","LEVELS_UPDATED","LEVEL_SWITCHED","AUDIO_TRACKS_UPDATED","AUDIO_TRACK_SWITCHED","DESTROYING"]){const l=n[t];if("string"!==typeof l||r.some(([t])=>t===l))continue;const s=()=>{c&&("DESTROYING"===t?o():e())};r.push([l,s]),i.on(l,s)}}catch(u){throw s(),u}return s}(t,()=>{d?s=t:y(t,!1)},()=>{h.add(t),f(),c===t&&(c=void 0,u||(g.clear(),v.clear()))})}catch(e){throw c=void 0,e}}y(t,!0)}}return n.on("ready",T),n.on("restart",T),n.on("destroy",function t(){u||(uLine truncated
@@ -129,6 +129,7 @@ function subscribeHls(hls, refresh, destroyed) {
if (!events || typeof hls.on !== "function" || typeof hls.off !== "function")
return () => {
};
const source = hls;
const subscriptions = [];
let active = true;
function release() {
@@ -136,7 +137,7 @@ function subscribeHls(hls, refresh, destroyed) {
return;
active = false;
for (const [event, callback] of subscriptions)
hls.off(event, callback);
source.off(event, callback);
subscriptions.length = 0;
}
try {
@@ -153,7 +154,7 @@ function subscribeHls(hls, refresh, destroyed) {
refresh();
};
subscriptions.push([event, callback]);
hls.on(event, callback);
source.on(event, callback);
}
} catch (error) {
release();
@@ -162,7 +163,8 @@ function subscribeHls(hls, refresh, destroyed) {
return release;
}
function artplayerPluginHlsControl(option = {}) {
return (art) => {
return (player) => {
const art = player;
const { $video } = art.template;
const { errorHandle } = art.constructor.utils;
let closed = false;
@@ -1,21 +1,48 @@
# HLS control maintenance
The public entry remains `src/index.js`: `artplayerPluginHlsControl(option = {})` returns a
The source entry is `src/index.ts`: `artplayerPluginHlsControl(option = {})` returns a
synchronous ArtPlayer plugin with `{ name: 'artplayerPluginHlsControl', update }`. `update()`
returns undefined. The consumer creates, attaches and destroys `art.hls`; this plugin does not
import Hls.js or own its media engine. Public declarations are still in `types/` pending the
separate TypeScript migration (PKG-HLS-04).
import Hls.js or own its media engine. Public declarations are authored in `types/`; implementation
imports their configuration/result types so there is one public contract. All five self-owned
modules are strict TypeScript, with unchecked indexed access enabled and no emitted compiler files.
| Module | Responsibility |
| --- | --- |
| `src/index.js` | Core ready/restart/destroy hooks, current engine identity, refresh generation, selection event barrier |
| `src/mapping.js` | Quality/audio labels, duplicate groups, selected item and Auto policy; no ArtPlayer/DOM dependencies |
| `src/menu.js` | Existing controls/setting update/remove/check APIs, current callback ownership and stable menu reuse |
| `src/sdk-events.js` | Optional SDK event capabilities, six subscriptions, setup rollback and listener release |
| `src/index.ts` | Core ready/restart/destroy hooks, current engine identity, refresh generation, selection event barrier |
| `src/mapping.ts` | Quality/audio labels, duplicate groups, selected item and Auto policy; no ArtPlayer/DOM dependencies |
| `src/menu.ts` | Existing controls/setting update/remove/check APIs, current callback ownership and stable menu reuse |
| `src/sdk-events.ts` | Optional SDK event capabilities, six subscriptions, setup rollback and listener release |
| `src/types.ts` | Internal SDK capabilities, menu models and the narrow host surface; no runtime code |
The entry depends on these three modules; they do not import the entry or each other.
SVG icons remain local build inputs. There is no cross-instance cache, timer, worker or fetch.
## Public types and editor generation
`types/artplayer-plugin-hls-control.d.ts` retains the historical path and TS 4.3.5 syntax.
Conditional `.d.cts`/`.d.mts` bridges preserve callable CommonJS/default ESM imports and named types;
legacy keeps the same JavaScript file and adds a `typesVersions` fallback for older resolution.
Do not change these runtime paths or add a runtime dependency on the Hls SDK to obtain its types.
Default `QualityLevel`/`AudioTrack` fields provide useful formatter inference. `Option<Level, Track>`
also accepts caller-supplied SDK types, including inference from an annotated callback. A formatter
still accepts the original object and optional index, returns a string and runs as a plain callback.
The optional factory overload permits omitted/undefined configuration. The required last overload
deliberately preserves old `Parameters<typeof artplayerPluginHlsControl>[0]['quality']` consumers;
do not remove it as redundant. `update()` stays synchronous and returns void.
The single ArtPlayer-to-Host assertion represents the externally attached SDK surface. `update`
still uses the historical errorHandle/media identity guard. The SDK subscription assertion follows
actual on/off function checks; guarded selector indices follow an equal-length check. These limited
boundaries are not proof that arbitrary external objects implement Hls. Keep runtime contract tests.
`scripts/plugin-editor-types.mjs` generates the HLS Monaco declaration from the authored public file.
It creates private definitions plus a callable global/named-type bridge, rejecting unsupported
imports/exports. `yarn build:ts` semantically checks that output with both compilers before writing it;
never hand-edit `docs/assets/ts/artplayer-plugin-hls-control.d.ts`. The generic defaults and both
overloads must survive generation. Other unconverted plugins still use their existing generator path.
## Refresh and selection
Registration does not require an Hls instance yet. The first ready/restart/manual update checks
@@ -65,8 +92,11 @@ Use the repository's pinned Node and Yarn:
```sh
node --test test/hls-control.test.js
node --test refactor/scripts/hls-types.test.mjs
yarn test:browser hls-control.spec.js
yarn test:browser hls-editor-types.spec.js
yarn build artplayer-plugin-hls-control
yarn build:ts
yarn ci:check
```
@@ -79,3 +109,8 @@ Windows Playwright WebKit lacks MSE; explicit capability/error cleanup runs ther
cases are marked skipped. Safari/native HLS, SDK workers, grouped track changes, SDK version range,
updated example and isolated package verification remain PKG-HLS-05/06 work. See
[refactor validation](../../refactor/hls-validation.md). Keep these gaps visible in release reviews.
Actual Hls.js 1.5.17 declaration consumers pass TS 5.9.3. TS 4.3.5 reports two existing SDK DOM type
errors (MediaDecodingConfiguration and MediaCapabilitiesDecodingInfo), reproduced with the SDK alone.
The plugin introduces no additional errors; do not hide those SDK diagnostics with skipLibCheck or
claim every SDK/compiler combination passes. Standalone plugin consumers pass both compilers.
@@ -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.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const o=e.get(t.html);return o?(t.default&&(o.default=!0,o.value=t.value),!1):(e.set(t.html,t),!0)})}function e(t,e,o,n,l,i){let r;const c={control:!1,setting:!1};function s(o){c[o]&&(c[o]=!1,t["control"===o?"controls":"setting"].remove(e))}function u(){r=void 0,s("control"),s("setting")}function a(e,o){for(const t of e.model.selector)t.default=t===o;e.config.control&&t.controls.check(o),e===r&&l(e.hls)&&e.config.setting&&t.setting.check(o)}return{update:function(f,d,h,m){if(!h)return void u();const g=r,v=h.selector.findIndex(t=>t.default);if(!m&&g?.hls===f&&g.model.title===h.title&&(-1!==v||!g.model.selector.some(t=>t.default)&&g.model.html===h.html)&&c.control===Boolean(d.control)&&c.setting===Boolean(d.setting)&&g.model.selector.length===h.selector.length&&g.model.selector.every((t,e)=>t.html===h.selector[e].html&&t.value===h.selector[e].value)){g.config=d;const t=g.model.selector[v];return void(!t||t.default&&g.model.html===h.html||(g.model.html=h.html,a(g,t)))}const p={hls:f,config:d,model:h};r=p;const y=()=>r===p&&l(f),T=e=>i(()=>y()?(f[o]=e.value,y()?(h.html=e.html,t.notice.show=`${h.title}: ${e.html}`,y()&&a(p,e),e.html):e.html):e.html);d.control?(c.control=!0,t.controls.update({name:e,position:"right",html:h.html,style:{padding:"0 10px"},selector:h.selector,onSelect:T})):s("control"),y()&&(d.setting?(c.setting=!0,t.setting.update({name:e,tooltip:h.html,html:h.title,icon:n,width:200,selector:h.selector,onSelect:T})):s("setting"))},clear:u,invalidate:function(){r=void 0}}}return function(o={}){return n=>{const{$video:l}=n.template,{errorHandle:i}=n.constructor.utils;let r,c,s=!1,u=0,a=()=>{},f=0;const d=new WeakSet,h=t=>!s&&!d.has(t)&&n.hls===t&&t.media===l,m=e(n,"hls-quality","currentLevel",'<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>',h,v),g=e(n,"hls-audio","audioTrack",'<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>',h,v);function v(t){f++;let e=!1;try{const o=t();return e=!0,o}finally{if(f--,!f){const t=c;c=void 0,e&&t&&p(t,!1)}}}function p(e,n){if(!h(e))return;const l=++u,i=o.quality||{},r=function(e,o){if(!e.levels.length)return null;const n=o.auto||"Auto",l=o.getName||(t=>t.name||`${t.height}P`),i=function(t){return!0===t.autoLevelEnabled?-1:t.currentLevel}(e),r=e.levels[i],c=r?l(r):n,s=t(e.levels.map((t,e)=>({html:l(t,e),value:e,default:i===e}))).sort((t,e)=>e.value-t.value);return s.push({html:n,value:-1,default:-1===i}),{html:c,title:o.title||"Quality",selector:s}}(e,i);if(!h(e)||l!==u)return;if(m.update(e,i,r,n),!h(e)||l!==u)return;const c=o.audio||{},s=function(e,o){if(!e.audioTracks.length)return null;const n=o.auto||"Auto",l=o.getName||(t=>t.name||t.lang||t.language),i=e.audioTracks[e.audioTrack],r=i?l(i):n,c=t(e.audioTracks.map((t,o)=>({html:l(t,o),value:t.id,default:e.audioTrack===t.id})));return{html:r,title:o.title||"Audio",selector:c}}(e,c);h(e)&&l===u&&g.update(e,c,s,n)}function y(){if(s)return;const t=n.hls;if(i(t?.media===l,'Cannot find instance of HLS from "art.hls"'),!d.has(t)){if(r!==t){a(),r=t;try{a=function(t,e,o){const n=t.constructor?.Events;if(!n||"function"!==typeof t.on||"function"!==typeof t.off)return()=>{};const l=[];let i=!0;function r(){if(i){i=!1;for(const[e,o]of l)t.off(e,o);l.length=0}}try{for(const r of["MANIFEST_PARSED","LEVELS_UPDATED","LEVEL_SWITCHED","AUDIO_TRACKS_UPDATED","AUDIO_TRACK_SWITCHED","DESTROYING"]){const c=n[r];if("string"!==typeof c||l.some(([t])=>t===c))continue;const s=()=>{i&&("DESTROYING"===r?o():e())};l.push([c,s]),t.on(c,s)}}catch(c){throw r(),c}return r}(t,()=>{f?c=t:p(t,!1)},()=>{d.add(t),a(),r===t&&(r=void 0,s||(m.clear(),g.clear()))})}catch(e){throw r=void 0,e}}p(t,!0)}}return n.on("ready",y),n.on("restart",y),n.on("destroy",function t(){s||(s=!0,u++,c=void 0,m.invalidate(),g.invalidate(),a(),n.off("ready",Line truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const o=e.get(t.html);return o?(t.default&&(o.default=!0,o.value=t.value),!1):(e.set(t.html,t),!0)})}function e(t,e,o,n,l,i){let r;const c={control:!1,setting:!1};function s(o){c[o]&&(c[o]=!1,t["control"===o?"controls":"setting"].remove(e))}function u(){r=void 0,s("control"),s("setting")}function a(e,o){for(const t of e.model.selector)t.default=t===o;e.config.control&&t.controls.check(o),e===r&&l(e.hls)&&e.config.setting&&t.setting.check(o)}return{update:function(f,d,h,m){if(!h)return void u();const g=r,v=h.selector.findIndex(t=>t.default);if(!m&&g?.hls===f&&g.model.title===h.title&&(-1!==v||!g.model.selector.some(t=>t.default)&&g.model.html===h.html)&&c.control===Boolean(d.control)&&c.setting===Boolean(d.setting)&&g.model.selector.length===h.selector.length&&g.model.selector.every((t,e)=>t.html===h.selector[e].html&&t.value===h.selector[e].value)){g.config=d;const t=g.model.selector[v];return void(!t||t.default&&g.model.html===h.html||(g.model.html=h.html,a(g,t)))}const p={hls:f,config:d,model:h};r=p;const y=()=>r===p&&l(f),T=e=>i(()=>y()?(f[o]=e.value,y()?(h.html=e.html,t.notice.show=`${h.title}: ${e.html}`,y()&&a(p,e),e.html):e.html):e.html);d.control?(c.control=!0,t.controls.update({name:e,position:"right",html:h.html,style:{padding:"0 10px"},selector:h.selector,onSelect:T})):s("control"),y()&&(d.setting?(c.setting=!0,t.setting.update({name:e,tooltip:h.html,html:h.title,icon:n,width:200,selector:h.selector,onSelect:T})):s("setting"))},clear:u,invalidate:function(){r=void 0}}}return function(o={}){return n=>{const l=n,{$video:i}=l.template,{errorHandle:r}=l.constructor.utils;let c,s,u=!1,a=0,f=()=>{},d=0;const h=new WeakSet,m=t=>!u&&!h.has(t)&&l.hls===t&&t.media===i,g=e(l,"hls-quality","currentLevel",'<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>',m,p),v=e(l,"hls-audio","audioTrack",'<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>',m,p);function p(t){d++;let e=!1;try{const o=t();return e=!0,o}finally{if(d--,!d){const t=s;s=void 0,e&&t&&y(t,!1)}}}function y(e,n){if(!m(e))return;const l=++a,i=o.quality||{},r=function(e,o){if(!e.levels.length)return null;const n=o.auto||"Auto",l=o.getName||(t=>t.name||`${t.height}P`),i=function(t){return!0===t.autoLevelEnabled?-1:t.currentLevel}(e),r=e.levels[i],c=r?l(r):n,s=t(e.levels.map((t,e)=>({html:l(t,e),value:e,default:i===e}))).sort((t,e)=>e.value-t.value);return s.push({html:n,value:-1,default:-1===i}),{html:c,title:o.title||"Quality",selector:s}}(e,i);if(!m(e)||l!==a)return;if(g.update(e,i,r,n),!m(e)||l!==a)return;const c=o.audio||{},s=function(e,o){if(!e.audioTracks.length)return null;const n=o.auto||"Auto",l=o.getName||(t=>t.name||t.lang||t.language),i=e.audioTracks[e.audioTrack],r=i?l(i):n,c=t(e.audioTracks.map((t,o)=>({html:l(t,o),value:t.id,default:e.audioTrack===t.id})));return{html:r,title:o.title||"Audio",selector:c}}(e,c);m(e)&&l===a&&v.update(e,c,s,n)}function T(){if(u)return;const t=l.hls;if(r(t?.media===i,'Cannot find instance of HLS from "art.hls"'),!h.has(t)){if(c!==t){f(),c=t;try{f=function(t,e,o){const n=t.constructor?.Events;if(!n||"function"!==typeof t.on||"function"!==typeof t.off)return()=>{};const l=t,i=[];let r=!0;function c(){if(r){r=!1;for(const[t,e]of i)l.off(t,e);i.length=0}}try{for(const t of["MANIFEST_PARSED","LEVELS_UPDATED","LEVEL_SWITCHED","AUDIO_TRACKS_UPDATED","AUDIO_TRACK_SWITCHED","DESTROYING"]){const c=n[t];if("string"!==typeof c||i.some(([t])=>t===c))continue;const s=()=>{r&&("DESTROYING"===t?o():e())};i.push([c,s]),l.on(c,s)}}catch(s){throw c(),s}return c}(t,()=>{d?s=t:y(t,!1)},()=>{h.add(t),f(),c===t&&(c=void 0,u||(g.clear(),v.clear()))})}catch(e){throw c=void 0,e}}y(t,!0)}}return l.on("ready",T),l.on("restart",T),l.on("destroy",function t(){u||(u=!0,a++,s=void 0,g.invalidate(),v.invalidate(),f(),l.offLine 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.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const o=e.get(t.html);return o?(t.default&&(o.default=!0,o.value=t.value),!1):(e.set(t.html,t),!0)})}function e(t,e,o,l,n,i){let r;const c={control:!1,setting:!1};function s(o){c[o]&&(c[o]=!1,t["control"===o?"controls":"setting"].remove(e))}function u(){r=void 0,s("control"),s("setting")}function a(e,o){for(const t of e.model.selector)t.default=t===o;e.config.control&&t.controls.check(o),e===r&&n(e.hls)&&e.config.setting&&t.setting.check(o)}return{update:function(f,d,h,m){if(!h)return void u();const g=r,v=h.selector.findIndex(t=>t.default);if(!m&&(null==g?void 0:g.hls)===f&&g.model.title===h.title&&(-1!==v||!g.model.selector.some(t=>t.default)&&g.model.html===h.html)&&c.control===Boolean(d.control)&&c.setting===Boolean(d.setting)&&g.model.selector.length===h.selector.length&&g.model.selector.every((t,e)=>t.html===h.selector[e].html&&t.value===h.selector[e].value)){g.config=d;const t=g.model.selector[v];return void(!t||t.default&&g.model.html===h.html||(g.model.html=h.html,a(g,t)))}const p={hls:f,config:d,model:h};r=p;const y=()=>r===p&&n(f),T=e=>i(()=>y()?(f[o]=e.value,y()?(h.html=e.html,t.notice.show=`${h.title}: ${e.html}`,y()&&a(p,e),e.html):e.html):e.html);d.control?(c.control=!0,t.controls.update({name:e,position:"right",html:h.html,style:{padding:"0 10px"},selector:h.selector,onSelect:T})):s("control"),y()&&(d.setting?(c.setting=!0,t.setting.update({name:e,tooltip:h.html,html:h.title,icon:l,width:200,selector:h.selector,onSelect:T})):s("setting"))},clear:u,invalidate:function(){r=void 0}}}return function(o={}){return l=>{const{$video:n}=l.template,{errorHandle:i}=l.constructor.utils;let r,c,s=!1,u=0,a=()=>{},f=0;const d=new WeakSet,h=t=>!s&&!d.has(t)&&l.hls===t&&t.media===n,m=e(l,"hls-quality","currentLevel",'<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>',h,v),g=e(l,"hls-audio","audioTrack",'<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>',h,v);function v(t){f++;let e=!1;try{const o=t();return e=!0,o}finally{if(f--,!f){const t=c;c=void 0,e&&t&&p(t,!1)}}}function p(e,l){if(!h(e))return;const n=++u,i=o.quality||{},r=function(e,o){if(!e.levels.length)return null;const l=o.auto||"Auto",n=o.getName||(t=>t.name||`${t.height}P`),i=function(t){return!0===t.autoLevelEnabled?-1:t.currentLevel}(e),r=e.levels[i],c=r?n(r):l,s=t(e.levels.map((t,e)=>({html:n(t,e),value:e,default:i===e}))).sort((t,e)=>e.value-t.value);return s.push({html:l,value:-1,default:-1===i}),{html:c,title:o.title||"Quality",selector:s}}(e,i);if(!h(e)||n!==u)return;if(m.update(e,i,r,l),!h(e)||n!==u)return;const c=o.audio||{},s=function(e,o){if(!e.audioTracks.length)return null;const l=o.auto||"Auto",n=o.getName||(t=>t.name||t.lang||t.language),i=e.audioTracks[e.audioTrack],r=i?n(i):l,c=t(e.audioTracks.map((t,o)=>({html:n(t,o),value:t.id,default:e.audioTrack===t.id})));return{html:r,title:o.title||"Audio",selector:c}}(e,c);h(e)&&n===u&&g.update(e,c,s,l)}function y(){if(s)return;const t=l.hls;if(i((null==t?void 0:t.media)===n,'Cannot find instance of HLS from "art.hls"'),!d.has(t)){if(r!==t){a(),r=t;try{a=function(t,e,o){var l;const n=null==(l=t.constructor)?void 0:l.Events;if(!n||"function"!==typeof t.on||"function"!==typeof t.off)return()=>{};const i=[];let r=!0;function c(){if(r){r=!1;for(const[e,o]of i)t.off(e,o);i.length=0}}try{for(const l of["MANIFEST_PARSED","LEVELS_UPDATED","LEVEL_SWITCHED","AUDIO_TRACKS_UPDATED","AUDIO_TRACK_SWITCHED","DESTROYING"]){const c=n[l];if("string"!==typeof c||i.some(([t])=>t===c))continue;const s=()=>{r&&("DESTROYING"===l?o():e())};i.push([c,s]),t.on(c,s)}}catch(s){throw c(),s}return c}(t,()=>{f?c=t:p(t,!1)},()=>{d.add(t),a(),r===t&&(r=void 0,s||(m.clear(),g.clear()))})}catch(e){throw r=void 0,e}}p(t,!0)}}return l.on("ready",y),l.on("restart",y),l.on("destroy",function t(){s||(s=!0,u++,cLine truncated
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const o=e.get(t.html);return o?(t.default&&(o.default=!0,o.value=t.value),!1):(e.set(t.html,t),!0)})}function e(t,e,o,l,n,i){let r;const c={control:!1,setting:!1};function s(o){c[o]&&(c[o]=!1,t["control"===o?"controls":"setting"].remove(e))}function u(){r=void 0,s("control"),s("setting")}function a(e,o){for(const t of e.model.selector)t.default=t===o;e.config.control&&t.controls.check(o),e===r&&n(e.hls)&&e.config.setting&&t.setting.check(o)}return{update:function(f,d,h,m){if(!h)return void u();const g=r,v=h.selector.findIndex(t=>t.default);if(!m&&(null==g?void 0:g.hls)===f&&g.model.title===h.title&&(-1!==v||!g.model.selector.some(t=>t.default)&&g.model.html===h.html)&&c.control===Boolean(d.control)&&c.setting===Boolean(d.setting)&&g.model.selector.length===h.selector.length&&g.model.selector.every((t,e)=>t.html===h.selector[e].html&&t.value===h.selector[e].value)){g.config=d;const t=g.model.selector[v];return void(!t||t.default&&g.model.html===h.html||(g.model.html=h.html,a(g,t)))}const p={hls:f,config:d,model:h};r=p;const y=()=>r===p&&n(f),T=e=>i(()=>y()?(f[o]=e.value,y()?(h.html=e.html,t.notice.show=`${h.title}: ${e.html}`,y()&&a(p,e),e.html):e.html):e.html);d.control?(c.control=!0,t.controls.update({name:e,position:"right",html:h.html,style:{padding:"0 10px"},selector:h.selector,onSelect:T})):s("control"),y()&&(d.setting?(c.setting=!0,t.setting.update({name:e,tooltip:h.html,html:h.title,icon:l,width:200,selector:h.selector,onSelect:T})):s("setting"))},clear:u,invalidate:function(){r=void 0}}}return function(o={}){return l=>{const n=l,{$video:i}=n.template,{errorHandle:r}=n.constructor.utils;let c,s,u=!1,a=0,f=()=>{},d=0;const h=new WeakSet,m=t=>!u&&!h.has(t)&&n.hls===t&&t.media===i,g=e(n,"hls-quality","currentLevel",'<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>',m,p),v=e(n,"hls-audio","audioTrack",'<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>',m,p);function p(t){d++;let e=!1;try{const o=t();return e=!0,o}finally{if(d--,!d){const t=s;s=void 0,e&&t&&y(t,!1)}}}function y(e,l){if(!m(e))return;const n=++a,i=o.quality||{},r=function(e,o){if(!e.levels.length)return null;const l=o.auto||"Auto",n=o.getName||(t=>t.name||`${t.height}P`),i=function(t){return!0===t.autoLevelEnabled?-1:t.currentLevel}(e),r=e.levels[i],c=r?n(r):l,s=t(e.levels.map((t,e)=>({html:n(t,e),value:e,default:i===e}))).sort((t,e)=>e.value-t.value);return s.push({html:l,value:-1,default:-1===i}),{html:c,title:o.title||"Quality",selector:s}}(e,i);if(!m(e)||n!==a)return;if(g.update(e,i,r,l),!m(e)||n!==a)return;const c=o.audio||{},s=function(e,o){if(!e.audioTracks.length)return null;const l=o.auto||"Auto",n=o.getName||(t=>t.name||t.lang||t.language),i=e.audioTracks[e.audioTrack],r=i?n(i):l,c=t(e.audioTracks.map((t,o)=>({html:n(t,o),value:t.id,default:e.audioTrack===t.id})));return{html:r,title:o.title||"Audio",selector:c}}(e,c);m(e)&&n===a&&v.update(e,c,s,l)}function T(){if(u)return;const t=n.hls;if(r((null==t?void 0:t.media)===i,'Cannot find instance of HLS from "art.hls"'),!h.has(t)){if(c!==t){f(),c=t;try{f=function(t,e,o){var l;const n=null==(l=t.constructor)?void 0:l.Events;if(!n||"function"!==typeof t.on||"function"!==typeof t.off)return()=>{};const i=t,r=[];let c=!0;function s(){if(c){c=!1;for(const[t,e]of r)i.off(t,e);r.length=0}}try{for(const t of["MANIFEST_PARSED","LEVELS_UPDATED","LEVEL_SWITCHED","AUDIO_TRACKS_UPDATED","AUDIO_TRACK_SWITCHED","DESTROYING"]){const l=n[t];if("string"!==typeof l||r.some(([t])=>t===l))continue;const s=()=>{c&&("DESTROYING"===t?o():e())};r.push([l,s]),i.on(l,s)}}catch(u){throw s(),u}return s}(t,()=>{d?s=t:y(t,!1)},()=>{h.add(t),f(),c===t&&(c=void 0,u||(g.clear(),v.clear()))})}catch(e){throw c=void 0,e}}y(t,!0)}}return n.on("ready",T),n.on("restart",T),n.on("destroy",function t(){u||(uLine truncated
@@ -129,6 +129,7 @@ function subscribeHls(hls, refresh, destroyed) {
if (!events || typeof hls.on !== "function" || typeof hls.off !== "function")
return () => {
};
const source = hls;
const subscriptions = [];
let active = true;
function release() {
@@ -136,7 +137,7 @@ function subscribeHls(hls, refresh, destroyed) {
return;
active = false;
for (const [event, callback] of subscriptions)
hls.off(event, callback);
source.off(event, callback);
subscriptions.length = 0;
}
try {
@@ -153,7 +154,7 @@ function subscribeHls(hls, refresh, destroyed) {
refresh();
};
subscriptions.push([event, callback]);
hls.on(event, callback);
source.on(event, callback);
}
} catch (error) {
release();
@@ -162,7 +163,8 @@ function subscribeHls(hls, refresh, destroyed) {
return release;
}
function artplayerPluginHlsControl(option = {}) {
return (art) => {
return (player) => {
const art = player;
const { $video } = art.template;
const { errorHandle } = art.constructor.utils;
let closed = false;
@@ -19,19 +19,36 @@
],
"exports": {
".": {
"types": "./types/artplayer-plugin-hls-control.d.ts",
"import": "./dist/artplayer-plugin-hls-control.mjs",
"require": "./dist/artplayer-plugin-hls-control.js"
"import": {
"types": "./types/artplayer-plugin-hls-control.d.mts",
"default": "./dist/artplayer-plugin-hls-control.mjs"
},
"require": {
"types": "./types/artplayer-plugin-hls-control.d.cts",
"default": "./dist/artplayer-plugin-hls-control.js"
}
},
"./legacy": {
"types": "./types/artplayer-plugin-hls-control.d.ts",
"import": "./dist/artplayer-plugin-hls-control.legacy.js",
"require": "./dist/artplayer-plugin-hls-control.legacy.js"
"import": {
"types": "./types/artplayer-plugin-hls-control.d.cts",
"default": "./dist/artplayer-plugin-hls-control.legacy.js"
},
"require": {
"types": "./types/artplayer-plugin-hls-control.d.cts",
"default": "./dist/artplayer-plugin-hls-control.legacy.js"
}
}
},
"main": "./dist/artplayer-plugin-hls-control.js",
"module": "./dist/artplayer-plugin-hls-control.mjs",
"types": "./types/artplayer-plugin-hls-control.d.ts",
"legacy": "./dist/artplayer-plugin-hls-control.legacy.js",
"browserslist": "last 1 Chrome version"
"browserslist": "last 1 Chrome version",
"typesVersions": {
"*": {
"legacy": [
"types/artplayer-plugin-hls-control.d.ts"
]
}
}
}
@@ -1,25 +1,30 @@
import type Artplayer from 'artplayer'
import type { AudioTrack, Option, QualityLevel, Result } from '../types/artplayer-plugin-hls-control'
import type { Cleanup, Hls, Host } from './types'
import $audio from './audio.svg?raw'
import { audioModel, qualityModel } from './mapping'
import { createMenu } from './menu'
import $quality from './quality.svg?raw'
import { subscribeHls } from './sdk-events'
export default function artplayerPluginHlsControl(option = {}) {
return (art) => {
export default function artplayerPluginHlsControl<Level extends object = QualityLevel, Track extends object = AudioTrack>(option: Option<Level, Track> = {}) {
return (player: Artplayer): Result => {
// The consumer owns the optional SDK; update validates its media identity before use.
const art = player as unknown as Host<Level, Track>
const { $video } = art.template
const { errorHandle } = art.constructor.utils
let closed = false
let revision = 0
let engine
let unsubscribe = () => {}
let engine: Hls<Level, Track> | undefined
let unsubscribe: Cleanup = () => {}
let selecting = 0
let pending
let pending: Hls<Level, Track> | undefined
const retired = new WeakSet()
const active = hls => !closed && !retired.has(hls) && art.hls === hls && hls.media === $video
const active = (hls: Hls<Level, Track>) => !closed && !retired.has(hls) && art.hls === hls && hls.media === $video
const quality = createMenu(art, 'hls-quality', 'currentLevel', $quality, active, select)
const audio = createMenu(art, 'hls-audio', 'audioTrack', $audio, active, select)
function select(callback) {
function select<T>(callback: () => T): T {
selecting++
let succeeded = false
try {
@@ -38,7 +43,7 @@ export default function artplayerPluginHlsControl(option = {}) {
}
}
function refresh(hls, force) {
function refresh(hls: Hls<Level, Track>, force: boolean): void {
if (!active(hls))
return
const version = ++revision
@@ -55,10 +60,10 @@ export default function artplayerPluginHlsControl(option = {}) {
audio.update(hls, audioConfig, audioView, force)
}
function update() {
function update(): void {
if (closed)
return
const hls = art.hls
const hls = art.hls!
errorHandle(hls?.media === $video, 'Cannot find instance of HLS from "art.hls"')
if (retired.has(hls))
return
@@ -91,7 +96,7 @@ export default function artplayerPluginHlsControl(option = {}) {
refresh(hls, true)
}
function destroy() {
function destroy(): void {
if (closed)
return
closed = true
@@ -1,5 +1,8 @@
function uniqueLabels(items) {
const seen = new Map()
import type { Config } from '../types/artplayer-plugin-hls-control'
import type { AudioFields, Hls, LevelFields, MenuModel, SelectorItem } from './types'
function uniqueLabels(items: SelectorItem[]): SelectorItem[] {
const seen = new Map<string, SelectorItem>()
return items.filter((item) => {
if (item.html === undefined)
return true
@@ -16,15 +19,15 @@ function uniqueLabels(items) {
})
}
export function qualitySelection(hls) {
export function qualitySelection(hls: Pick<Hls, 'currentLevel' | 'autoLevelEnabled'>): number {
return hls.autoLevelEnabled === true ? -1 : hls.currentLevel
}
export function qualityModel(hls, config) {
export function qualityModel<Level extends object>(hls: Pick<Hls<Level>, 'levels' | 'currentLevel' | 'autoLevelEnabled'>, config: Config<Level>): MenuModel | null {
if (!hls.levels.length)
return null
const auto = config.auto || 'Auto'
const getName = config.getName || (level => level.name || `${level.height}P`)
const getName = config.getName || ((level: LevelFields) => level.name || `${level.height}P`)
const selected = qualitySelection(hls)
const level = hls.levels[selected]
const html = level ? getName(level) : auto
@@ -34,11 +37,11 @@ export function qualityModel(hls, config) {
return { html, title: config.title || 'Quality', selector }
}
export function audioModel(hls, config) {
export function audioModel<Track extends object>(hls: Pick<Hls<object, Track>, 'audioTracks' | 'audioTrack'>, config: Config<Track>): MenuModel | null {
if (!hls.audioTracks.length)
return null
const auto = config.auto || 'Auto'
const getName = config.getName || (track => track.name || track.lang || track.language)
const getName = config.getName || ((track: AudioFields) => track.name || track.lang || track.language)
const track = hls.audioTracks[hls.audioTrack]
const html = track ? getName(track) : auto
const selector = uniqueLabels(hls.audioTracks.map((item, index) => ({ html: getName(item, index), value: item.id, default: hls.audioTrack === item.id })))
@@ -1,21 +1,23 @@
export function createMenu(art, name, field, icon, active, select) {
let current
import type { DisplayConfig, MenuHost, MenuModel, MenuState, SelectorItem } from './types'
export function createMenu<Engine extends { currentLevel: number, audioTrack: number }>(art: MenuHost, name: string, field: 'currentLevel' | 'audioTrack', icon: string, active: (hls: Engine) => boolean, select: <T>(callback: () => T) => T) {
let current: MenuState<Engine> | undefined
const owned = { control: false, setting: false }
function remove(surface) {
function remove(surface: 'control' | 'setting'): void {
if (!owned[surface])
return
owned[surface] = false
art[surface === 'control' ? 'controls' : 'setting'].remove(name)
}
function clear() {
function clear(): void {
current = undefined
remove('control')
remove('setting')
}
function check(state, item) {
function check(state: MenuState<Engine>, item: SelectorItem): void {
for (const entry of state.model.selector)
entry.default = entry === item
if (state.config.control)
@@ -26,18 +28,19 @@ export function createMenu(art, name, field, icon, active, select) {
art.setting.check(item)
}
function update(hls, config, model, force) {
function update(hls: Engine, config: DisplayConfig, model: MenuModel | null, force: boolean): void {
if (!model) {
clear()
return
}
const previous = current
const selectedIndex = model.selector.findIndex(item => item.default)
// Equal lengths below protect the same-index reads in the comparison.
const reusable = !force && previous?.hls === hls && previous.model.title === model.title
&& (selectedIndex !== -1 || (!previous.model.selector.some(item => item.default) && previous.model.html === model.html))
&& owned.control === Boolean(config.control) && owned.setting === Boolean(config.setting)
&& previous.model.selector.length === model.selector.length
&& previous.model.selector.every((item, index) => item.html === model.selector[index].html && item.value === model.selector[index].value)
&& previous.model.selector.every((item, index) => item.html === model.selector[index]!.html && item.value === model.selector[index]!.value)
if (reusable) {
previous.config = config
const target = previous.model.selector[selectedIndex]
@@ -50,7 +53,7 @@ export function createMenu(art, name, field, icon, active, select) {
const state = { hls, config, model }
current = state
const valid = () => current === state && active(hls)
const onSelect = item => select(() => {
const onSelect = (item: SelectorItem) => select(() => {
if (!valid())
return item.html
hls[field] = item.value
@@ -80,7 +83,7 @@ export function createMenu(art, name, field, icon, active, select) {
}
}
function invalidate() {
function invalidate(): void {
current = undefined
}
return { update, clear, invalidate }
@@ -1,19 +1,23 @@
export function subscribeHls(hls, refresh, destroyed) {
import type { Cleanup, Hls, SdkEvent } from './types'
export function subscribeHls(hls: Hls, refresh: Cleanup, destroyed: Cleanup): Cleanup {
const events = hls.constructor?.Events
if (!events || typeof hls.on !== 'function' || typeof hls.off !== 'function')
return () => {}
const subscriptions = []
// Keep the checked SDK object so method calls retain their original receiver.
const source = hls as Hls & Required<Pick<Hls, 'on' | 'off'>>
const subscriptions: [string, Cleanup][] = []
let active = true
function release() {
function release(): void {
if (!active)
return
active = false
for (const [event, callback] of subscriptions)
hls.off(event, callback)
source.off(event, callback)
subscriptions.length = 0
}
try {
for (const key of ['MANIFEST_PARSED', 'LEVELS_UPDATED', 'LEVEL_SWITCHED', 'AUDIO_TRACKS_UPDATED', 'AUDIO_TRACK_SWITCHED', 'DESTROYING']) {
for (const key of ['MANIFEST_PARSED', 'LEVELS_UPDATED', 'LEVEL_SWITCHED', 'AUDIO_TRACKS_UPDATED', 'AUDIO_TRACK_SWITCHED', 'DESTROYING'] satisfies SdkEvent[]) {
const event = events[key]
if (typeof event !== 'string' || subscriptions.some(([name]) => name === event))
continue
@@ -26,7 +30,7 @@ export function subscribeHls(hls, refresh, destroyed) {
refresh()
}
subscriptions.push([event, callback])
hls.on(event, callback)
source.on(event, callback)
}
}
catch (error) {
@@ -0,0 +1,79 @@
import type { Config } from '../types/artplayer-plugin-hls-control'
export interface LevelFields {
height?: number
name?: string
}
export interface AudioFields {
id: number
name?: string
lang?: string
language?: string
}
export type SdkEvent = 'MANIFEST_PARSED' | 'LEVELS_UPDATED' | 'LEVEL_SWITCHED' | 'AUDIO_TRACKS_UPDATED' | 'AUDIO_TRACK_SWITCHED' | 'DESTROYING'
export type Cleanup = () => void
export interface Hls<Level extends object = object, Track extends object = object> {
media: object | null
levels: (Level & LevelFields)[]
audioTracks: (Track & AudioFields)[]
currentLevel: number
audioTrack: number
autoLevelEnabled?: boolean
constructor?: { Events?: Partial<Record<SdkEvent, unknown>> }
on?: (event: string, callback: Cleanup) => unknown
off?: (event: string, callback: Cleanup) => unknown
}
export interface SelectorItem {
html: string | undefined
value: number
default: boolean
}
export interface MenuModel {
html: string | undefined
title: string
selector: SelectorItem[]
}
export interface MenuOption {
name: string
html: string | undefined
selector: SelectorItem[]
onSelect: (item: SelectorItem) => string | undefined
position?: 'right'
style?: { padding: string }
tooltip?: string
icon?: string
width?: number
}
export interface Registry {
update: (option: MenuOption) => unknown
remove: (name: string) => unknown
check: (item: SelectorItem) => unknown
}
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 } }
hls?: Hls<Level, Track>
on: (event: 'ready' | 'restart' | 'destroy', callback: Cleanup) => unknown
off: (event: 'ready' | 'restart' | 'destroy', callback: Cleanup) => unknown
}
export type DisplayConfig = Pick<Config, 'control' | 'setting'>
export interface MenuState<Engine> {
hls: Engine
config: DisplayConfig
model: MenuModel
}
@@ -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-hls-control.js'
declare const artplayerPluginHlsControl: typeof Definition.default
declare namespace artplayerPluginHlsControl {
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 = artplayerPluginHlsControl
@@ -0,0 +1,2 @@
export { default } from './artplayer-plugin-hls-control.cjs'
export type { AudioTrack, Config, Option, QualityLevel, Result } from './artplayer-plugin-hls-control.js'
@@ -1,16 +1,38 @@
import type Artplayer from 'artplayer'
interface Config {
export interface QualityLevel {
height: number
name?: string
}
export interface AudioTrack {
id: number
name: string
lang?: string
language?: string
}
export interface Config<Item extends object = object> {
control?: boolean
setting?: boolean
title?: string
auto?: string
getName?: (level: object) => string
/** Plain callback; current-label calls omit index. SDK objects retain their identity. */
getName?: (item: Item, index?: number) => string
}
declare const artplayerPluginHlsControl: (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: 'artplayerPluginHlsControl'
update: () => void
}
declare function artplayerPluginHlsControl<Level extends object = QualityLevel, Track extends object = AudioTrack>(option?: Option<Level, Track>): (art: Artplayer) => Result
// Keep the required last signature for historical Parameters<typeof factory>[0] consumers.
declare function artplayerPluginHlsControl<Level extends object = QualityLevel, Track extends object = AudioTrack>(option: Option<Level, Track>): (art: Artplayer) => Result
export default artplayerPluginHlsControl
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,71 @@
# PKG-HLS-04:HLS 严格 TypeScript 与兼容声明
## 实现与兼容边界
上一任务建立的入口、映射、菜单、SDK 订阅模块迁移到 .ts,新增 types.ts 描述窄宿主、
SDK 可选能力和选择模型。五个自有模块均纳入 strict/noUncheckedIndexedAccess,
不引入 any、通配模块声明或 Hls 生产依赖。源码直接复用公开 Option/Result 类型,
独立消费夹具校验源码工厂与公开工厂相互可赋值。实际模块地图见包内 ARCHITECTURE.md。
公开工厂、字面量 name、同步 update、控件名/图标、DOM/CSS 和运行时根/legacy 路径不变。
getName 保留原 SDK 对象身份和普通函数调用,索引准确标为可选;默认质量/音轨字段可以推断,
调用方可用泛型或回调注解选择真实 SDK 类型。旧 object 参数 formatter 仍可赋值。
新增可选配置重载支持原来运行时就允许的省略/undefined;保留最后的必填重载,
以兼容旧 Parameters<typeof factory>[0]['quality'] 类型提取,不将其改成可能 undefined。
旧声明的 name 已是字面量,本次没有新增 name 写入限制。
.d.ts 继续为历史类型入口,.d.cts/.d.mts 提供 callable CommonJS/ESM 默认和命名类型桥,
legacy 的 typesVersions 支持 TS 4.3 Node10 查找。没有改变三个 JavaScript 文件的路径,
没有提前升级版本;逐包 next-major 与最终安装包检查继续由发布任务及 HLS-06 执行。
有限断言均有边界说明:入口承接外部附加的 art.hls,仍通过历史 errorHandle/media 检查;
SDK on/off 断言位于函数能力检查之后;选择项索引位于相等长度检查之后。它们不验证全部
外部 SDK 结构,不能替代运行时契约测试。新增的局部对象别名保持原方法接收者。
## 在线编辑器修复
旧生成方式保留 default/named exports 又附加 export=,严格编译生成结果会报告 TS2309。
新增 scripts/plugin-editor-types.mjs 使用 AST 为 HLS 生成私有定义和 callable/global 类型桥,
保留泛型默认和重载,拒绝未知 import/export,不静默删除依赖。build:ts 在写入该声明前
以 TS 4.3.5/5.9.3 对核心+插件独立语义检查,skipLibCheck=false。
生成器暂仅应用已验证的 HLS,其他插件按各自迁移任务接续;未手改 docs/assets/ts。
新测试检查输出可再生成、三条错误用法确实拒绝、损坏声明/未解析依赖不会被隐藏。
真实 Monaco worker 用例在三个浏览器中加载实际生成文件,验证默认字段、旧类型提取、
错误 formatter 诊断,并执行编译后的插件工厂。原核心 Monaco 示例也重跑并播放成功。
HLS 编辑器用例自身只执行工厂,不宣称播放;实际播放由独立 HLS SDK 矩阵验证。
首轮编辑器测试把重载错误预期写成 2322;实际 worker 正确返回 2769,无其他正例诊断。
按真实重载语义纠正断言后,最终六项全部通过,没有修改声明来容忍非法返回。
## 验收证据
冻结文件为 [hls-types-validation.json](../baselines/hls-types-validation.json),保留源码/类型/
生成脚本及产物指纹、浏览器输入与各用例结果、CI 日志摘要;旧任务冻结证据不覆盖。
| 项目 | 本次结果 |
| --- | --- |
| 结构与类型 | HLS 五个 TS 模块;全仓实际已迁移生产 TS 254 个严格检查通过 |
| 旧/新类型消费者 | TS 5.9 Node10 CJS、NodeNext CJS/ESM、Bundler ESM 和 TS 4.3 Node10 CJS 正例零诊断,每组八条非法用法拒绝;另检查 CJS 命名空间与源码工厂 |
| 实际 SDK 类型 | 真实 Hls.js 1.5.17 的 Level/MediaPlaylist 泛型消费;TS 5.9 零错误,TS 4.3 与 SDK-only 对照均仅两个既有 DOM 类型错误,插件无新增 |
| Node 与产物 | 源码/发布专项 56 通过;加 main/legacy/ESM 构建共享契约累计 83 通过 |
| 实际播放 | modern/legacy 各 35 passed、16 skipped、0 failed/retry,输入 SHA 与最终构建一致;覆盖新旧核心和插件四组合、选择/切源/失败/销毁/SDK 事件同步 |
| 编辑器 | 核心+HLS 在 Chromium/Firefox/WebKit 共六项通过,无失败/重试/跳过 |
| 完整本地 CI | yarn ci:check:536 项通过,496 单元、11 工程、29 基线;现有生成核心 editor unused eslint-disable 警告未手改 |
| 构建 | yarn build artplayer-plugin-hls-control、yarn build:ts 通过;三份 dist 与 docs/compiled 字节一致 |
main 为 5266 raw / 2539 gzip9 字节,legacy 5322 / 2564,ESM 9649 / 3117。
与上一任务相比只有局部别名/类型擦除后的微小变化,类型依赖未进入运行包,不声称体积优化。
## 未覆盖范围与交接
TS 4.3 的 MediaDecodingConfiguration/MediaCapabilitiesDecodingInfo 缺失来自真实 SDK
声明与旧 lib.dom 组合;保留明确对照,不加虚假 DOM 定义,不以 skipLibCheck 隐藏。
独立插件的旧编译器验收仍严格零诊断。SDK-01 和 HLS-ENV-01 保持 open。
Windows WebKit 的 16 个播放跳过源于已确认缺少 MSE;能力/失败清理及 Monaco 测试照常运行。
Safari/native、SDK worker/版本/分组轨道、官方示例及隔离 tarball 为 HLS-05/06 和发布门槛。
本步没有新增依赖;改进现有 build:ts、typecheck,新增生成 helper 和专项测试,已纳入
现有 test:baseline/ci:check。包内文档、类型入口和生成产物随源码一起更新。
本任务独立提交主题:refactor(hls): [PKG-HLS-04] migrate modules and compatible types。
回退该 commit 可恢复上一任务四模块 JS 和旧类型/生成行为;保留 HLS-01~03 的基线与修复。
提交并核实 Git 状态后进入 PKG-HLS-05。全项目目标继续,没有 push/tag/publish/merge。
+16 -4
View File
@@ -59,8 +59,20 @@ EX-03、REL-09/REVIEW-02 接续,SDK-01/HLS-ENV-01 保持 open。
Safari/native fallback、更多 SDK 版本、安装包及示例页面全部组合不是本轮完成内容。
显式安装产物映射暂不接受 HLS 源码回退;HLS-06 必须加入 HLS 包对应映射和隔离 tarball 消费。
## 后续实施
## PKG-HLS-03/04 后续验证
PKG-HLS-03 拆出映射、UI 提交/删除、Hls 订阅与生命周期;把上述缺陷变成候选修复断言,
保留发布版观察。PKG-HLS-04 迁移自有模块和公开类型,修正可选 option、getName 对象与可选 index,
保持旧声明接受范围及分发路径。PKG-HLS-05/06 完成实际 SDK、浏览器、示例及安装包验收。
PKG-HLS-03 已拆出映射、UI 提交/删除、Hls 订阅与生命周期;旧缺陷成为候选修复断言,
保留发布版观察。该步最终 Node 专项 56 通过,三格式共享契约累计 83 通过,modern/legacy
浏览器各 35 通过、16 跳过;以上早期 02 结果保持其原始范围,不冒充当前源码结果。
PKG-HLS-04 把五个模块迁移为严格 TS,公开类型保留旧调用和类型提取习惯,补齐默认字段、
SDK 泛型、可选 index 和 CJS/ESM/legacy 声明路径。在线编辑器声明从公开源生成,构建时用
TS 4.3/5.9 严格校验;真实 Monaco 验证推断、错误提示及编译后的工厂执行。
这些编辑器用例不声称执行 HLS 播放,实际播放仍由 hls-control.spec.js 负责。
本步结果见 [类型迁移记录](changes/2026-09-12-PKG-HLS-04-types.md) 和
[冻结证据](baselines/hls-types-validation.json)。
实际 Hls.js 1.5.17 声明在 TS 4.3 缺少 MediaDecodingConfiguration 和
MediaCapabilitiesDecodingInfo;仅 SDK 的消费者也出现相同两个诊断。插件无新增诊断,
不注入虚假 DOM 定义或打开 skipLibCheck。独立插件五组类型模式均要求零诊断。
PKG-HLS-05/06 继续完整 SDK、浏览器、示例及安装包验收,SDK-01/HLS-ENV-01 仍开放。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 215 项,范围 22 个包及工作区/示例。
状态:todo 151 / doing 0 / blocked 0 / done 64 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 150 / doing 0 / blocked 0 / done 65 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -191,7 +191,7 @@
| PKG-HLS-01 | artplayer-plugin-hls-control<br>核对包契约与历史用法 | BASE-05 | quality/audio 配置、getName、update、控件名及 Hls 实例绑定 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-HLS-02 | artplayer-plugin-hls-control<br>建立特有行为与错误测试 | PKG-HLS-01, ENG-03, ENG-05 | Auto/手动实际 level、音轨、去重/过滤和拓扑变化 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-HLS-03 | artplayer-plugin-hls-control<br>整理内部职责与资源 | PKG-HLS-02, CORE-11, CORE-14 | 映射计算与 selector 更新/删除、Hls 事件订阅分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-HLS-04 | artplayer-plugin-hls-control<br>迁移自有源码和公开类型 | PKG-HLS-03, ENG-04, ENG-06, CORE-07 | HLS 能力适配类型、回调上下文和旧 getName 参数 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| PKG-HLS-04 | artplayer-plugin-hls-control<br>迁移自有源码和公开类型 | PKG-HLS-03, ENG-04, ENG-06, CORE-07 | HLS 能力适配类型、回调上下文和旧 getName 参数 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done |
| PKG-HLS-05 | artplayer-plugin-hls-control<br>验证新旧核心和组合 | PKG-HLS-04, CORE-22 | 本地多码率 HLS、换成无轨道来源、旧核心和最终核心 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-HLS-06 | artplayer-plugin-hls-control<br>验证分发并同步文档 | PKG-HLS-05, ENG-07 | hls.control.js、依赖范围与回退记录 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
@@ -468,3 +468,4 @@
- PKG-HLS-01: [记录](baselines/hls-control-contract.md) [记录](baselines/hls-control-release.json) [记录](changes/2026-09-11-PKG-HLS-01-contract.md)
- PKG-HLS-02: [记录](hls-validation.md) [记录](baselines/hls-sdk.json) [记录](baselines/hls-validation.json) [记录](changes/2026-09-11-PKG-HLS-02-tests.md)
- PKG-HLS-03: [记录](changes/2026-09-12-PKG-HLS-03-modules.md) [记录](baselines/hls-modules-validation.json) [记录](hls-validation.md)
- PKG-HLS-04: [记录](changes/2026-09-12-PKG-HLS-04-types.md) [记录](baselines/hls-types-validation.json) [记录](hls-validation.md)
+15
View File
@@ -1,5 +1,20 @@
# 进度与证据
## 最新完成:PKG-HLS-04 严格 TS 与公开类型
HLS 五个自有模块已迁移严格 TS;公开声明保留旧 object formatter、Parameters 配置提取、
同步 update 及运行时入口,补齐默认字段、SDK 泛型、可选索引和 CJS/ESM/legacy 类型桥。
在线编辑器由公开声明生成全局桥,并在 build:ts 内做双编译器严格语义检查。
完整本地 CI 536 项、254 个生产 TS 文件通过;HLS Node 专项 56 项,三格式累计 83 项通过。
modern/legacy 浏览器各 35 通过、16 跳过,核心+HLS Monaco 六项通过;无失败/重试。
最终产物输入指纹一致。实际 SDK 在 TS 4.3 的两个 DOM 类型缺失诊断有 SDK-only 对照,
不隐藏或称为全 SDK 支持;Windows WebKit 的 MSE/Safari 缺口依然开放。
见 [交付记录](changes/2026-09-12-PKG-HLS-04-types.md)、[冻结证据](baselines/hls-types-validation.json)
和包内 ARCHITECTURE.md。65 done、150 todo;本任务独立本地 commit,核实后进入 PKG-HLS-05。
完整 SDK/组合/安装包、其余生态包、性能/CI/CD/版本/复盘继续执行原计划,没有公开发布。
## 最新完成:PKG-HLS-03 源码分层与生命周期修复
HLS control 已拆为入口、选择映射、菜单管理、SDK 订阅四个 JS 模块,保持公开入口和同步调用;修复空轨道旧菜单、过期回调、Auto 标签、重复组丢失选中态,并补事件同步、销毁/订阅失败恢复及重入保护。公开类型和严格 TS 迁移属于下一步 04。
+1 -1
View File
@@ -610,7 +610,7 @@
"EX-03"
],
"evidence": [
"packages/artplayer-plugin-hls-control/src/index.js",
"packages/artplayer-plugin-hls-control/src/index.ts",
"docs/assets/example/hls.control.js",
"refactor/hls-validation.md"
],
+92
View File
@@ -0,0 +1,92 @@
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 -- Use the repository's built-in baseline runner.
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, readMember, refactorDir } from './releases.mjs'
test('HLS generated editor types are reproducible, standalone and reject invalid formatter uses', async () => {
const source = fs.readFileSync(new URL('../../packages/artplayer-plugin-hls-control/types/artplayer-plugin-hls-control.d.ts', import.meta.url), 'utf8')
const generated = generatePluginEditorDeclaration(source, 'artplayerPluginHlsControl')
const file = 'docs/assets/ts/artplayer-plugin-hls-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: artplayerPluginHlsControl.Option = { quality: { getName: (level, index) => level.height + ':' + index }, audio: { getName: track => track.name } }
const legacyOption: NonNullable<Parameters<typeof artplayerPluginHlsControl>[0]['quality']> = { getName: (value: object) => String(value) }
const plugin: (art: Artplayer) => artplayerPluginHlsControl.Result = artplayerPluginHlsControl(option)
artplayerPluginHlsControl()
artplayerPluginHlsControl(undefined)
artplayerPluginHlsControl({ quality: { getName: (value: { bitrate: number }) => String(value.bitrate) } })
// @ts-expect-error Formatter must return a string.
artplayerPluginHlsControl({ quality: { getName: level => level.height } })
// @ts-expect-error Default quality type cannot silently become any.
artplayerPluginHlsControl({ quality: { getName: level => level.missing } })
// @ts-expect-error Private definitions must not leak into globals.
const hidden: artplayerPluginHlsControlDefinitions.Option = {}
void [legacyOption, plugin, 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)
assert(checkPluginEditorDeclaration(`${generated}\nexport interface Broken { value: MissingType }`, core, '', compiler).some(item => item.code === 2309), 'Do not hide incompatible export assignment or unresolved declarations')
}
assert.throws(() => generatePluginEditorDeclaration(source.replace('from \'artplayer\'', 'from \'missing-sdk\''), 'artplayerPluginHlsControl'), /Unsupported plugin editor import/)
assert.throws(() => generatePluginEditorDeclaration(`${source}\nexport { Missing } from './missing'`, 'artplayerPluginHlsControl'), /Unsupported plugin editor declaration/)
assert.throws(() => generatePluginEditorDeclaration('export interface {', 'plugin'), /Invalid plugin declaration syntax/)
})
test('HLS root, legacy, named types and callback inference work across old and modern compilers', () => {
const source = fs.readFileSync(path.join(refactorDir, '../test/types/hls-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, `${compiler.version} ${mode} must reject all invalid uses`)
}
const cjs = 'import hls = require(\'artplayer-plugin-hls-control\'); import legacy = require(\'artplayer-plugin-hls-control/legacy\'); const option: hls.Option = { audio: { getName: track => track.name } }; hls(option); legacy();'
assert.deepEqual(checkConsumer(ts, 'nodenext-cjs', cjs), [])
})
test('HLS callback generics accept actual pinned SDK declarations without a runtime dependency', async () => {
const { release } = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/hls-sdk.json')))
const archive = await ensureArchive(release)
const directory = fs.mkdtempSync(path.join(refactorDir, '.cache/hls-types-'))
try {
fs.writeFileSync(path.join(directory, 'sdk.d.ts'), readMember(archive, 'package/dist/hls.js.d.ts'))
const filename = path.join(directory, 'consumer.ts')
fs.writeFileSync(filename, `import type { Level, MediaPlaylist } from './sdk';
import hls from 'artplayer-plugin-hls-control';
hls<Level, MediaPlaylist>({ quality: { getName: level => String(level.bitrate) }, audio: { getName: track => track.name } });
hls({ quality: { getName: (level: Level, index?: number) => level.height + ':' + index }, audio: { getName: (track: MediaPlaylist) => track.groupId } });
`)
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 describe = program => compiler.getPreEmitDiagnostics(program).map(item => ({ file: item.file && path.basename(item.file.fileName), code: item.code, message: compiler.flattenDiagnosticMessageText(item.messageText, '\n') }))
const expected = compiler === compat
? [
{ file: 'sdk.d.ts', code: 2304, message: 'Cannot find name \'MediaDecodingConfiguration\'.' },
{ file: 'sdk.d.ts', code: 2304, message: 'Cannot find name \'MediaCapabilitiesDecodingInfo\'.' },
]
: []
assert.deepEqual(describe(program), expected, `Actual Hls.js types / TS ${compiler.version}`)
const sdkOnly = path.join(directory, 'sdk-only.ts')
fs.writeFileSync(sdkOnly, 'import type { Level, MediaPlaylist } from \'./sdk\'; export type Inputs = [Level, MediaPlaylist];\n')
const control = compiler.createProgram([sdkOnly], program.getCompilerOptions())
assert.deepEqual(describe(control), expected, 'Old DOM library errors must originate in the SDK alone, not the plugin')
assert(program.getSourceFiles().some(file => path.resolve(file.fileName) === path.join(directory, 'sdk.d.ts')))
}
}
finally {
const resolved = fs.realpathSync(directory)
assert.equal(path.dirname(resolved), fs.realpathSync(path.join(refactorDir, '.cache')))
assert(path.basename(resolved).startsWith('hls-types-'))
fs.rmSync(resolved, { recursive: true, force: true })
}
})
+6 -2
View File
@@ -1850,11 +1850,15 @@
"ENG-06",
"CORE-07"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "HLS 能力适配类型、回调上下文和旧 getName 参数",
"acceptance": "严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容",
"evidence": []
"evidence": [
"changes/2026-09-12-PKG-HLS-04-types.md",
"baselines/hls-types-validation.json",
"hls-validation.md"
]
},
{
"id": "PKG-HLS-05",
+4 -4
View File
@@ -1027,10 +1027,10 @@
"EX-03"
],
"sourceFiles": [
"packages/artplayer-plugin-hls-control/src/index.js",
"packages/artplayer-plugin-hls-control/src/mapping.js",
"packages/artplayer-plugin-hls-control/src/menu.js",
"packages/artplayer-plugin-hls-control/src/sdk-events.js",
"packages/artplayer-plugin-hls-control/src/index.ts",
"packages/artplayer-plugin-hls-control/src/mapping.ts",
"packages/artplayer-plugin-hls-control/src/menu.ts",
"packages/artplayer-plugin-hls-control/src/sdk-events.ts",
"docs/assets/example/hls.control.js"
],
"contract": "Caller supplies art.hls; homepage demo loads hls.js 1.5.17. Preserve quality/audio selectors and old core integration.",
+10
View File
@@ -1,5 +1,15 @@
# 已实现的类型检查与迁移入口
PKG-HLS-04 增加 HLS 的 5 个严格 TS 源模块,公开声明保留历史 .d.ts 并增加 CJS/ESM 条件桥。
test/types/hls-control.ts 覆盖配置省略、旧 Parameters 提取、默认字段和 SDK 泛型、同步返回,
hls-source.ts 检查源码工厂与公开工厂可相互赋值。hls-types.test.mjs 对五组编译模式检查正反例,
并消费实际 Hls.js 1.5.17 声明。TS 4.3 的两个 SDK 自有 DOM 缺失诊断有 SDK-only 对照,
不作插件通过的豁免;独立插件消费者仍要求零错误。
scripts/plugin-editor-types.mjs 与 build:ts 负责 HLS 全局声明生成和双编译器语义校验,
真实 Monaco 用例为 test/browser/hls-editor-types.spec.js;它验证类型和编译后工厂执行,
实际 HLS 播放由 hls-control.spec.js 验证。详见 changes/2026-09-12-PKG-HLS-04-types.md。
下方按原任务保留历史计数;当前执行统计以本次冻结证据和 typecheck 输出为准。
CORE-21 已完成声明与消费者验收。核心公开声明源已迁至 packages/artplayer/public/,
使用 yarn build:types 生成历史 types/ 路径;yarn check:types 只读检查漂移、
源错误和内部依赖,已接入 ci:check。public/ 与 src/ 分别纳入严格检查,
+12 -1
View File
@@ -2,7 +2,9 @@ import fs from 'node:fs'
import path from 'node:path'
import { ESLint } from 'eslint'
import { glob } from 'glob'
import compat from 'typescript-compat'
import { generateCoreEditorDeclaration } from './editor-types.mjs'
import { checkPluginEditorDeclaration, generatePluginEditorDeclaration } from './plugin-editor-types.mjs'
function ensureDirExists(filePath) {
const dir = path.dirname(filePath)
@@ -40,7 +42,16 @@ console.log(`✨ Built ${artplayerTSoutput}`);
for (let index = 0; index < pluginsTS.length; index++) {
const type = pluginsTS[index]
const { name, file } = parsePluginInfo(type)
const code = `${String(fs.readFileSync(type)).replace(reg, '')}\nexport = ${name};\nexport as namespace ${name};\n`
const source = String(fs.readFileSync(type))
const code = name === 'artplayerPluginHlsControl'
? generatePluginEditorDeclaration(source, name)
: `${source.replace(reg, '')}\nexport = ${name};\nexport as namespace ${name};\n`
if (name === 'artplayerPluginHlsControl') {
const core = fs.readFileSync(artplayerTSoutput, 'utf8')
const diagnostics = [...checkPluginEditorDeclaration(code, core), ...checkPluginEditorDeclaration(code, core, '', compat)]
if (diagnostics.length)
throw new Error(`Invalid ${name} editor declaration: ${JSON.stringify(diagnostics)}`)
}
const output = path.join('docs/assets/ts', file)
ensureDirExists(output)
fs.writeFileSync(output, code.trim())
+76
View File
@@ -0,0 +1,76 @@
import assert from 'node:assert/strict'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import ts from 'typescript'
// Migrate each package explicitly; unknown imports/exports must not disappear silently.
export function generatePluginEditorDeclaration(code, name) {
assert(/^[a-z_$][\w$]*$/i.test(name), 'Invalid plugin global')
const source = ts.createSourceFile('plugin.d.ts', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
assert.equal(source.parseDiagnostics.length, 0, 'Invalid plugin declaration syntax')
const factory = ts.factory
const definitions = []
const aliases = []
const internal = `${name}Definitions`
let exported = false
let callable = false
for (const node of source.statements) {
if (ts.isImportDeclaration(node)) {
assert(node.importClause?.isTypeOnly && !node.importClause.namedBindings && node.importClause.name?.text === 'Artplayer' && node.moduleSpecifier.text === 'artplayer', 'Unsupported plugin editor import')
continue
}
if (ts.isExportAssignment(node)) {
assert(!node.isExportEquals && ts.isIdentifier(node.expression) && node.expression.text === name, 'Unexpected plugin default export')
exported = true
continue
}
assert(ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isFunctionDeclaration(node), 'Unsupported plugin editor declaration')
assert(node.name && node.name.text !== internal, 'Invalid plugin declaration name')
definitions.push(factory.replaceModifiers(node, [factory.createModifier(ts.SyntaxKind.ExportKeyword)]))
if (ts.isFunctionDeclaration(node)) {
assert.equal(node.name.text, name, 'Unexpected plugin callable')
callable = true
continue
}
if (node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
aliases.push(factory.createTypeAliasDeclaration(
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
node.name,
node.typeParameters,
factory.createTypeReferenceNode(factory.createQualifiedName(factory.createIdentifier(internal), node.name), node.typeParameters?.map(parameter => factory.createTypeReferenceNode(parameter.name))),
))
}
}
assert(exported && callable, 'Plugin editor requires a default function')
const namespace = (identifier, statements) => factory.createModuleDeclaration([factory.createModifier(ts.SyntaxKind.DeclareKeyword)], factory.createIdentifier(identifier), factory.createModuleBlock(statements), ts.NodeFlags.Namespace)
const statements = [
namespace(internal, definitions),
factory.createVariableStatement([factory.createModifier(ts.SyntaxKind.DeclareKeyword)], factory.createVariableDeclarationList([
factory.createVariableDeclaration(name, undefined, factory.createTypeQueryNode(factory.createQualifiedName(factory.createIdentifier(internal), factory.createIdentifier(name)))),
], ts.NodeFlags.Const)),
namespace(name, aliases),
factory.createExportAssignment(undefined, true, factory.createIdentifier(name)),
factory.createNamespaceExportDeclaration(factory.createIdentifier(name)),
]
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed })
return `// Generated from the package public declaration by yarn build:ts. Do not edit.\n/* eslint-disable ts/no-redeclare -- Callable and public type namespace intentionally merge. */\n${statements.map(node => printer.printNode(ts.EmitHint.Unspecified, node, source)).join('\n')}\n`
}
export function checkPluginEditorDeclaration(code, core, consumer = '', compiler = ts) {
const folder = fileURLToPath(new URL('../refactor/.cache/plugin-editor-semantic/', import.meta.url))
const sources = new Map([
[path.join(folder, 'artplayer.d.ts'), core],
[path.join(folder, 'plugin.d.ts'), code],
[path.join(folder, 'consumer.ts'), consumer],
])
const options = { strict: true, noEmit: true, skipLibCheck: false, types: [], target: compiler.ScriptTarget.ES2020, lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'], module: compiler.ModuleKind.CommonJS, moduleResolution: compiler.ModuleResolutionKind.NodeJs }
const host = compiler.createCompilerHost(options)
const getSourceFile = host.getSourceFile.bind(host)
host.getSourceFile = (file, version, onError, create) => sources.has(path.resolve(file))
? compiler.createSourceFile(file, sources.get(path.resolve(file)), version, true)
: getSourceFile(file, version, onError, create)
const program = compiler.createProgram([...sources.keys()], options, host)
for (const file of program.getSourceFiles())
assert(sources.has(path.resolve(file.fileName)) || program.isSourceFileDefaultLibrary(file), `Plugin editor types escaped the standalone bundle: ${file.fileName}`)
return compiler.getPreEmitDiagnostics(program).map(diagnostic => ({ code: diagnostic.code, message: compiler.flattenDiagnosticMessageText(diagnostic.messageText, '\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/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/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 HLS global types and executes the emitted plugin factory', async ({ page }, testInfo) => {
const code = process.env.ARTPLAYER_HLS_ARTIFACT
? fs.readFileSync(process.env.ARTPLAYER_HLS_ARTIFACT, 'utf8')
: await compilePackage('artplayer-plugin-hls-control', 'umd')
const declarations = Object.fromEntries(['artplayer', 'artplayer-plugin-hls-control'].map(name => [name, hash(fs.readFileSync(new URL(`../../docs/assets/ts/${name}.d.ts`, import.meta.url)))]))
const inputs = { candidate: process.env.ARTPLAYER_HLS_ARTIFACT || 'workspace source build', sha256: hash(code), declarations }
await testInfo.attach('hls-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-hls-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 hlsOption: artplayerPluginHlsControl.Option = {
quality: { getName: (level, index) => level.height + ':' + index },
audio: { getName: track => track.name },
}
const oldQuality: NonNullable<Parameters<typeof artplayerPluginHlsControl>[0]['quality']> = { getName: (value: object) => String(value) }
const typedFactory: (art: Artplayer) => artplayerPluginHlsControl.Result = artplayerPluginHlsControl(hlsOption)
const defaultFactory = artplayerPluginHlsControl()
`, 'typescript', api.Uri.parse('file:///hls-consumer.ts'))
const invalid = api.editor.createModel('artplayerPluginHlsControl({ quality: { getName: level => level.height } })', 'typescript', api.Uri.parse('file:///hls-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 HLS 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-hls-control'
import Artplayer from 'artplayer'
import hlsControl from 'artplayer-plugin-hls-control'
import legacy from 'artplayer-plugin-hls-control/legacy'
const option: Option = {
quality: { control: true, getName: (level, index) => `${level.height}P ${index ?? ''}` },
audio: { setting: true, getName: track => track.name },
}
const art = new Artplayer({ container: '#player', url: 'video.m3u8', plugins: [hlsControl(), hlsControl(option), legacy()] })
const result: Result = hlsControl(option)(art)
type HistoricalOption = Parameters<typeof hlsControl>[0]
const historicalQuality: HistoricalOption['quality'] = { getName: (item: object) => String(item) }
hlsControl({ quality: historicalQuality })
hlsControl(undefined)
const done: void = result.update()
const generic: Config = { getName: (item: object) => String(item) }
hlsControl({ quality: generic, audio: generic })
const level: QualityLevel = { height: 720 }
const track: AudioTrack = { id: 0, name: 'English' }
interface CustomLevel { height: number, bitrate: number, label: string }
interface CustomTrack { id: number, name: string, groupId: string }
hlsControl({ quality: { getName: (value: CustomLevel) => value.label }, audio: { getName: (value: CustomTrack) => value.groupId } })
hlsControl<CustomLevel, CustomTrack>({ quality: { getName: value => String(value.bitrate) } })
// @ts-expect-error update stays synchronous and returns void.
const promise: Promise<void> = result.update()
// @ts-expect-error update takes no configuration argument.
result.update({})
// @ts-expect-error The result name remains a literal.
const invalidName: Result['name'] = 'hls'
// @ts-expect-error Selected-label calls do not supply the index.
hlsControl({ quality: { getName: (value: QualityLevel, index: number) => String(index) } })
// @ts-expect-error A formatter must return a string.
hlsControl({ audio: { getName: () => 123 } })
// @ts-expect-error Control visibility is boolean.
legacy({ quality: { control: 'yes' } })
// @ts-expect-error Null was never a valid options object.
hlsControl(null)
// @ts-expect-error Default quality fields are typed, not an any escape.
hlsControl({ quality: { getName: value => value.unknownProperty } })
void [done, level, track, promise, invalidName]
+9
View File
@@ -0,0 +1,9 @@
import type PublicFactory from 'artplayer-plugin-hls-control'
import factory from '../../packages/artplayer-plugin-hls-control/src/index'
const compatible: typeof PublicFactory = factory
const implementation: typeof factory = compatible
implementation({ quality: { getName: level => `${level.height}P` }, audio: { getName: track => track.name } })
compatible()
// @ts-expect-error Source and public declaration both reject non-string formatter results.
factory({ quality: { getName: () => 1 } })