diff --git a/docs/compiled/artplayer-plugin-dash-control.js b/docs/compiled/artplayer-plugin-dash-control.js
index 97ce60f03..2c9950667 100644
--- a/docs/compiled/artplayer-plugin-dash-control.js
+++ b/docs/compiled/artplayer-plugin-dash-control.js
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
-!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";function t(t,e){const o=new Map;return t.filter(t=>{const n=t[e];return void 0===n||!o.has(n)&&o.set(n,1)})}return function(e={}){return o=>{const{$video:n}=o.template,{errorHandle:i}=o.constructor.utils;function a(){i(o.dash.getVideoElement()===n,'Cannot find instance of DASH from "art.dash"'),function(n){const i=n.getRepresentationsByType("video");if(!i||!i.length)return;const a=e.quality||{},l=a.auto||"Auto",s=a.title||"Quality",r=a.getName||(t=>`${t.height}p`),c=n.getCurrentRepresentationForType("video"),u=c&&c.id,d=n.getSettings().streaming.abr.autoSwitchBitrate.video,h=!d&&c?r(c):l,g=t(i.map((t,e)=>({html:r(t),value:e,id:t.id,default:!d&&!!u&&t.id===u})),"html").sort((t,e)=>e.value-t.value);g.push({html:l,value:"auto",default:d});const p=t=>("auto"===t.value?n.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!0}}}}):(n.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!1}}}}),n.setRepresentationForTypeById("video",t.id)),o.notice.show=`${s}: ${t.html}`,a.control&&o.controls.check(t),a.setting&&o.setting.check(t),t.html);a.control&&o.controls.update({name:"dash-quality",position:"right",html:h,style:{padding:"0 10px"},selector:g,onSelect:p}),a.setting&&o.setting.update({name:"dash-quality",tooltip:h,html:s,icon:'',width:200,selector:g,onSelect:p})}(o.dash),function(n){const i=n.getTracksFor("audio");if(!i||!i.length)return;const a=e.audio||{},l=a.auto||"Auto",s=a.title||"Audio",r=a.getName||(t=>t.lang||t.id),c=n.getCurrentTrackFor("audio")||i[0],u=c?r(c):l,d=t(i.map(t=>({html:r(t),value:t,default:c===t})),"html"),h=t=>(n.setCurrentTrack(t.value),o.notice.show=`${s}: ${t.html}`,a.control&&o.controls.check(t),a.setting&&o.setting.check(t),t.html);a.control&&o.controls.update({name:"dash-audio",position:"right",html:u,style:{padding:"0 10px"},selector:d,onSelect:h}),a.setting&&o.setting.update({name:"dash-audio",tooltip:u,html:s,icon:'',width:200,selector:d,onSelect:h})}(o.dash)}return o.on("ready",a),o.on("restart",a),{name:"artplayerPluginDashControl",update:a}}}});
+!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function e(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){if(!o||r.get(n)!==o)return;r.delete(n);const l=t[n];"controls"===n&&l.cache?.get&&l.cache.get(e)?.option?.onSelect!==o||"setting"===n&&l.find&&l.find(e)?.onSelect!==o||l.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,s,u){if(!s)return void i();const a={};o=a;const f=()=>a===o&&u(),d=e=>f()?(s.select(e,f),f()?(t.notice.show=`${s.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:s.html,style:{padding:"0 10px"},selector:s.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:s.html,html:s.title,icon:n,width:200,selector:s.selector,onSelect:d})):l("setting"))},clear:i}}return function(n={}){return o=>{const{$video:r}=o.template,{errorHandle:l}=o.constructor.utils;let i=!1,c=0;const s=e(o,"dash-quality",''),u=e(o,"dash-audio",''),a=[];function f(t=()=>!0){let e;for(const o of[s.clear,u.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function d(){if(i||o.isDestroy)return;const e=++c,a=o.dash,d=()=>!i&&!o.isDestroy&&e===c&&o.dash===a,h=()=>d()&&a.getVideoElement()===r&&d();try{if(l(a.getVideoElement()===r,'Cannot find instance of DASH from "art.dash"'),!d())return;const e=n.quality||{},o=function(e,n,o){const r=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(e),l=r.levels();if(!o()||!l?.length)return null;const i=n.auto||"Auto",c=n.getName||(t=>`${t.height}p`),s=r.current(l);if(!o())return null;const u=e.getSettings().streaming.abr.autoSwitchBitrate.video;if(!o())return null;const a=!u&&s?c(s):i;if(!o())return null;const f=[];for(const[t,h]of l.entries()){const e=c(h);if(!o())return null;f.push({html:e,...r.item(h,t,s,u)})}const d=t(f).sort((t,e)=>e.value-t.value);return d.push({html:i,value:"auto",default:u}),{html:a,title:n.title||"Quality",selector:d,select:r.select}}(a,e,h);if(!h())return;if(s.update(e,o,h),!h())return;const i=n.audio||{},c=function(e,n,o){const r=e.getTracksFor("audio");if(!o()||!r?.length)return null;const l=n.auto||"Auto",i=n.getName||(t=>t.lang||t.id),c=e.getCurrentTrackFor("audio")||r[0];if(!o())return null;const s=c?i(c):l;if(!o())return null;const u=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=t.filter(t=>["id","index","lang"].every(n=>null==e[n]||t[n]===e[n]));return 1===n.length?n[0]:void 0}(r,c),a=[];for(const t of r){const e=i(t);if(!o())return null;a.push({html:e,value:t,default:t===u})}const f=t(a);return{html:s,title:n.title||"Audio",selector:f,select:t=>e.setCurrentTrack(t.value)}}(a,i,h);h()&&u.update(i,c,h)}catch(g){if(d()){const t=++c;try{f(()=>c===t)}catch(p){console.warn("ArtPlayer DASH cleanup failed:",p)}}throw g}}function h(){if(i)return;let t;i=!0,c++;const e=[f,...a.splice(0).map(([t,e])=>()=>o.off(t,e))];for(const o of e)try{o()}catch(n){t||(t=n)}if(t)throw t}try{for(const t of[["ready",d],["restart",d],["destroy",h]]){if(i)break;a.push(t),o.on(...t)}}catch(g){try{h()}catch(p){console.warn("ArtPlayer DASH subscription cleanup failed:",p)}throw g}return{name:"artplayerPluginDashControl",update:d}}}});
diff --git a/docs/compiled/artplayer-plugin-dash-control.legacy.js b/docs/compiled/artplayer-plugin-dash-control.legacy.js
index 97ce60f03..a270906b6 100644
--- a/docs/compiled/artplayer-plugin-dash-control.legacy.js
+++ b/docs/compiled/artplayer-plugin-dash-control.legacy.js
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
-!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";function t(t,e){const o=new Map;return t.filter(t=>{const n=t[e];return void 0===n||!o.has(n)&&o.set(n,1)})}return function(e={}){return o=>{const{$video:n}=o.template,{errorHandle:i}=o.constructor.utils;function a(){i(o.dash.getVideoElement()===n,'Cannot find instance of DASH from "art.dash"'),function(n){const i=n.getRepresentationsByType("video");if(!i||!i.length)return;const a=e.quality||{},l=a.auto||"Auto",s=a.title||"Quality",r=a.getName||(t=>`${t.height}p`),c=n.getCurrentRepresentationForType("video"),u=c&&c.id,d=n.getSettings().streaming.abr.autoSwitchBitrate.video,h=!d&&c?r(c):l,g=t(i.map((t,e)=>({html:r(t),value:e,id:t.id,default:!d&&!!u&&t.id===u})),"html").sort((t,e)=>e.value-t.value);g.push({html:l,value:"auto",default:d});const p=t=>("auto"===t.value?n.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!0}}}}):(n.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!1}}}}),n.setRepresentationForTypeById("video",t.id)),o.notice.show=`${s}: ${t.html}`,a.control&&o.controls.check(t),a.setting&&o.setting.check(t),t.html);a.control&&o.controls.update({name:"dash-quality",position:"right",html:h,style:{padding:"0 10px"},selector:g,onSelect:p}),a.setting&&o.setting.update({name:"dash-quality",tooltip:h,html:s,icon:'',width:200,selector:g,onSelect:p})}(o.dash),function(n){const i=n.getTracksFor("audio");if(!i||!i.length)return;const a=e.audio||{},l=a.auto||"Auto",s=a.title||"Audio",r=a.getName||(t=>t.lang||t.id),c=n.getCurrentTrackFor("audio")||i[0],u=c?r(c):l,d=t(i.map(t=>({html:r(t),value:t,default:c===t})),"html"),h=t=>(n.setCurrentTrack(t.value),o.notice.show=`${s}: ${t.html}`,a.control&&o.controls.check(t),a.setting&&o.setting.check(t),t.html);a.control&&o.controls.update({name:"dash-audio",position:"right",html:u,style:{padding:"0 10px"},selector:d,onSelect:h}),a.setting&&o.setting.update({name:"dash-audio",tooltip:u,html:s,icon:'',width:200,selector:d,onSelect:h})}(o.dash)}return o.on("ready",a),o.on("restart",a),{name:"artplayerPluginDashControl",update:a}}}});
+!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,r=(e,n,o)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[n]=o,l=(t,l)=>{for(var i in l||(l={}))n.call(l,i)&&r(t,i,l[i]);if(e)for(var i of e(l))o.call(l,i)&&r(t,i,l[i]);return t};function i(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function c(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){var l,i,c,u;if(!o||r.get(n)!==o)return;r.delete(n);const s=t[n];"controls"===n&&(null==(l=s.cache)?void 0:l.get)&&(null==(c=null==(i=s.cache.get(e))?void 0:i.option)?void 0:c.onSelect)!==o||"setting"===n&&s.find&&(null==(u=s.find(e))?void 0:u.onSelect)!==o||s.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,u,s){if(!u)return void i();const a={};o=a;const f=()=>a===o&&s(),d=e=>f()?(u.select(e,f),f()?(t.notice.show=`${u.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:u.html,style:{padding:"0 10px"},selector:u.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:u.html,html:u.title,icon:n,width:200,selector:u.selector,onSelect:d})):l("setting"))},clear:i}}return function(t={}){return e=>{const{$video:n}=e.template,{errorHandle:o}=e.constructor.utils;let r=!1,u=0;const s=c(e,"dash-quality",''),a=c(e,"dash-audio",''),f=[];function d(t=()=>!0){let e;for(const o of[s.clear,a.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function h(){if(r||e.isDestroy)return;const c=++u,f=e.dash,h=()=>!r&&!e.isDestroy&&c===u&&e.dash===f,p=()=>h()&&f.getVideoElement()===n&&h();try{if(o(f.getVideoElement()===n,'Cannot find instance of DASH from "art.dash"'),!h())return;const e=t.quality||{},r=function(t,e,n){const o=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(t),r=o.levels();if(!n()||!(null==r?void 0:r.length))return null;const c=e.auto||"Auto",u=e.getName||(t=>`${t.height}p`),s=o.current(r);if(!n())return null;const a=t.getSettings().streaming.abr.autoSwitchBitrate.video;if(!n())return null;const f=!a&&s?u(s):c;if(!n())return null;const d=[];for(const[i,p]of r.entries()){const t=u(p);if(!n())return null;d.push(l({html:t},o.item(p,i,s,a)))}const h=i(d).sort((t,e)=>e.value-t.value);return h.push({html:c,value:"auto",default:a}),{html:f,title:e.title||"Quality",selector:h,select:o.select}}(f,e,p);if(!p())return;if(s.update(e,r,p),!p())return;const c=t.audio||{},u=function(t,e,n){const o=t.getTracksFor("audio");if(!n()||!(null==o?void 0:o.length))return null;const r=e.auto||"Auto",l=e.getName||(t=>t.lang||t.id),c=t.getCurrentTrackFor("audio")||o[0];if(!n())return null;const u=c?l(c):r;if(!n())return null;const s=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=t.filter(t=>["id","index","lang"].every(n=>null==e[n]||t[n]===e[n]));return 1===n.length?n[0]:void 0}(o,c),a=[];for(const i of o){const t=l(i);if(!n())return null;a.push({html:t,value:i,default:i===s})}const f=i(a);return{html:u,title:e.title||"Audio",selector:f,select:e=>t.setCurrentTrack(e.value)}}(f,c,p);p()&&a.update(c,u,p)}catch(g){if(h()){const t=++u;try{d(()=>u===t)}catch(v){console.warn("ArtPlayer DASH cleanup failed:",v)}}throw g}}function p(){if(r)return;let t;r=!0,u++;const n=[d,...f.splice(0).map(([t,n])=>()=>e.off(t,n))];for(const e of n)try{e()}catch(o){t||(t=o)}if(t)throw t}try{for(const t of[["ready",h],["restart",h],["destroy",p]]){if(r)break;f.push(t),e.on(...t)}}catch(g){try{p()}catch(v){console.warn("ArtPlayer DASH subscription cleanup failed:",v)}throw g}return{name:"artplayerPluginDashControl",update:h}}}});
diff --git a/docs/compiled/artplayer-plugin-dash-control.mjs b/docs/compiled/artplayer-plugin-dash-control.mjs
index 4ce15acf9..a0c85dbe8 100644
--- a/docs/compiled/artplayer-plugin-dash-control.mjs
+++ b/docs/compiled/artplayer-plugin-dash-control.mjs
@@ -5,163 +5,260 @@
* Released under the MIT License.
*/
const $audio = '';
-const $quality = '';
-function uniqBy(array, property) {
- const seen = /* @__PURE__ */ new Map();
- return array.filter((item) => {
- const key = item[property];
- if (key === void 0) {
- return true;
+function qualityAdapter(dash) {
+ const modern = typeof dash.getRepresentationsByType === "function";
+ return {
+ levels: () => modern ? dash.getRepresentationsByType("video") : dash.getBitrateInfoListFor("video"),
+ current(levels) {
+ if (modern)
+ return dash.getCurrentRepresentationForType("video");
+ const quality = dash.getQualityFor("video");
+ return levels.find((level) => level.qualityIndex === quality);
+ },
+ item(level, index, selected, automatic) {
+ return modern ? { value: index, id: level.id, default: !automatic && selected != null && level.id === selected.id } : { value: level.qualityIndex, default: !automatic && selected != null && level.qualityIndex === selected.qualityIndex };
+ },
+ select(item, valid) {
+ const automatic = item.value === "auto";
+ dash.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: automatic } } } });
+ if (!automatic && valid()) {
+ if (modern)
+ dash.setRepresentationForTypeById("video", item.id);
+ else
+ dash.setQualityFor("video", item.value);
+ }
}
- return !seen.has(key) && seen.set(key, 1);
+ };
+}
+function uniqueLabels(items) {
+ const seen = /* @__PURE__ */ new Map();
+ return items.filter((item) => {
+ if (item.html === void 0)
+ return true;
+ const first = seen.get(item.html);
+ if (first) {
+ if (item.default)
+ Object.assign(first, item);
+ return false;
+ }
+ seen.set(item.html, item);
+ return true;
});
}
+function qualityModel(dash, config, active) {
+ const adapter = qualityAdapter(dash);
+ const levels = adapter.levels();
+ if (!active() || !levels?.length)
+ return null;
+ const auto = config.auto || "Auto";
+ const getName = config.getName || ((level) => `${level.height}p`);
+ const selected = adapter.current(levels);
+ if (!active())
+ return null;
+ const automatic = dash.getSettings().streaming.abr.autoSwitchBitrate.video;
+ if (!active())
+ return null;
+ const html = !automatic && selected ? getName(selected) : auto;
+ if (!active())
+ return null;
+ const items = [];
+ for (const [index, level] of levels.entries()) {
+ const label = getName(level);
+ if (!active())
+ return null;
+ items.push({ html: label, ...adapter.item(level, index, selected, automatic) });
+ }
+ const selector = uniqueLabels(items).sort((left, right) => right.value - left.value);
+ selector.push({ html: auto, value: "auto", default: automatic });
+ return { html, title: config.title || "Quality", selector, select: adapter.select };
+}
+function selectedTrack(tracks, current) {
+ if (tracks.includes(current))
+ return current;
+ if (current.id == null && current.index == null)
+ return void 0;
+ const matching = tracks.filter((track) => ["id", "index", "lang"].every((key) => current[key] == null || track[key] === current[key]));
+ return matching.length === 1 ? matching[0] : void 0;
+}
+function audioModel(dash, config, active) {
+ const tracks = dash.getTracksFor("audio");
+ if (!active() || !tracks?.length)
+ return null;
+ const auto = config.auto || "Auto";
+ const getName = config.getName || ((track) => track.lang || track.id);
+ const current = dash.getCurrentTrackFor("audio") || tracks[0];
+ if (!active())
+ return null;
+ const html = current ? getName(current) : auto;
+ if (!active())
+ return null;
+ const selected = selectedTrack(tracks, current);
+ const items = [];
+ for (const track of tracks) {
+ const label = getName(track);
+ if (!active())
+ return null;
+ items.push({ html: label, value: track, default: track === selected });
+ }
+ const selector = uniqueLabels(items);
+ return { html, title: config.title || "Audio", selector, select: (item) => dash.setCurrentTrack(item.value) };
+}
+function createMenu(art, name, icon) {
+ let current;
+ const owned = /* @__PURE__ */ new Map();
+ function remove(surface, callback = owned.get(surface)) {
+ if (!callback || owned.get(surface) !== callback)
+ return;
+ owned.delete(surface);
+ const registry = art[surface];
+ if (surface === "controls" && registry.cache?.get && registry.cache.get(name)?.option?.onSelect !== callback)
+ return;
+ if (surface === "setting" && registry.find && registry.find(name)?.onSelect !== callback)
+ return;
+ registry.remove(name);
+ }
+ function clear() {
+ current = void 0;
+ let failure;
+ for (const [surface, callback] of [...owned]) {
+ try {
+ remove(surface, callback);
+ } catch (error) {
+ failure || (failure = error);
+ }
+ }
+ if (failure)
+ throw failure;
+ }
+ function update(config, model, active) {
+ if (!model) {
+ clear();
+ return;
+ }
+ const state = {};
+ current = state;
+ const valid = () => state === current && active();
+ const onSelect = (item) => {
+ if (!valid())
+ return item.html;
+ model.select(item, valid);
+ if (!valid())
+ return item.html;
+ art.notice.show = `${model.title}: ${item.html}`;
+ if (valid() && config.control)
+ art.controls.check(item);
+ if (valid() && config.setting)
+ art.setting.check(item);
+ return item.html;
+ };
+ if (config.control) {
+ owned.set("controls", onSelect);
+ art.controls.update({ name, position: "right", html: model.html, style: { padding: "0 10px" }, selector: model.selector, onSelect });
+ } else {
+ remove("controls");
+ }
+ if (!valid())
+ return;
+ if (config.setting) {
+ owned.set("setting", onSelect);
+ art.setting.update({ name, tooltip: model.html, html: model.title, icon, width: 200, selector: model.selector, onSelect });
+ } else {
+ remove("setting");
+ }
+ }
+ return { update, clear };
+}
+const $quality = '';
function artplayerPluginDashControl(option = {}) {
return (art) => {
const { $video } = art.template;
const { errorHandle } = art.constructor.utils;
- function updateQuality(dash) {
- const qualities = dash.getRepresentationsByType("video");
- if (!qualities || !qualities.length)
- return;
- const config = option.quality || {};
- const auto = config.auto || "Auto";
- const title = config.title || "Quality";
- const getName = config.getName || ((level) => `${level.height}p`);
- const currentRepresentation = dash.getCurrentRepresentationForType("video");
- const currentId = currentRepresentation && currentRepresentation.id;
- const currentAuto = dash.getSettings().streaming.abr.autoSwitchBitrate.video;
- const defaultHtml = !currentAuto && currentRepresentation ? getName(currentRepresentation) : auto;
- const selector = uniqBy(
- qualities.map((item, index) => {
- return {
- html: getName(item),
- value: index,
- id: item.id,
- default: !currentAuto && !!currentId && item.id === currentId
- };
- }),
- "html"
- ).sort((a, b) => b.value - a.value);
- selector.push({
- html: auto,
- value: "auto",
- default: currentAuto
- });
- const onSelect = (item) => {
- if (item.value === "auto") {
- dash.updateSettings({
- streaming: {
- abr: {
- autoSwitchBitrate: {
- video: true
- }
- }
- }
- });
- } else {
- dash.updateSettings({
- streaming: {
- abr: {
- autoSwitchBitrate: {
- video: false
- }
- }
- }
- });
- dash.setRepresentationForTypeById("video", item.id);
+ let closed = false;
+ let revision = 0;
+ const quality = createMenu(art, "dash-quality", $quality);
+ const audio = createMenu(art, "dash-audio", $audio);
+ const subscriptions = [];
+ function clear(current = () => true) {
+ let failure;
+ for (const cleanup of [quality.clear, audio.clear]) {
+ if (!current())
+ break;
+ try {
+ cleanup();
+ } catch (error) {
+ failure || (failure = error);
}
- art.notice.show = `${title}: ${item.html}`;
- if (config.control)
- art.controls.check(item);
- if (config.setting)
- art.setting.check(item);
- return item.html;
- };
- if (config.control) {
- art.controls.update({
- name: "dash-quality",
- position: "right",
- html: defaultHtml,
- style: { padding: "0 10px" },
- selector,
- onSelect
- });
- }
- if (config.setting) {
- art.setting.update({
- name: "dash-quality",
- tooltip: defaultHtml,
- html: title,
- icon: $quality,
- width: 200,
- selector,
- onSelect
- });
- }
- }
- function updateAudio(dash) {
- const audioTracks = dash.getTracksFor("audio");
- if (!audioTracks || !audioTracks.length)
- return;
- const config = option.audio || {};
- const auto = config.auto || "Auto";
- const title = config.title || "Audio";
- const getName = config.getName || ((track) => track.lang || track.id);
- const currentTrack = dash.getCurrentTrackFor("audio") || audioTracks[0];
- const defaultHtml = currentTrack ? getName(currentTrack) : auto;
- const selector = uniqBy(
- audioTracks.map((item) => {
- return {
- html: getName(item),
- value: item,
- default: currentTrack === item
- };
- }),
- "html"
- );
- const onSelect = (item) => {
- dash.setCurrentTrack(item.value);
- art.notice.show = `${title}: ${item.html}`;
- if (config.control)
- art.controls.check(item);
- if (config.setting)
- art.setting.check(item);
- return item.html;
- };
- if (config.control) {
- art.controls.update({
- name: "dash-audio",
- position: "right",
- html: defaultHtml,
- style: { padding: "0 10px" },
- selector,
- onSelect
- });
- }
- if (config.setting) {
- art.setting.update({
- name: "dash-audio",
- tooltip: defaultHtml,
- html: title,
- icon: $audio,
- width: 200,
- selector,
- onSelect
- });
}
+ if (failure)
+ throw failure;
}
function update() {
- errorHandle(art.dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"');
- updateQuality(art.dash);
- updateAudio(art.dash);
+ if (closed || art.isDestroy)
+ return;
+ const version = ++revision;
+ const dash = art.dash;
+ const current = () => !closed && !art.isDestroy && version === revision && art.dash === dash;
+ const valid = () => current() && dash.getVideoElement() === $video && current();
+ try {
+ errorHandle(dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"');
+ if (!current())
+ return;
+ const qualityConfig = option.quality || {};
+ const qualities = qualityModel(dash, qualityConfig, valid);
+ if (!valid())
+ return;
+ quality.update(qualityConfig, qualities, valid);
+ if (!valid())
+ return;
+ const audioConfig = option.audio || {};
+ const tracks = audioModel(dash, audioConfig, valid);
+ if (valid())
+ audio.update(audioConfig, tracks, valid);
+ } catch (error) {
+ if (current()) {
+ const cleanupVersion = ++revision;
+ try {
+ clear(() => revision === cleanupVersion);
+ } catch (cleanupError) {
+ console.warn("ArtPlayer DASH cleanup failed:", cleanupError);
+ }
+ }
+ throw error;
+ }
}
- art.on("ready", update);
- art.on("restart", update);
- return {
- name: "artplayerPluginDashControl",
- update
- };
+ function destroy() {
+ if (closed)
+ return;
+ closed = true;
+ revision++;
+ let failure;
+ const actions = [clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))];
+ for (const action of actions) {
+ try {
+ action();
+ } catch (error) {
+ failure || (failure = error);
+ }
+ }
+ if (failure)
+ throw failure;
+ }
+ try {
+ for (const entry of [["ready", update], ["restart", update], ["destroy", destroy]]) {
+ if (closed)
+ break;
+ subscriptions.push(entry);
+ art.on(...entry);
+ }
+ } catch (error) {
+ try {
+ destroy();
+ } catch (cleanupError) {
+ console.warn("ArtPlayer DASH subscription cleanup failed:", cleanupError);
+ }
+ throw error;
+ }
+ return { name: "artplayerPluginDashControl", update };
};
}
export {
diff --git a/package.json b/package.json
index c893adf41..e32558ece 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
"test:playback": "node --test test/playback.test.js",
"trans:docs": "node ./scripts/trans-docs.js",
"create:plugin": "node ./scripts/plugin/create.js",
- "test:dash-control": "node --test test/dash-control.test.js test/dash-contract.test.js",
+ "test:dash-control": "node --test test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js",
"dev": "npx cross-env NODE_ENV=development node ./scripts/dev.js",
"build": "npx cross-env NODE_ENV=production node ./scripts/build.js",
"lint": "eslint \"packages/*/{src,public,types,package.json}\" \"scripts/*.{js,mjs}\" \"test/*\" \"docs/assets/ts/*\" \"types/*.d.ts\" \"playwright*.config.js\" --no-fix",
@@ -50,7 +50,7 @@
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
"typecheck": "node scripts/typecheck.mjs",
- "test:unit": "node --test test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
+ "test:unit": "node --test test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
"test": "yarn test:node && yarn test:baseline",
"test:browser": "playwright test",
diff --git a/packages/artplayer-plugin-dash-control/ARCHITECTURE.md b/packages/artplayer-plugin-dash-control/ARCHITECTURE.md
new file mode 100644
index 000000000..7e622ea0e
--- /dev/null
+++ b/packages/artplayer-plugin-dash-control/ARCHITECTURE.md
@@ -0,0 +1,80 @@
+# DASH Control implementation
+
+The factory remains `artplayerPluginDashControl(option = {})(art)`. Its result has
+`name` and synchronous `update(): void`. Installation does not require `art.dash`
+yet; `ready`, `restart`, or explicit `update()` reads and validates that instance.
+The caller creates, attaches, replaces, and destroys the SDK. This plugin never
+destroys it, changes its source, or adds a runtime dependency on dash.js.
+
+## Module map
+
+| File | Responsibility |
+| --- | --- |
+| `src/index.js` | Deferred installation, media identity, update revisions, event subscription and cleanup |
+| `src/sdk.js` | 4.x qualityIndex vs 5.x representation ID access and manual/Auto selection |
+| `src/mapping.js` | Names, current item matching, duplicate labels and selector ordering |
+| `src/menu.js` | Existing control/setting registries, menu ownership and guarded selection callbacks |
+| `src/audio.svg`, `src/quality.svg` | Existing setting icons |
+| `types/artplayer-plugin-dash-control.d.ts` | Existing public declarations; precise TS migration follows in PKG-DASH-04 |
+
+Dependencies flow from the entry into mapping/menu, and from mapping into the SDK
+adapter. Mapping has no DOM dependency. Menu receives a model and validity callback;
+it does not choose SDK versions. This is the structural step, still in JavaScript.
+
+## Selection and lifetime
+
+An update validates the captured media element and starts a new revision. A callback
+from an older update, replaced SDK, or destroyed player returns its previous label
+without mutating the SDK or UI. This includes references retained by consumers.
+Every caller formatter and SDK operation is followed by a validity check before
+subsequent effects. A nested update wins over its older caller.
+
+Manual quality first disables video ABR, then uses the stored SDK selection key.
+Auto only enables video ABR. Audio passes the original track object to the SDK.
+Successful selection retains the existing notice, controls.check, setting.check
+order and synchronous label return. SDK/getName exceptions remain synchronous with
+their original identity. Other ABR settings are not overwritten.
+
+Duplicate labels still occupy one row. When a duplicate is current, that row keeps
+the selected item's key/object so its highlight and subsequent click agree. Numeric
+zero is a valid representation ID; a representation named `auto` is still manual
+because the synthetic Auto row is distinguished by its value. Audio prefers object
+identity, then a unique match on the current track's available id/index/lang fields.
+Ambiguous matches remain unselected instead of selecting an arbitrary track.
+
+Only menus actually installed by this instance are removed on empty topology,
+disabled surfaces, failed rendering, or destruction. Where registry lookup exists,
+the saved callback identity protects an entry replaced by the caller. Cleanup takes
+a snapshot so a synchronous restart cannot cause old cleanup to remove new menus.
+Cleanup attempts all owned surfaces/subscriptions even if one removal fails.
+Installation failure rolls back subscriptions, including one installed by a throwing
+host. The primary error is preserved; secondary cleanup errors are reported.
+
+Keep `dash-quality`/`dash-audio`, selector shape, right control placement/padding,
+setting width/icons, defaults and unbound getName calls compatible. Do not use a
+filtered representation array index or absoluteIndex as the SDK 5 selection key.
+
+## Validation and next work
+
+Run at the workspace root with pinned Node and Yarn:
+
+```sh
+yarn test:dash-control
+yarn build artplayer-plugin-dash-control
+yarn test:browser test/browser/dash-control.spec.js --trace on
+yarn ci:check
+```
+
+`ARTPLAYER_TEST_DASH` adds main/legacy/module files to both SDK generations of Node
+contracts and lifecycle tests; use the operating system's path delimiter.
+`ARTPLAYER_DASH_ARTIFACT` selects the browser plugin artifact. Archive browser report
+and result/trace directory before running another suite because the output is shared.
+
+Tests use frozen npm 1.1.0, a frozen pre-refactor 5.x implementation, and candidates.
+Browser tests exercise real old/new core DOM, menu clicks, native MP4 and cleanup,
+with controlled SDK methods. They do not load dash.js or test MPD/ABR decoding.
+PKG-DASH-05 must verify pinned dash.js 4.5.2 and 5.2.1 with real adaptive media and
+review SDK-driven refresh/source events and the existing demo lifecycle. Physical
+devices and installed package acceptance remain separate release gates.
+See [refactor validation](../../refactor/dash-validation.md) for baseline failures
+and [task plan](../../refactor/plan.md) for remaining type and release work.
diff --git a/packages/artplayer-plugin-dash-control/README.md b/packages/artplayer-plugin-dash-control/README.md
index a7b9176bd..48f8175cb 100644
--- a/packages/artplayer-plugin-dash-control/README.md
+++ b/packages/artplayer-plugin-dash-control/README.md
@@ -2,7 +2,9 @@
Dash control plugin for ArtPlayer
-Requires dash.js 5.x. dash.js 4.x is not supported.
+Accepts a caller-owned dash.js instance at `art.dash`. Quality selection adapts to the
+dash.js 4.x quality methods or 5.x representation methods. Version 5 selection uses
+representation IDs so bitrate filtering does not change the selected quality.
## Demo
@@ -11,3 +13,8 @@ Requires dash.js 5.x. dash.js 4.x is not supported.
## License
MIT © Harvey Zhao
+
+## Maintenance
+
+See [ARCHITECTURE.md](ARCHITECTURE.md) for module ownership, compatibility rules,
+test commands and the remaining real SDK playback validation scope.
diff --git a/packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.js b/packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.js
index 97ce60f03..2c9950667 100644
--- a/packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.js
+++ b/packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.js
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
-!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";function t(t,e){const o=new Map;return t.filter(t=>{const n=t[e];return void 0===n||!o.has(n)&&o.set(n,1)})}return function(e={}){return o=>{const{$video:n}=o.template,{errorHandle:i}=o.constructor.utils;function a(){i(o.dash.getVideoElement()===n,'Cannot find instance of DASH from "art.dash"'),function(n){const i=n.getRepresentationsByType("video");if(!i||!i.length)return;const a=e.quality||{},l=a.auto||"Auto",s=a.title||"Quality",r=a.getName||(t=>`${t.height}p`),c=n.getCurrentRepresentationForType("video"),u=c&&c.id,d=n.getSettings().streaming.abr.autoSwitchBitrate.video,h=!d&&c?r(c):l,g=t(i.map((t,e)=>({html:r(t),value:e,id:t.id,default:!d&&!!u&&t.id===u})),"html").sort((t,e)=>e.value-t.value);g.push({html:l,value:"auto",default:d});const p=t=>("auto"===t.value?n.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!0}}}}):(n.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!1}}}}),n.setRepresentationForTypeById("video",t.id)),o.notice.show=`${s}: ${t.html}`,a.control&&o.controls.check(t),a.setting&&o.setting.check(t),t.html);a.control&&o.controls.update({name:"dash-quality",position:"right",html:h,style:{padding:"0 10px"},selector:g,onSelect:p}),a.setting&&o.setting.update({name:"dash-quality",tooltip:h,html:s,icon:'',width:200,selector:g,onSelect:p})}(o.dash),function(n){const i=n.getTracksFor("audio");if(!i||!i.length)return;const a=e.audio||{},l=a.auto||"Auto",s=a.title||"Audio",r=a.getName||(t=>t.lang||t.id),c=n.getCurrentTrackFor("audio")||i[0],u=c?r(c):l,d=t(i.map(t=>({html:r(t),value:t,default:c===t})),"html"),h=t=>(n.setCurrentTrack(t.value),o.notice.show=`${s}: ${t.html}`,a.control&&o.controls.check(t),a.setting&&o.setting.check(t),t.html);a.control&&o.controls.update({name:"dash-audio",position:"right",html:u,style:{padding:"0 10px"},selector:d,onSelect:h}),a.setting&&o.setting.update({name:"dash-audio",tooltip:u,html:s,icon:'',width:200,selector:d,onSelect:h})}(o.dash)}return o.on("ready",a),o.on("restart",a),{name:"artplayerPluginDashControl",update:a}}}});
+!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function e(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){if(!o||r.get(n)!==o)return;r.delete(n);const l=t[n];"controls"===n&&l.cache?.get&&l.cache.get(e)?.option?.onSelect!==o||"setting"===n&&l.find&&l.find(e)?.onSelect!==o||l.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,s,u){if(!s)return void i();const a={};o=a;const f=()=>a===o&&u(),d=e=>f()?(s.select(e,f),f()?(t.notice.show=`${s.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:s.html,style:{padding:"0 10px"},selector:s.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:s.html,html:s.title,icon:n,width:200,selector:s.selector,onSelect:d})):l("setting"))},clear:i}}return function(n={}){return o=>{const{$video:r}=o.template,{errorHandle:l}=o.constructor.utils;let i=!1,c=0;const s=e(o,"dash-quality",''),u=e(o,"dash-audio",''),a=[];function f(t=()=>!0){let e;for(const o of[s.clear,u.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function d(){if(i||o.isDestroy)return;const e=++c,a=o.dash,d=()=>!i&&!o.isDestroy&&e===c&&o.dash===a,h=()=>d()&&a.getVideoElement()===r&&d();try{if(l(a.getVideoElement()===r,'Cannot find instance of DASH from "art.dash"'),!d())return;const e=n.quality||{},o=function(e,n,o){const r=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(e),l=r.levels();if(!o()||!l?.length)return null;const i=n.auto||"Auto",c=n.getName||(t=>`${t.height}p`),s=r.current(l);if(!o())return null;const u=e.getSettings().streaming.abr.autoSwitchBitrate.video;if(!o())return null;const a=!u&&s?c(s):i;if(!o())return null;const f=[];for(const[t,h]of l.entries()){const e=c(h);if(!o())return null;f.push({html:e,...r.item(h,t,s,u)})}const d=t(f).sort((t,e)=>e.value-t.value);return d.push({html:i,value:"auto",default:u}),{html:a,title:n.title||"Quality",selector:d,select:r.select}}(a,e,h);if(!h())return;if(s.update(e,o,h),!h())return;const i=n.audio||{},c=function(e,n,o){const r=e.getTracksFor("audio");if(!o()||!r?.length)return null;const l=n.auto||"Auto",i=n.getName||(t=>t.lang||t.id),c=e.getCurrentTrackFor("audio")||r[0];if(!o())return null;const s=c?i(c):l;if(!o())return null;const u=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=t.filter(t=>["id","index","lang"].every(n=>null==e[n]||t[n]===e[n]));return 1===n.length?n[0]:void 0}(r,c),a=[];for(const t of r){const e=i(t);if(!o())return null;a.push({html:e,value:t,default:t===u})}const f=t(a);return{html:s,title:n.title||"Audio",selector:f,select:t=>e.setCurrentTrack(t.value)}}(a,i,h);h()&&u.update(i,c,h)}catch(g){if(d()){const t=++c;try{f(()=>c===t)}catch(p){console.warn("ArtPlayer DASH cleanup failed:",p)}}throw g}}function h(){if(i)return;let t;i=!0,c++;const e=[f,...a.splice(0).map(([t,e])=>()=>o.off(t,e))];for(const o of e)try{o()}catch(n){t||(t=n)}if(t)throw t}try{for(const t of[["ready",d],["restart",d],["destroy",h]]){if(i)break;a.push(t),o.on(...t)}}catch(g){try{h()}catch(p){console.warn("ArtPlayer DASH subscription cleanup failed:",p)}throw g}return{name:"artplayerPluginDashControl",update:d}}}});
diff --git a/packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.legacy.js b/packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.legacy.js
index 97ce60f03..a270906b6 100644
--- a/packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.legacy.js
+++ b/packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.legacy.js
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
-!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";function t(t,e){const o=new Map;return t.filter(t=>{const n=t[e];return void 0===n||!o.has(n)&&o.set(n,1)})}return function(e={}){return o=>{const{$video:n}=o.template,{errorHandle:i}=o.constructor.utils;function a(){i(o.dash.getVideoElement()===n,'Cannot find instance of DASH from "art.dash"'),function(n){const i=n.getRepresentationsByType("video");if(!i||!i.length)return;const a=e.quality||{},l=a.auto||"Auto",s=a.title||"Quality",r=a.getName||(t=>`${t.height}p`),c=n.getCurrentRepresentationForType("video"),u=c&&c.id,d=n.getSettings().streaming.abr.autoSwitchBitrate.video,h=!d&&c?r(c):l,g=t(i.map((t,e)=>({html:r(t),value:e,id:t.id,default:!d&&!!u&&t.id===u})),"html").sort((t,e)=>e.value-t.value);g.push({html:l,value:"auto",default:d});const p=t=>("auto"===t.value?n.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!0}}}}):(n.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!1}}}}),n.setRepresentationForTypeById("video",t.id)),o.notice.show=`${s}: ${t.html}`,a.control&&o.controls.check(t),a.setting&&o.setting.check(t),t.html);a.control&&o.controls.update({name:"dash-quality",position:"right",html:h,style:{padding:"0 10px"},selector:g,onSelect:p}),a.setting&&o.setting.update({name:"dash-quality",tooltip:h,html:s,icon:'',width:200,selector:g,onSelect:p})}(o.dash),function(n){const i=n.getTracksFor("audio");if(!i||!i.length)return;const a=e.audio||{},l=a.auto||"Auto",s=a.title||"Audio",r=a.getName||(t=>t.lang||t.id),c=n.getCurrentTrackFor("audio")||i[0],u=c?r(c):l,d=t(i.map(t=>({html:r(t),value:t,default:c===t})),"html"),h=t=>(n.setCurrentTrack(t.value),o.notice.show=`${s}: ${t.html}`,a.control&&o.controls.check(t),a.setting&&o.setting.check(t),t.html);a.control&&o.controls.update({name:"dash-audio",position:"right",html:u,style:{padding:"0 10px"},selector:d,onSelect:h}),a.setting&&o.setting.update({name:"dash-audio",tooltip:u,html:s,icon:'',width:200,selector:d,onSelect:h})}(o.dash)}return o.on("ready",a),o.on("restart",a),{name:"artplayerPluginDashControl",update:a}}}});
+!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,r=(e,n,o)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[n]=o,l=(t,l)=>{for(var i in l||(l={}))n.call(l,i)&&r(t,i,l[i]);if(e)for(var i of e(l))o.call(l,i)&&r(t,i,l[i]);return t};function i(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function c(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){var l,i,c,u;if(!o||r.get(n)!==o)return;r.delete(n);const s=t[n];"controls"===n&&(null==(l=s.cache)?void 0:l.get)&&(null==(c=null==(i=s.cache.get(e))?void 0:i.option)?void 0:c.onSelect)!==o||"setting"===n&&s.find&&(null==(u=s.find(e))?void 0:u.onSelect)!==o||s.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,u,s){if(!u)return void i();const a={};o=a;const f=()=>a===o&&s(),d=e=>f()?(u.select(e,f),f()?(t.notice.show=`${u.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:u.html,style:{padding:"0 10px"},selector:u.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:u.html,html:u.title,icon:n,width:200,selector:u.selector,onSelect:d})):l("setting"))},clear:i}}return function(t={}){return e=>{const{$video:n}=e.template,{errorHandle:o}=e.constructor.utils;let r=!1,u=0;const s=c(e,"dash-quality",''),a=c(e,"dash-audio",''),f=[];function d(t=()=>!0){let e;for(const o of[s.clear,a.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function h(){if(r||e.isDestroy)return;const c=++u,f=e.dash,h=()=>!r&&!e.isDestroy&&c===u&&e.dash===f,p=()=>h()&&f.getVideoElement()===n&&h();try{if(o(f.getVideoElement()===n,'Cannot find instance of DASH from "art.dash"'),!h())return;const e=t.quality||{},r=function(t,e,n){const o=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(t),r=o.levels();if(!n()||!(null==r?void 0:r.length))return null;const c=e.auto||"Auto",u=e.getName||(t=>`${t.height}p`),s=o.current(r);if(!n())return null;const a=t.getSettings().streaming.abr.autoSwitchBitrate.video;if(!n())return null;const f=!a&&s?u(s):c;if(!n())return null;const d=[];for(const[i,p]of r.entries()){const t=u(p);if(!n())return null;d.push(l({html:t},o.item(p,i,s,a)))}const h=i(d).sort((t,e)=>e.value-t.value);return h.push({html:c,value:"auto",default:a}),{html:f,title:e.title||"Quality",selector:h,select:o.select}}(f,e,p);if(!p())return;if(s.update(e,r,p),!p())return;const c=t.audio||{},u=function(t,e,n){const o=t.getTracksFor("audio");if(!n()||!(null==o?void 0:o.length))return null;const r=e.auto||"Auto",l=e.getName||(t=>t.lang||t.id),c=t.getCurrentTrackFor("audio")||o[0];if(!n())return null;const u=c?l(c):r;if(!n())return null;const s=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=t.filter(t=>["id","index","lang"].every(n=>null==e[n]||t[n]===e[n]));return 1===n.length?n[0]:void 0}(o,c),a=[];for(const i of o){const t=l(i);if(!n())return null;a.push({html:t,value:i,default:i===s})}const f=i(a);return{html:u,title:e.title||"Audio",selector:f,select:e=>t.setCurrentTrack(e.value)}}(f,c,p);p()&&a.update(c,u,p)}catch(g){if(h()){const t=++u;try{d(()=>u===t)}catch(v){console.warn("ArtPlayer DASH cleanup failed:",v)}}throw g}}function p(){if(r)return;let t;r=!0,u++;const n=[d,...f.splice(0).map(([t,n])=>()=>e.off(t,n))];for(const e of n)try{e()}catch(o){t||(t=o)}if(t)throw t}try{for(const t of[["ready",h],["restart",h],["destroy",p]]){if(r)break;f.push(t),e.on(...t)}}catch(g){try{p()}catch(v){console.warn("ArtPlayer DASH subscription cleanup failed:",v)}throw g}return{name:"artplayerPluginDashControl",update:h}}}});
diff --git a/packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.mjs b/packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.mjs
index 4ce15acf9..a0c85dbe8 100644
--- a/packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.mjs
+++ b/packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.mjs
@@ -5,163 +5,260 @@
* Released under the MIT License.
*/
const $audio = '';
-const $quality = '';
-function uniqBy(array, property) {
- const seen = /* @__PURE__ */ new Map();
- return array.filter((item) => {
- const key = item[property];
- if (key === void 0) {
- return true;
+function qualityAdapter(dash) {
+ const modern = typeof dash.getRepresentationsByType === "function";
+ return {
+ levels: () => modern ? dash.getRepresentationsByType("video") : dash.getBitrateInfoListFor("video"),
+ current(levels) {
+ if (modern)
+ return dash.getCurrentRepresentationForType("video");
+ const quality = dash.getQualityFor("video");
+ return levels.find((level) => level.qualityIndex === quality);
+ },
+ item(level, index, selected, automatic) {
+ return modern ? { value: index, id: level.id, default: !automatic && selected != null && level.id === selected.id } : { value: level.qualityIndex, default: !automatic && selected != null && level.qualityIndex === selected.qualityIndex };
+ },
+ select(item, valid) {
+ const automatic = item.value === "auto";
+ dash.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: automatic } } } });
+ if (!automatic && valid()) {
+ if (modern)
+ dash.setRepresentationForTypeById("video", item.id);
+ else
+ dash.setQualityFor("video", item.value);
+ }
}
- return !seen.has(key) && seen.set(key, 1);
+ };
+}
+function uniqueLabels(items) {
+ const seen = /* @__PURE__ */ new Map();
+ return items.filter((item) => {
+ if (item.html === void 0)
+ return true;
+ const first = seen.get(item.html);
+ if (first) {
+ if (item.default)
+ Object.assign(first, item);
+ return false;
+ }
+ seen.set(item.html, item);
+ return true;
});
}
+function qualityModel(dash, config, active) {
+ const adapter = qualityAdapter(dash);
+ const levels = adapter.levels();
+ if (!active() || !levels?.length)
+ return null;
+ const auto = config.auto || "Auto";
+ const getName = config.getName || ((level) => `${level.height}p`);
+ const selected = adapter.current(levels);
+ if (!active())
+ return null;
+ const automatic = dash.getSettings().streaming.abr.autoSwitchBitrate.video;
+ if (!active())
+ return null;
+ const html = !automatic && selected ? getName(selected) : auto;
+ if (!active())
+ return null;
+ const items = [];
+ for (const [index, level] of levels.entries()) {
+ const label = getName(level);
+ if (!active())
+ return null;
+ items.push({ html: label, ...adapter.item(level, index, selected, automatic) });
+ }
+ const selector = uniqueLabels(items).sort((left, right) => right.value - left.value);
+ selector.push({ html: auto, value: "auto", default: automatic });
+ return { html, title: config.title || "Quality", selector, select: adapter.select };
+}
+function selectedTrack(tracks, current) {
+ if (tracks.includes(current))
+ return current;
+ if (current.id == null && current.index == null)
+ return void 0;
+ const matching = tracks.filter((track) => ["id", "index", "lang"].every((key) => current[key] == null || track[key] === current[key]));
+ return matching.length === 1 ? matching[0] : void 0;
+}
+function audioModel(dash, config, active) {
+ const tracks = dash.getTracksFor("audio");
+ if (!active() || !tracks?.length)
+ return null;
+ const auto = config.auto || "Auto";
+ const getName = config.getName || ((track) => track.lang || track.id);
+ const current = dash.getCurrentTrackFor("audio") || tracks[0];
+ if (!active())
+ return null;
+ const html = current ? getName(current) : auto;
+ if (!active())
+ return null;
+ const selected = selectedTrack(tracks, current);
+ const items = [];
+ for (const track of tracks) {
+ const label = getName(track);
+ if (!active())
+ return null;
+ items.push({ html: label, value: track, default: track === selected });
+ }
+ const selector = uniqueLabels(items);
+ return { html, title: config.title || "Audio", selector, select: (item) => dash.setCurrentTrack(item.value) };
+}
+function createMenu(art, name, icon) {
+ let current;
+ const owned = /* @__PURE__ */ new Map();
+ function remove(surface, callback = owned.get(surface)) {
+ if (!callback || owned.get(surface) !== callback)
+ return;
+ owned.delete(surface);
+ const registry = art[surface];
+ if (surface === "controls" && registry.cache?.get && registry.cache.get(name)?.option?.onSelect !== callback)
+ return;
+ if (surface === "setting" && registry.find && registry.find(name)?.onSelect !== callback)
+ return;
+ registry.remove(name);
+ }
+ function clear() {
+ current = void 0;
+ let failure;
+ for (const [surface, callback] of [...owned]) {
+ try {
+ remove(surface, callback);
+ } catch (error) {
+ failure || (failure = error);
+ }
+ }
+ if (failure)
+ throw failure;
+ }
+ function update(config, model, active) {
+ if (!model) {
+ clear();
+ return;
+ }
+ const state = {};
+ current = state;
+ const valid = () => state === current && active();
+ const onSelect = (item) => {
+ if (!valid())
+ return item.html;
+ model.select(item, valid);
+ if (!valid())
+ return item.html;
+ art.notice.show = `${model.title}: ${item.html}`;
+ if (valid() && config.control)
+ art.controls.check(item);
+ if (valid() && config.setting)
+ art.setting.check(item);
+ return item.html;
+ };
+ if (config.control) {
+ owned.set("controls", onSelect);
+ art.controls.update({ name, position: "right", html: model.html, style: { padding: "0 10px" }, selector: model.selector, onSelect });
+ } else {
+ remove("controls");
+ }
+ if (!valid())
+ return;
+ if (config.setting) {
+ owned.set("setting", onSelect);
+ art.setting.update({ name, tooltip: model.html, html: model.title, icon, width: 200, selector: model.selector, onSelect });
+ } else {
+ remove("setting");
+ }
+ }
+ return { update, clear };
+}
+const $quality = '';
function artplayerPluginDashControl(option = {}) {
return (art) => {
const { $video } = art.template;
const { errorHandle } = art.constructor.utils;
- function updateQuality(dash) {
- const qualities = dash.getRepresentationsByType("video");
- if (!qualities || !qualities.length)
- return;
- const config = option.quality || {};
- const auto = config.auto || "Auto";
- const title = config.title || "Quality";
- const getName = config.getName || ((level) => `${level.height}p`);
- const currentRepresentation = dash.getCurrentRepresentationForType("video");
- const currentId = currentRepresentation && currentRepresentation.id;
- const currentAuto = dash.getSettings().streaming.abr.autoSwitchBitrate.video;
- const defaultHtml = !currentAuto && currentRepresentation ? getName(currentRepresentation) : auto;
- const selector = uniqBy(
- qualities.map((item, index) => {
- return {
- html: getName(item),
- value: index,
- id: item.id,
- default: !currentAuto && !!currentId && item.id === currentId
- };
- }),
- "html"
- ).sort((a, b) => b.value - a.value);
- selector.push({
- html: auto,
- value: "auto",
- default: currentAuto
- });
- const onSelect = (item) => {
- if (item.value === "auto") {
- dash.updateSettings({
- streaming: {
- abr: {
- autoSwitchBitrate: {
- video: true
- }
- }
- }
- });
- } else {
- dash.updateSettings({
- streaming: {
- abr: {
- autoSwitchBitrate: {
- video: false
- }
- }
- }
- });
- dash.setRepresentationForTypeById("video", item.id);
+ let closed = false;
+ let revision = 0;
+ const quality = createMenu(art, "dash-quality", $quality);
+ const audio = createMenu(art, "dash-audio", $audio);
+ const subscriptions = [];
+ function clear(current = () => true) {
+ let failure;
+ for (const cleanup of [quality.clear, audio.clear]) {
+ if (!current())
+ break;
+ try {
+ cleanup();
+ } catch (error) {
+ failure || (failure = error);
}
- art.notice.show = `${title}: ${item.html}`;
- if (config.control)
- art.controls.check(item);
- if (config.setting)
- art.setting.check(item);
- return item.html;
- };
- if (config.control) {
- art.controls.update({
- name: "dash-quality",
- position: "right",
- html: defaultHtml,
- style: { padding: "0 10px" },
- selector,
- onSelect
- });
- }
- if (config.setting) {
- art.setting.update({
- name: "dash-quality",
- tooltip: defaultHtml,
- html: title,
- icon: $quality,
- width: 200,
- selector,
- onSelect
- });
- }
- }
- function updateAudio(dash) {
- const audioTracks = dash.getTracksFor("audio");
- if (!audioTracks || !audioTracks.length)
- return;
- const config = option.audio || {};
- const auto = config.auto || "Auto";
- const title = config.title || "Audio";
- const getName = config.getName || ((track) => track.lang || track.id);
- const currentTrack = dash.getCurrentTrackFor("audio") || audioTracks[0];
- const defaultHtml = currentTrack ? getName(currentTrack) : auto;
- const selector = uniqBy(
- audioTracks.map((item) => {
- return {
- html: getName(item),
- value: item,
- default: currentTrack === item
- };
- }),
- "html"
- );
- const onSelect = (item) => {
- dash.setCurrentTrack(item.value);
- art.notice.show = `${title}: ${item.html}`;
- if (config.control)
- art.controls.check(item);
- if (config.setting)
- art.setting.check(item);
- return item.html;
- };
- if (config.control) {
- art.controls.update({
- name: "dash-audio",
- position: "right",
- html: defaultHtml,
- style: { padding: "0 10px" },
- selector,
- onSelect
- });
- }
- if (config.setting) {
- art.setting.update({
- name: "dash-audio",
- tooltip: defaultHtml,
- html: title,
- icon: $audio,
- width: 200,
- selector,
- onSelect
- });
}
+ if (failure)
+ throw failure;
}
function update() {
- errorHandle(art.dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"');
- updateQuality(art.dash);
- updateAudio(art.dash);
+ if (closed || art.isDestroy)
+ return;
+ const version = ++revision;
+ const dash = art.dash;
+ const current = () => !closed && !art.isDestroy && version === revision && art.dash === dash;
+ const valid = () => current() && dash.getVideoElement() === $video && current();
+ try {
+ errorHandle(dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"');
+ if (!current())
+ return;
+ const qualityConfig = option.quality || {};
+ const qualities = qualityModel(dash, qualityConfig, valid);
+ if (!valid())
+ return;
+ quality.update(qualityConfig, qualities, valid);
+ if (!valid())
+ return;
+ const audioConfig = option.audio || {};
+ const tracks = audioModel(dash, audioConfig, valid);
+ if (valid())
+ audio.update(audioConfig, tracks, valid);
+ } catch (error) {
+ if (current()) {
+ const cleanupVersion = ++revision;
+ try {
+ clear(() => revision === cleanupVersion);
+ } catch (cleanupError) {
+ console.warn("ArtPlayer DASH cleanup failed:", cleanupError);
+ }
+ }
+ throw error;
+ }
}
- art.on("ready", update);
- art.on("restart", update);
- return {
- name: "artplayerPluginDashControl",
- update
- };
+ function destroy() {
+ if (closed)
+ return;
+ closed = true;
+ revision++;
+ let failure;
+ const actions = [clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))];
+ for (const action of actions) {
+ try {
+ action();
+ } catch (error) {
+ failure || (failure = error);
+ }
+ }
+ if (failure)
+ throw failure;
+ }
+ try {
+ for (const entry of [["ready", update], ["restart", update], ["destroy", destroy]]) {
+ if (closed)
+ break;
+ subscriptions.push(entry);
+ art.on(...entry);
+ }
+ } catch (error) {
+ try {
+ destroy();
+ } catch (cleanupError) {
+ console.warn("ArtPlayer DASH subscription cleanup failed:", cleanupError);
+ }
+ throw error;
+ }
+ return { name: "artplayerPluginDashControl", update };
};
}
export {
diff --git a/packages/artplayer-plugin-dash-control/src/index.js b/packages/artplayer-plugin-dash-control/src/index.js
index 04de6a6d5..897fb28a0 100644
--- a/packages/artplayer-plugin-dash-control/src/index.js
+++ b/packages/artplayer-plugin-dash-control/src/index.js
@@ -1,179 +1,107 @@
import $audio from './audio.svg?raw'
+import { audioModel, qualityModel } from './mapping'
+import { createMenu } from './menu'
import $quality from './quality.svg?raw'
-function uniqBy(array, property) {
- const seen = new Map()
- return array.filter((item) => {
- const key = item[property]
- if (key === undefined) {
- return true
- }
- return !seen.has(key) && seen.set(key, 1)
- })
-}
-
export default function artplayerPluginDashControl(option = {}) {
return (art) => {
const { $video } = art.template
const { errorHandle } = art.constructor.utils
+ let closed = false
+ let revision = 0
+ const quality = createMenu(art, 'dash-quality', $quality)
+ const audio = createMenu(art, 'dash-audio', $audio)
+ const subscriptions = []
- function updateQuality(dash) {
- const qualities = dash.getRepresentationsByType('video')
- if (!qualities || !qualities.length)
- return
-
- const config = option.quality || {}
- const auto = config.auto || 'Auto'
- const title = config.title || 'Quality'
- const getName = config.getName || (level => `${level.height}p`)
- const currentRepresentation = dash.getCurrentRepresentationForType('video')
- const currentId = currentRepresentation && currentRepresentation.id
- const currentAuto = dash.getSettings().streaming.abr.autoSwitchBitrate.video
- const defaultHtml = !currentAuto && currentRepresentation ? getName(currentRepresentation) : auto
-
- const selector = uniqBy(
- qualities.map((item, index) => {
- return {
- html: getName(item),
- value: index,
- id: item.id,
- default: !currentAuto && !!currentId && item.id === currentId,
- }
- }),
- 'html',
- ).sort((a, b) => b.value - a.value)
-
- selector.push({
- html: auto,
- value: 'auto',
- default: currentAuto,
- })
-
- const onSelect = (item) => {
- if (item.value === 'auto') {
- dash.updateSettings({
- streaming: {
- abr: {
- autoSwitchBitrate: {
- video: true,
- },
- },
- },
- })
+ function clear(current = () => true) {
+ let failure
+ for (const cleanup of [quality.clear, audio.clear]) {
+ if (!current())
+ break
+ try {
+ cleanup()
}
- else {
- dash.updateSettings({
- streaming: {
- abr: {
- autoSwitchBitrate: {
- video: false,
- },
- },
- },
- })
- dash.setRepresentationForTypeById('video', item.id)
+ catch (error) {
+ failure ||= error
}
- art.notice.show = `${title}: ${item.html}`
- if (config.control)
- art.controls.check(item)
- if (config.setting)
- art.setting.check(item)
- return item.html
- }
-
- if (config.control) {
- art.controls.update({
- name: 'dash-quality',
- position: 'right',
- html: defaultHtml,
- style: { padding: '0 10px' },
- selector,
- onSelect,
- })
- }
-
- if (config.setting) {
- art.setting.update({
- name: 'dash-quality',
- tooltip: defaultHtml,
- html: title,
- icon: $quality,
- width: 200,
- selector,
- onSelect,
- })
- }
- }
-
- function updateAudio(dash) {
- const audioTracks = dash.getTracksFor('audio')
- if (!audioTracks || !audioTracks.length)
- return
-
- const config = option.audio || {}
- const auto = config.auto || 'Auto'
- const title = config.title || 'Audio'
- const getName = config.getName || (track => track.lang || track.id)
- const currentTrack = dash.getCurrentTrackFor('audio') || audioTracks[0]
- const defaultHtml = currentTrack ? getName(currentTrack) : auto
-
- const selector = uniqBy(
- audioTracks.map((item) => {
- return {
- html: getName(item),
- value: item,
- default: currentTrack === item,
- }
- }),
- 'html',
- )
-
- const onSelect = (item) => {
- dash.setCurrentTrack(item.value)
- art.notice.show = `${title}: ${item.html}`
- if (config.control)
- art.controls.check(item)
- if (config.setting)
- art.setting.check(item)
- return item.html
- }
-
- if (config.control) {
- art.controls.update({
- name: 'dash-audio',
- position: 'right',
- html: defaultHtml,
- style: { padding: '0 10px' },
- selector,
- onSelect,
- })
- }
-
- if (config.setting) {
- art.setting.update({
- name: 'dash-audio',
- tooltip: defaultHtml,
- html: title,
- icon: $audio,
- width: 200,
- selector,
- onSelect,
- })
}
+ if (failure)
+ throw failure
}
function update() {
- errorHandle(art.dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"')
- updateQuality(art.dash)
- updateAudio(art.dash)
+ if (closed || art.isDestroy)
+ return
+ const version = ++revision
+ const dash = art.dash
+ const current = () => !closed && !art.isDestroy && version === revision && art.dash === dash
+ const valid = () => current() && dash.getVideoElement() === $video && current()
+ try {
+ errorHandle(dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"')
+ if (!current())
+ return
+ const qualityConfig = option.quality || {}
+ const qualities = qualityModel(dash, qualityConfig, valid)
+ if (!valid())
+ return
+ quality.update(qualityConfig, qualities, valid)
+ if (!valid())
+ return
+ const audioConfig = option.audio || {}
+ const tracks = audioModel(dash, audioConfig, valid)
+ if (valid())
+ audio.update(audioConfig, tracks, valid)
+ }
+ catch (error) {
+ if (current()) {
+ const cleanupVersion = ++revision
+ try {
+ clear(() => revision === cleanupVersion)
+ }
+ catch (cleanupError) {
+ console.warn('ArtPlayer DASH cleanup failed:', cleanupError)
+ }
+ }
+ throw error
+ }
}
- art.on('ready', update)
- art.on('restart', update)
-
- return {
- name: 'artplayerPluginDashControl',
- update,
+ function destroy() {
+ if (closed)
+ return
+ closed = true
+ revision++
+ let failure
+ const actions = [clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))]
+ for (const action of actions) {
+ try {
+ action()
+ }
+ catch (error) {
+ failure ||= error
+ }
+ }
+ if (failure)
+ throw failure
}
+
+ try {
+ for (const entry of [['ready', update], ['restart', update], ['destroy', destroy]]) {
+ if (closed)
+ break
+ subscriptions.push(entry)
+ art.on(...entry)
+ }
+ }
+ catch (error) {
+ try {
+ destroy()
+ }
+ catch (cleanupError) {
+ console.warn('ArtPlayer DASH subscription cleanup failed:', cleanupError)
+ }
+ throw error
+ }
+ return { name: 'artplayerPluginDashControl', update }
}
}
diff --git a/packages/artplayer-plugin-dash-control/src/mapping.js b/packages/artplayer-plugin-dash-control/src/mapping.js
new file mode 100644
index 000000000..88b4b71dd
--- /dev/null
+++ b/packages/artplayer-plugin-dash-control/src/mapping.js
@@ -0,0 +1,79 @@
+import { qualityAdapter } from './sdk'
+
+function uniqueLabels(items) {
+ const seen = new Map()
+ return items.filter((item) => {
+ if (item.html === undefined)
+ return true
+ const first = seen.get(item.html)
+ if (first) {
+ if (item.default)
+ Object.assign(first, item)
+ return false
+ }
+ seen.set(item.html, item)
+ return true
+ })
+}
+
+export function qualityModel(dash, config, active) {
+ const adapter = qualityAdapter(dash)
+ const levels = adapter.levels()
+ if (!active() || !levels?.length)
+ return null
+ const auto = config.auto || 'Auto'
+ const getName = config.getName || (level => `${level.height}p`)
+ const selected = adapter.current(levels)
+ if (!active())
+ return null
+ const automatic = dash.getSettings().streaming.abr.autoSwitchBitrate.video
+ if (!active())
+ return null
+ const html = !automatic && selected ? getName(selected) : auto
+ if (!active())
+ return null
+ const items = []
+ for (const [index, level] of levels.entries()) {
+ const label = getName(level)
+ if (!active())
+ return null
+ items.push({ html: label, ...adapter.item(level, index, selected, automatic) })
+ }
+ const selector = uniqueLabels(items)
+ .sort((left, right) => right.value - left.value)
+ selector.push({ html: auto, value: 'auto', default: automatic })
+ return { html, title: config.title || 'Quality', selector, select: adapter.select }
+}
+
+function selectedTrack(tracks, current) {
+ if (tracks.includes(current))
+ return current
+ if (current.id == null && current.index == null)
+ return undefined
+ const matching = tracks.filter(track => ['id', 'index', 'lang'].every(key => current[key] == null || track[key] === current[key]))
+ return matching.length === 1 ? matching[0] : undefined
+}
+
+export function audioModel(dash, config, active) {
+ const tracks = dash.getTracksFor('audio')
+ if (!active() || !tracks?.length)
+ return null
+ const auto = config.auto || 'Auto'
+ const getName = config.getName || (track => track.lang || track.id)
+ const current = dash.getCurrentTrackFor('audio') || tracks[0]
+ if (!active())
+ return null
+ const html = current ? getName(current) : auto
+ if (!active())
+ return null
+ const selected = selectedTrack(tracks, current)
+ const items = []
+ for (const track of tracks) {
+ const label = getName(track)
+ if (!active())
+ return null
+ items.push({ html: label, value: track, default: track === selected })
+ }
+ const selector = uniqueLabels(items)
+ return { html, title: config.title || 'Audio', selector, select: item => dash.setCurrentTrack(item.value) }
+}
diff --git a/packages/artplayer-plugin-dash-control/src/menu.js b/packages/artplayer-plugin-dash-control/src/menu.js
new file mode 100644
index 000000000..447709a90
--- /dev/null
+++ b/packages/artplayer-plugin-dash-control/src/menu.js
@@ -0,0 +1,72 @@
+export function createMenu(art, name, icon) {
+ let current
+ const owned = new Map()
+
+ function remove(surface, callback = owned.get(surface)) {
+ if (!callback || owned.get(surface) !== callback)
+ return
+ owned.delete(surface)
+ const registry = art[surface]
+ if (surface === 'controls' && registry.cache?.get && registry.cache.get(name)?.option?.onSelect !== callback)
+ return
+ if (surface === 'setting' && registry.find && registry.find(name)?.onSelect !== callback)
+ return
+ registry.remove(name)
+ }
+
+ function clear() {
+ current = undefined
+ let failure
+ for (const [surface, callback] of [...owned]) {
+ try {
+ remove(surface, callback)
+ }
+ catch (error) {
+ failure ||= error
+ }
+ }
+ if (failure)
+ throw failure
+ }
+
+ function update(config, model, active) {
+ if (!model) {
+ clear()
+ return
+ }
+ const state = {}
+ current = state
+ const valid = () => state === current && active()
+ const onSelect = (item) => {
+ if (!valid())
+ return item.html
+ model.select(item, valid)
+ if (!valid())
+ return item.html
+ art.notice.show = `${model.title}: ${item.html}`
+ if (valid() && config.control)
+ art.controls.check(item)
+ if (valid() && config.setting)
+ art.setting.check(item)
+ return item.html
+ }
+ if (config.control) {
+ owned.set('controls', onSelect)
+ art.controls.update({ name, position: 'right', html: model.html, style: { padding: '0 10px' }, selector: model.selector, onSelect })
+ }
+ else {
+ remove('controls')
+ }
+ if (!valid())
+ return
+ if (config.setting) {
+ owned.set('setting', onSelect)
+ art.setting.update({ name, tooltip: model.html, html: model.title, icon, width: 200, selector: model.selector, onSelect })
+ }
+ else {
+ remove('setting')
+ }
+ }
+
+ return { update, clear }
+}
diff --git a/packages/artplayer-plugin-dash-control/src/sdk.js b/packages/artplayer-plugin-dash-control/src/sdk.js
new file mode 100644
index 000000000..49dfc0a6c
--- /dev/null
+++ b/packages/artplayer-plugin-dash-control/src/sdk.js
@@ -0,0 +1,27 @@
+export function qualityAdapter(dash) {
+ const modern = typeof dash.getRepresentationsByType === 'function'
+ return {
+ levels: () => modern ? dash.getRepresentationsByType('video') : dash.getBitrateInfoListFor('video'),
+ current(levels) {
+ if (modern)
+ return dash.getCurrentRepresentationForType('video')
+ const quality = dash.getQualityFor('video')
+ return levels.find(level => level.qualityIndex === quality)
+ },
+ item(level, index, selected, automatic) {
+ return modern
+ ? { value: index, id: level.id, default: !automatic && selected != null && level.id === selected.id }
+ : { value: level.qualityIndex, default: !automatic && selected != null && level.qualityIndex === selected.qualityIndex }
+ },
+ select(item, valid) {
+ const automatic = item.value === 'auto'
+ dash.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: automatic } } } })
+ if (!automatic && valid()) {
+ if (modern)
+ dash.setRepresentationForTypeById('video', item.id)
+ else
+ dash.setQualityFor('video', item.value)
+ }
+ },
+ }
+}
diff --git a/refactor/baselines/dash-runtime.json b/refactor/baselines/dash-runtime.json
new file mode 100644
index 000000000..0ea94c0e3
--- /dev/null
+++ b/refactor/baselines/dash-runtime.json
@@ -0,0 +1,854 @@
+{
+ "schemaVersion": 1,
+ "task": "PKG-DASH-03",
+ "baseCommit": "762cc634",
+ "scope": "Runtime structure and compatible SDK call surfaces. Real old/new core DOM with formal plugin main/legacy and native MP4. SDK is controlled; actual dash.js, MPD/ABR, physical devices and installed tarball acceptance remain outstanding.",
+ "inputs": [
+ {
+ "file": "packages/artplayer-plugin-dash-control/src/index.js",
+ "sha256LF": "3d82930fa8da9f729235ed34e5b9ae732bde6a986124b0ca1016eabefae4fc70"
+ },
+ {
+ "file": "packages/artplayer-plugin-dash-control/src/sdk.js",
+ "sha256LF": "90f77e57fd9d8cae3c7876166b048285d18634a93eecfcf50a70f8d6c9da1050"
+ },
+ {
+ "file": "packages/artplayer-plugin-dash-control/src/mapping.js",
+ "sha256LF": "03c1b99e0869263778f05896b05d2fe4004cf657fd5168cb303926a90de35b34"
+ },
+ {
+ "file": "packages/artplayer-plugin-dash-control/src/menu.js",
+ "sha256LF": "e852cbdf07bd681594c95ee368834995d34c6d1314d06c1c6089a025b1d7b896"
+ },
+ {
+ "file": "test/helpers/dash-control.js",
+ "sha256LF": "a32852c960a25c27ae18a6910f2b6d9e4f5786c1339db91f414b70a1fec041db"
+ },
+ {
+ "file": "test/dash-contract.test.js",
+ "sha256LF": "4477b48fc53224890a7eb22ed5f1554b57fd6265c864fb4039393978e63ceb31"
+ },
+ {
+ "file": "test/dash-lifecycle.test.js",
+ "sha256LF": "1374d51d7952c44a63580a6b127bc53f626ba161a03982414299d807cfc1525c"
+ },
+ {
+ "file": "test/browser/dash-control.spec.js",
+ "sha256LF": "a770394af0f3291925734e116ab53f8c090349673aa14205bd9a1300c82ad154"
+ }
+ ],
+ "artifacts": [
+ {
+ "file": "packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.js",
+ "sha256": "c1c63bd0ec787a6f9eb445b53341dd5eaeaccccd1589455234be311fcf6ce5e6",
+ "bytes": 5469
+ },
+ {
+ "file": "packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.legacy.js",
+ "sha256": "1b6fba04c90ba19197213cf1868599dab13d430e8923d3081e37c43dc4d5d00e",
+ "bytes": 5924
+ },
+ {
+ "file": "packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.mjs",
+ "sha256": "84b50383a8d6e75ad27dad6b550826b0d690aaaec9b2831a65111ec4f4c277b0",
+ "bytes": 9724
+ }
+ ],
+ "reports": [
+ {
+ "file": "refactor/.cache/dash03-main-first/report.json",
+ "sha256": "b17971ffd22f20acd68b64bb4ba7bff62b65c6fcc9ce10836b1f16f2d1bc9bd3",
+ "bytes": 15822733,
+ "stats": {
+ "startTime": "2026-09-11T20:51:00.164Z",
+ "duration": 64825.979,
+ "expected": 84,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "results": {
+ "directory": "refactor/.cache/dash03-main-first/results",
+ "files": 85,
+ "sha256": "a92c517dd7be719052b8766b3d19c00619469c0da792a9b2eead751048893594",
+ "algorithm": "SHA256 of JSON.stringify(sorted relative path and raw SHA256 pairs), UTF-8, slash paths"
+ },
+ "pluginInput": {
+ "published": {
+ "name": "artplayer-plugin-dash-control",
+ "version": "1.1.0",
+ "metadataUrl": "https://registry.npmjs.org/artplayer-plugin-dash-control/1.1.0",
+ "tarball": "https://registry.npmjs.org/artplayer-plugin-dash-control/-/artplayer-plugin-dash-control-1.1.0.tgz",
+ "integrity": "sha512-EssRkzYXWcln3a6nAzi4ESacBhzMih8s0bzdjXuvBBCh2UFmvK99ITD5E+0cuU8i5ZzIQGQSdce1/gANTrX0BQ==",
+ "sha256": "f4047a569d99b6a7de57b3719b7ad40109f2a1d360566a840f50ab32aacefcd9",
+ "archiveBytes": 3385,
+ "registryGitHead": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
+ "files": {
+ "package/README.md": "98982f1123ff9a11f5d3d9a52da6e8fea9cd0989f7465605c27b0f9a46f2dbcd",
+ "package/dist/artplayer-plugin-dash-control.js": "f0cbbcf8badeaca13c05ae178932818cb52fbce37ed504ac0e62160b9c7e1145",
+ "package/dist/artplayer-plugin-dash-control.legacy.js": "f0cbbcf8badeaca13c05ae178932818cb52fbce37ed504ac0e62160b9c7e1145",
+ "package/dist/artplayer-plugin-dash-control.mjs": "e32bf08c33a481781883067dcc639f0495c9dcb8a88b91be13736370f4932fc4",
+ "package/package.json": "556e2adc479bab36c5973de617c0b57c5d86aa8425643a1952ab04738e9ce927",
+ "package/types/artplayer-plugin-dash-control.d.ts": "e15ef683e0c335d81531587828593325e037efe7ed29c022477cd14fe71aebba"
+ },
+ "manifest": {
+ "name": "artplayer-plugin-dash-control",
+ "version": "1.1.0",
+ "description": "Dash control plugin for ArtPlayer",
+ "author": "Harvey Zhao ",
+ "license": "MIT",
+ "homepage": "https://artplayer.org",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/zhw2590582/ArtPlayer.git"
+ },
+ "bugs": {
+ "url": "https://github.com/zhw2590582/ArtPlayer/issues"
+ },
+ "keywords": [
+ "html5",
+ "video",
+ "player"
+ ],
+ "exports": {
+ ".": {
+ "types": "./types/artplayer-plugin-dash-control.d.ts",
+ "import": "./dist/artplayer-plugin-dash-control.mjs",
+ "require": "./dist/artplayer-plugin-dash-control.js"
+ },
+ "./legacy": {
+ "types": "./types/artplayer-plugin-dash-control.d.ts",
+ "import": "./dist/artplayer-plugin-dash-control.legacy.js",
+ "require": "./dist/artplayer-plugin-dash-control.legacy.js"
+ }
+ },
+ "main": "./dist/artplayer-plugin-dash-control.js",
+ "module": "./dist/artplayer-plugin-dash-control.mjs",
+ "types": "./types/artplayer-plugin-dash-control.d.ts",
+ "legacy": "./dist/artplayer-plugin-dash-control.legacy.js",
+ "browserslist": "last 1 Chrome version"
+ }
+ },
+ "sourceSHA256": "6c37d58c427aa688fe252127dbbc5bb131d0ef6e5141a3b22400f40e5fd177f9",
+ "candidate": "packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.js",
+ "scope": "Real ArtPlayer DOM and native MP4 with controlled SDK methods. No dash.js instance, MPD, ABR or adaptive media validation."
+ },
+ "engines": [
+ {
+ "project": "chromium",
+ "version": "153.0.8010.12",
+ "platform": "win32"
+ },
+ {
+ "project": "firefox",
+ "version": "155.0",
+ "platform": "win32"
+ },
+ {
+ "project": "webkit",
+ "version": "26.6",
+ "platform": "win32"
+ }
+ ],
+ "manifest": {
+ "kind": "artplayer-browser-test",
+ "resources": {
+ "/candidate/artplayer.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer\\src\\index.ts"
+ },
+ "sha256": "df495dd386d32ea96be5006e1bbfda1ea91cfeb32aa21758197b46a155ebe0dc",
+ "bytes": 378040
+ },
+ "/compiled/artplayer.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer\\src\\index.ts"
+ },
+ "sha256": "df495dd386d32ea96be5006e1bbfda1ea91cfeb32aa21758197b46a155ebe0dc",
+ "bytes": 378040,
+ "aliasOf": "/candidate/artplayer.js"
+ },
+ "/uncompiled/artplayer/index.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer\\src\\index.ts"
+ },
+ "sha256": "df495dd386d32ea96be5006e1bbfda1ea91cfeb32aa21758197b46a155ebe0dc",
+ "bytes": 378040,
+ "aliasOf": "/candidate/artplayer.js"
+ },
+ "/candidate/artplayer-plugin-chapter.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts"
+ },
+ "sha256": "1a8eb891d3690e2526e78a2e53183a25367b1d02a5fd349d821548edb731ee3a",
+ "bytes": 8529
+ },
+ "/compiled/artplayer-plugin-chapter.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts"
+ },
+ "sha256": "1a8eb891d3690e2526e78a2e53183a25367b1d02a5fd349d821548edb731ee3a",
+ "bytes": 8529,
+ "aliasOf": "/candidate/artplayer-plugin-chapter.js"
+ },
+ "/uncompiled/artplayer-plugin-chapter/index.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts"
+ },
+ "sha256": "1a8eb891d3690e2526e78a2e53183a25367b1d02a5fd349d821548edb731ee3a",
+ "bytes": 8529,
+ "aliasOf": "/candidate/artplayer-plugin-chapter.js"
+ },
+ "/published/artplayer.js": {
+ "source": {
+ "kind": "npm-release",
+ "version": "5.4.0",
+ "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==",
+ "member": "package/dist/artplayer.js"
+ },
+ "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f",
+ "bytes": 134037
+ },
+ "/published/artplayer-plugin-chapter.js": {
+ "source": {
+ "kind": "npm-release",
+ "version": "1.1.0",
+ "integrity": "sha512-ji66Lr2K2S+OjFMuMby3yq+pefWlOC51Sbpop/DIr2N6w3F0smrF+YZbshN1Dy8/Tx1TYpT7N6WkkZWy5Jix1w==",
+ "member": "package/dist/artplayer-plugin-chapter.js"
+ },
+ "sha256": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0",
+ "bytes": 4973
+ },
+ "/test/pattern.mp4": {
+ "source": {
+ "kind": "generated-media",
+ "file": "test/browser/media/pattern.mp4"
+ },
+ "sha256": "0ad1d7ad286aec6e0fa9e27fc1e487e7277df2e603e30040fb942365763d0233",
+ "bytes": 251538
+ },
+ "/test/pattern.webm": {
+ "source": {
+ "kind": "generated-media",
+ "file": "test/browser/media/pattern.webm"
+ },
+ "sha256": "9d3c2b3dec9581a599de2d6732992ef7846790e0d1368e6e131bd339b39e984f",
+ "bytes": 162073
+ },
+ "/test/audio-tone.m4a": {
+ "source": {
+ "kind": "generated-media",
+ "file": "test/browser/media/audio-tone.m4a"
+ },
+ "sha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "bytes": 132436
+ },
+ "/test/declaration-cues.vtt": {
+ "source": {
+ "kind": "test-subtitles",
+ "file": "test/browser/media/declaration-cues.vtt"
+ },
+ "sha256": "5d149d4e641ce41cbb83376835c03daf1fb89f3b9f568fa59e064d51e2b57704",
+ "bytes": 54
+ },
+ "/test/thumbnail-grid.svg": {
+ "source": {
+ "kind": "test-thumbnail-grid",
+ "file": "test/browser/media/thumbnail-grid.svg"
+ },
+ "sha256": "bb3f1ec9895d37def77ca4b192d3cd73577a2668558cc99de1588f511d65d3e6",
+ "bytes": 14969
+ },
+ "/test/legacy-safe-area.js": {
+ "source": {
+ "kind": "frozen-own-source",
+ "file": "test/helpers/legacy-safe-area.js",
+ "commit": "ccf77c4e"
+ },
+ "sha256": "ce0368618ef6363b78271b8bcc7973abd0478e797cc464ddd912929e6dfa501a",
+ "bytes": 795
+ },
+ "/assets/sample/video.mp4": {
+ "source": {
+ "kind": "docs-media",
+ "file": "docs/assets/sample/video.mp4"
+ },
+ "sha256": "30a2de667ba267b6bc605edcee1c4bd728c048ed76f500ea0e73d1f1a6f8f980",
+ "bytes": 5818706
+ },
+ "/test/performance.js": {
+ "source": {
+ "kind": "adapted-baseline-fixture",
+ "file": "refactor/fixtures/performance.js"
+ },
+ "sha256": "2318f39bdb5f3a3348d755cb4335907ae4adc944be3a435839c1bba201e81500",
+ "bytes": 8994
+ }
+ }
+ }
+ },
+ {
+ "file": "refactor/.cache/dash03-main-final/report.json",
+ "sha256": "c3175930e53b8d4771c5393de015520f0dbcfb56362cd8017c1263ac1eba8758",
+ "bytes": 15813200,
+ "stats": {
+ "startTime": "2026-09-11T20:53:02.032Z",
+ "duration": 64490.32,
+ "expected": 84,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "results": {
+ "directory": "refactor/.cache/dash03-main-final/results",
+ "files": 85,
+ "sha256": "d3efa538c4ba0e698aa95b40b112caabf8324b5f3beec05f6845e7f5675de857",
+ "algorithm": "SHA256 of JSON.stringify(sorted relative path and raw SHA256 pairs), UTF-8, slash paths"
+ },
+ "pluginInput": {
+ "published": {
+ "name": "artplayer-plugin-dash-control",
+ "version": "1.1.0",
+ "metadataUrl": "https://registry.npmjs.org/artplayer-plugin-dash-control/1.1.0",
+ "tarball": "https://registry.npmjs.org/artplayer-plugin-dash-control/-/artplayer-plugin-dash-control-1.1.0.tgz",
+ "integrity": "sha512-EssRkzYXWcln3a6nAzi4ESacBhzMih8s0bzdjXuvBBCh2UFmvK99ITD5E+0cuU8i5ZzIQGQSdce1/gANTrX0BQ==",
+ "sha256": "f4047a569d99b6a7de57b3719b7ad40109f2a1d360566a840f50ab32aacefcd9",
+ "archiveBytes": 3385,
+ "registryGitHead": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
+ "files": {
+ "package/README.md": "98982f1123ff9a11f5d3d9a52da6e8fea9cd0989f7465605c27b0f9a46f2dbcd",
+ "package/dist/artplayer-plugin-dash-control.js": "f0cbbcf8badeaca13c05ae178932818cb52fbce37ed504ac0e62160b9c7e1145",
+ "package/dist/artplayer-plugin-dash-control.legacy.js": "f0cbbcf8badeaca13c05ae178932818cb52fbce37ed504ac0e62160b9c7e1145",
+ "package/dist/artplayer-plugin-dash-control.mjs": "e32bf08c33a481781883067dcc639f0495c9dcb8a88b91be13736370f4932fc4",
+ "package/package.json": "556e2adc479bab36c5973de617c0b57c5d86aa8425643a1952ab04738e9ce927",
+ "package/types/artplayer-plugin-dash-control.d.ts": "e15ef683e0c335d81531587828593325e037efe7ed29c022477cd14fe71aebba"
+ },
+ "manifest": {
+ "name": "artplayer-plugin-dash-control",
+ "version": "1.1.0",
+ "description": "Dash control plugin for ArtPlayer",
+ "author": "Harvey Zhao ",
+ "license": "MIT",
+ "homepage": "https://artplayer.org",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/zhw2590582/ArtPlayer.git"
+ },
+ "bugs": {
+ "url": "https://github.com/zhw2590582/ArtPlayer/issues"
+ },
+ "keywords": [
+ "html5",
+ "video",
+ "player"
+ ],
+ "exports": {
+ ".": {
+ "types": "./types/artplayer-plugin-dash-control.d.ts",
+ "import": "./dist/artplayer-plugin-dash-control.mjs",
+ "require": "./dist/artplayer-plugin-dash-control.js"
+ },
+ "./legacy": {
+ "types": "./types/artplayer-plugin-dash-control.d.ts",
+ "import": "./dist/artplayer-plugin-dash-control.legacy.js",
+ "require": "./dist/artplayer-plugin-dash-control.legacy.js"
+ }
+ },
+ "main": "./dist/artplayer-plugin-dash-control.js",
+ "module": "./dist/artplayer-plugin-dash-control.mjs",
+ "types": "./types/artplayer-plugin-dash-control.d.ts",
+ "legacy": "./dist/artplayer-plugin-dash-control.legacy.js",
+ "browserslist": "last 1 Chrome version"
+ }
+ },
+ "sourceSHA256": "c1c63bd0ec787a6f9eb445b53341dd5eaeaccccd1589455234be311fcf6ce5e6",
+ "candidate": "packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.js",
+ "scope": "Real ArtPlayer DOM and native MP4 with controlled SDK methods. No dash.js instance, MPD, ABR or adaptive media validation."
+ },
+ "engines": [
+ {
+ "project": "chromium",
+ "version": "153.0.8010.12",
+ "platform": "win32"
+ },
+ {
+ "project": "firefox",
+ "version": "155.0",
+ "platform": "win32"
+ },
+ {
+ "project": "webkit",
+ "version": "26.6",
+ "platform": "win32"
+ }
+ ],
+ "manifest": {
+ "kind": "artplayer-browser-test",
+ "resources": {
+ "/candidate/artplayer.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer\\src\\index.ts"
+ },
+ "sha256": "df495dd386d32ea96be5006e1bbfda1ea91cfeb32aa21758197b46a155ebe0dc",
+ "bytes": 378040
+ },
+ "/compiled/artplayer.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer\\src\\index.ts"
+ },
+ "sha256": "df495dd386d32ea96be5006e1bbfda1ea91cfeb32aa21758197b46a155ebe0dc",
+ "bytes": 378040,
+ "aliasOf": "/candidate/artplayer.js"
+ },
+ "/uncompiled/artplayer/index.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer\\src\\index.ts"
+ },
+ "sha256": "df495dd386d32ea96be5006e1bbfda1ea91cfeb32aa21758197b46a155ebe0dc",
+ "bytes": 378040,
+ "aliasOf": "/candidate/artplayer.js"
+ },
+ "/candidate/artplayer-plugin-chapter.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts"
+ },
+ "sha256": "1a8eb891d3690e2526e78a2e53183a25367b1d02a5fd349d821548edb731ee3a",
+ "bytes": 8529
+ },
+ "/compiled/artplayer-plugin-chapter.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts"
+ },
+ "sha256": "1a8eb891d3690e2526e78a2e53183a25367b1d02a5fd349d821548edb731ee3a",
+ "bytes": 8529,
+ "aliasOf": "/candidate/artplayer-plugin-chapter.js"
+ },
+ "/uncompiled/artplayer-plugin-chapter/index.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts"
+ },
+ "sha256": "1a8eb891d3690e2526e78a2e53183a25367b1d02a5fd349d821548edb731ee3a",
+ "bytes": 8529,
+ "aliasOf": "/candidate/artplayer-plugin-chapter.js"
+ },
+ "/published/artplayer.js": {
+ "source": {
+ "kind": "npm-release",
+ "version": "5.4.0",
+ "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==",
+ "member": "package/dist/artplayer.js"
+ },
+ "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f",
+ "bytes": 134037
+ },
+ "/published/artplayer-plugin-chapter.js": {
+ "source": {
+ "kind": "npm-release",
+ "version": "1.1.0",
+ "integrity": "sha512-ji66Lr2K2S+OjFMuMby3yq+pefWlOC51Sbpop/DIr2N6w3F0smrF+YZbshN1Dy8/Tx1TYpT7N6WkkZWy5Jix1w==",
+ "member": "package/dist/artplayer-plugin-chapter.js"
+ },
+ "sha256": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0",
+ "bytes": 4973
+ },
+ "/test/pattern.mp4": {
+ "source": {
+ "kind": "generated-media",
+ "file": "test/browser/media/pattern.mp4"
+ },
+ "sha256": "0ad1d7ad286aec6e0fa9e27fc1e487e7277df2e603e30040fb942365763d0233",
+ "bytes": 251538
+ },
+ "/test/pattern.webm": {
+ "source": {
+ "kind": "generated-media",
+ "file": "test/browser/media/pattern.webm"
+ },
+ "sha256": "9d3c2b3dec9581a599de2d6732992ef7846790e0d1368e6e131bd339b39e984f",
+ "bytes": 162073
+ },
+ "/test/audio-tone.m4a": {
+ "source": {
+ "kind": "generated-media",
+ "file": "test/browser/media/audio-tone.m4a"
+ },
+ "sha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "bytes": 132436
+ },
+ "/test/declaration-cues.vtt": {
+ "source": {
+ "kind": "test-subtitles",
+ "file": "test/browser/media/declaration-cues.vtt"
+ },
+ "sha256": "5d149d4e641ce41cbb83376835c03daf1fb89f3b9f568fa59e064d51e2b57704",
+ "bytes": 54
+ },
+ "/test/thumbnail-grid.svg": {
+ "source": {
+ "kind": "test-thumbnail-grid",
+ "file": "test/browser/media/thumbnail-grid.svg"
+ },
+ "sha256": "bb3f1ec9895d37def77ca4b192d3cd73577a2668558cc99de1588f511d65d3e6",
+ "bytes": 14969
+ },
+ "/test/legacy-safe-area.js": {
+ "source": {
+ "kind": "frozen-own-source",
+ "file": "test/helpers/legacy-safe-area.js",
+ "commit": "ccf77c4e"
+ },
+ "sha256": "ce0368618ef6363b78271b8bcc7973abd0478e797cc464ddd912929e6dfa501a",
+ "bytes": 795
+ },
+ "/assets/sample/video.mp4": {
+ "source": {
+ "kind": "docs-media",
+ "file": "docs/assets/sample/video.mp4"
+ },
+ "sha256": "30a2de667ba267b6bc605edcee1c4bd728c048ed76f500ea0e73d1f1a6f8f980",
+ "bytes": 5818706
+ },
+ "/test/performance.js": {
+ "source": {
+ "kind": "adapted-baseline-fixture",
+ "file": "refactor/fixtures/performance.js"
+ },
+ "sha256": "2318f39bdb5f3a3348d755cb4335907ae4adc944be3a435839c1bba201e81500",
+ "bytes": 8994
+ }
+ }
+ }
+ },
+ {
+ "file": "refactor/.cache/dash03-legacy-final/report.json",
+ "sha256": "5eaa45267a3f0e6a5c5922f974d46737a7d3c559c9a8e2dab84e8262269e6d83",
+ "bytes": 15807757,
+ "stats": {
+ "startTime": "2026-09-11T20:55:36.976Z",
+ "duration": 69224.891,
+ "expected": 84,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "results": {
+ "directory": "refactor/.cache/dash03-legacy-final/results",
+ "files": 85,
+ "sha256": "a7016f915efb43c45b31f9d25b755fba95c649ec7008bb397130cc840e78b7fe",
+ "algorithm": "SHA256 of JSON.stringify(sorted relative path and raw SHA256 pairs), UTF-8, slash paths"
+ },
+ "pluginInput": {
+ "published": {
+ "name": "artplayer-plugin-dash-control",
+ "version": "1.1.0",
+ "metadataUrl": "https://registry.npmjs.org/artplayer-plugin-dash-control/1.1.0",
+ "tarball": "https://registry.npmjs.org/artplayer-plugin-dash-control/-/artplayer-plugin-dash-control-1.1.0.tgz",
+ "integrity": "sha512-EssRkzYXWcln3a6nAzi4ESacBhzMih8s0bzdjXuvBBCh2UFmvK99ITD5E+0cuU8i5ZzIQGQSdce1/gANTrX0BQ==",
+ "sha256": "f4047a569d99b6a7de57b3719b7ad40109f2a1d360566a840f50ab32aacefcd9",
+ "archiveBytes": 3385,
+ "registryGitHead": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
+ "files": {
+ "package/README.md": "98982f1123ff9a11f5d3d9a52da6e8fea9cd0989f7465605c27b0f9a46f2dbcd",
+ "package/dist/artplayer-plugin-dash-control.js": "f0cbbcf8badeaca13c05ae178932818cb52fbce37ed504ac0e62160b9c7e1145",
+ "package/dist/artplayer-plugin-dash-control.legacy.js": "f0cbbcf8badeaca13c05ae178932818cb52fbce37ed504ac0e62160b9c7e1145",
+ "package/dist/artplayer-plugin-dash-control.mjs": "e32bf08c33a481781883067dcc639f0495c9dcb8a88b91be13736370f4932fc4",
+ "package/package.json": "556e2adc479bab36c5973de617c0b57c5d86aa8425643a1952ab04738e9ce927",
+ "package/types/artplayer-plugin-dash-control.d.ts": "e15ef683e0c335d81531587828593325e037efe7ed29c022477cd14fe71aebba"
+ },
+ "manifest": {
+ "name": "artplayer-plugin-dash-control",
+ "version": "1.1.0",
+ "description": "Dash control plugin for ArtPlayer",
+ "author": "Harvey Zhao ",
+ "license": "MIT",
+ "homepage": "https://artplayer.org",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/zhw2590582/ArtPlayer.git"
+ },
+ "bugs": {
+ "url": "https://github.com/zhw2590582/ArtPlayer/issues"
+ },
+ "keywords": [
+ "html5",
+ "video",
+ "player"
+ ],
+ "exports": {
+ ".": {
+ "types": "./types/artplayer-plugin-dash-control.d.ts",
+ "import": "./dist/artplayer-plugin-dash-control.mjs",
+ "require": "./dist/artplayer-plugin-dash-control.js"
+ },
+ "./legacy": {
+ "types": "./types/artplayer-plugin-dash-control.d.ts",
+ "import": "./dist/artplayer-plugin-dash-control.legacy.js",
+ "require": "./dist/artplayer-plugin-dash-control.legacy.js"
+ }
+ },
+ "main": "./dist/artplayer-plugin-dash-control.js",
+ "module": "./dist/artplayer-plugin-dash-control.mjs",
+ "types": "./types/artplayer-plugin-dash-control.d.ts",
+ "legacy": "./dist/artplayer-plugin-dash-control.legacy.js",
+ "browserslist": "last 1 Chrome version"
+ }
+ },
+ "sourceSHA256": "1b6fba04c90ba19197213cf1868599dab13d430e8923d3081e37c43dc4d5d00e",
+ "candidate": "packages/artplayer-plugin-dash-control/dist/artplayer-plugin-dash-control.legacy.js",
+ "scope": "Real ArtPlayer DOM and native MP4 with controlled SDK methods. No dash.js instance, MPD, ABR or adaptive media validation."
+ },
+ "engines": [
+ {
+ "project": "chromium",
+ "version": "153.0.8010.12",
+ "platform": "win32"
+ },
+ {
+ "project": "firefox",
+ "version": "155.0",
+ "platform": "win32"
+ },
+ {
+ "project": "webkit",
+ "version": "26.6",
+ "platform": "win32"
+ }
+ ],
+ "manifest": {
+ "kind": "artplayer-browser-test",
+ "resources": {
+ "/candidate/artplayer.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer\\src\\index.ts"
+ },
+ "sha256": "df495dd386d32ea96be5006e1bbfda1ea91cfeb32aa21758197b46a155ebe0dc",
+ "bytes": 378040
+ },
+ "/compiled/artplayer.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer\\src\\index.ts"
+ },
+ "sha256": "df495dd386d32ea96be5006e1bbfda1ea91cfeb32aa21758197b46a155ebe0dc",
+ "bytes": 378040,
+ "aliasOf": "/candidate/artplayer.js"
+ },
+ "/uncompiled/artplayer/index.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer\\src\\index.ts"
+ },
+ "sha256": "df495dd386d32ea96be5006e1bbfda1ea91cfeb32aa21758197b46a155ebe0dc",
+ "bytes": 378040,
+ "aliasOf": "/candidate/artplayer.js"
+ },
+ "/candidate/artplayer-plugin-chapter.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts"
+ },
+ "sha256": "1a8eb891d3690e2526e78a2e53183a25367b1d02a5fd349d821548edb731ee3a",
+ "bytes": 8529
+ },
+ "/compiled/artplayer-plugin-chapter.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts"
+ },
+ "sha256": "1a8eb891d3690e2526e78a2e53183a25367b1d02a5fd349d821548edb731ee3a",
+ "bytes": 8529,
+ "aliasOf": "/candidate/artplayer-plugin-chapter.js"
+ },
+ "/uncompiled/artplayer-plugin-chapter/index.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts"
+ },
+ "sha256": "1a8eb891d3690e2526e78a2e53183a25367b1d02a5fd349d821548edb731ee3a",
+ "bytes": 8529,
+ "aliasOf": "/candidate/artplayer-plugin-chapter.js"
+ },
+ "/published/artplayer.js": {
+ "source": {
+ "kind": "npm-release",
+ "version": "5.4.0",
+ "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==",
+ "member": "package/dist/artplayer.js"
+ },
+ "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f",
+ "bytes": 134037
+ },
+ "/published/artplayer-plugin-chapter.js": {
+ "source": {
+ "kind": "npm-release",
+ "version": "1.1.0",
+ "integrity": "sha512-ji66Lr2K2S+OjFMuMby3yq+pefWlOC51Sbpop/DIr2N6w3F0smrF+YZbshN1Dy8/Tx1TYpT7N6WkkZWy5Jix1w==",
+ "member": "package/dist/artplayer-plugin-chapter.js"
+ },
+ "sha256": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0",
+ "bytes": 4973
+ },
+ "/test/pattern.mp4": {
+ "source": {
+ "kind": "generated-media",
+ "file": "test/browser/media/pattern.mp4"
+ },
+ "sha256": "0ad1d7ad286aec6e0fa9e27fc1e487e7277df2e603e30040fb942365763d0233",
+ "bytes": 251538
+ },
+ "/test/pattern.webm": {
+ "source": {
+ "kind": "generated-media",
+ "file": "test/browser/media/pattern.webm"
+ },
+ "sha256": "9d3c2b3dec9581a599de2d6732992ef7846790e0d1368e6e131bd339b39e984f",
+ "bytes": 162073
+ },
+ "/test/audio-tone.m4a": {
+ "source": {
+ "kind": "generated-media",
+ "file": "test/browser/media/audio-tone.m4a"
+ },
+ "sha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "bytes": 132436
+ },
+ "/test/declaration-cues.vtt": {
+ "source": {
+ "kind": "test-subtitles",
+ "file": "test/browser/media/declaration-cues.vtt"
+ },
+ "sha256": "5d149d4e641ce41cbb83376835c03daf1fb89f3b9f568fa59e064d51e2b57704",
+ "bytes": 54
+ },
+ "/test/thumbnail-grid.svg": {
+ "source": {
+ "kind": "test-thumbnail-grid",
+ "file": "test/browser/media/thumbnail-grid.svg"
+ },
+ "sha256": "bb3f1ec9895d37def77ca4b192d3cd73577a2668558cc99de1588f511d65d3e6",
+ "bytes": 14969
+ },
+ "/test/legacy-safe-area.js": {
+ "source": {
+ "kind": "frozen-own-source",
+ "file": "test/helpers/legacy-safe-area.js",
+ "commit": "ccf77c4e"
+ },
+ "sha256": "ce0368618ef6363b78271b8bcc7973abd0478e797cc464ddd912929e6dfa501a",
+ "bytes": 795
+ },
+ "/assets/sample/video.mp4": {
+ "source": {
+ "kind": "docs-media",
+ "file": "docs/assets/sample/video.mp4"
+ },
+ "sha256": "30a2de667ba267b6bc605edcee1c4bd728c048ed76f500ea0e73d1f1a6f8f980",
+ "bytes": 5818706
+ },
+ "/test/performance.js": {
+ "source": {
+ "kind": "adapted-baseline-fixture",
+ "file": "refactor/fixtures/performance.js"
+ },
+ "sha256": "2318f39bdb5f3a3348d755cb4335907ae4adc944be3a435839c1bba201e81500",
+ "bytes": 8994
+ }
+ }
+ }
+ }
+ ],
+ "logs": [
+ {
+ "file": "refactor/.cache/dash03-before.log",
+ "sha256": "5a4e92a1bcf6a8117ba6745d1d54ade25d58643200ffc1d18d0f09e8e1ba8e28",
+ "bytes": 175508
+ },
+ {
+ "file": "refactor/.cache/dash03-reentry-before.log",
+ "sha256": "bc04fcd6b8518f74718bcc732a26f88fb919a79f63a1dc073fe724849ea0c99d",
+ "bytes": 4865
+ },
+ {
+ "file": "refactor/.cache/dash03-formatter-before.log",
+ "sha256": "0799ccc96096116e75e83db65289102bde825f57796bfb620579927d982084d0",
+ "bytes": 4840
+ },
+ {
+ "file": "refactor/.cache/dash03-node-final.log",
+ "sha256": "4455e88c907ea0f2f335b67c020bf819e47b124fc3e766c1bb220b6135934f6d",
+ "bytes": 11463
+ },
+ {
+ "file": "refactor/.cache/dash03-artifacts-realm-before.log",
+ "sha256": "0a09f96f3457128982aa93d3d5c9a93f84900ea1bf09add729c47d048714b7be",
+ "bytes": 36034
+ },
+ {
+ "file": "refactor/.cache/dash03-artifacts-final.log",
+ "sha256": "c68ec1c1ab54f7e389ce210bc95276ed6484f499e3d2d90cb023bf92a2dcb76b",
+ "bytes": 31662
+ },
+ {
+ "file": "refactor/.cache/dash03-build-final.log",
+ "sha256": "90a8251da20f5ebe7cf2759e87284ab0265bc9db4b7a12dc14304f14019883c1",
+ "bytes": 1472
+ },
+ {
+ "file": "refactor/.cache/dash03-main.log",
+ "sha256": "9c55a02c2bc12d2128779677d9a02a4be3e3c57a50a5ad46631bc703b9d58a99",
+ "bytes": 16163
+ },
+ {
+ "file": "refactor/.cache/dash03-main-final.log",
+ "sha256": "896e9acf28c7b66998dd25ad9c39304242180d8e273f27f16a52a8c3d02fe162",
+ "bytes": 16162
+ },
+ {
+ "file": "refactor/.cache/dash03-legacy-final.log",
+ "sha256": "837a8ad81b4306231d44f9ca38f328afef8a57c1f604196bc64319b772049167",
+ "bytes": 16160
+ },
+ {
+ "file": "refactor/.cache/dash03-ci.log",
+ "sha256": "4d88eba96b783e9e26ea2197c398160b2565e622a58ab5b80db44375bcd85722",
+ "bytes": 74484
+ }
+ ],
+ "results": {
+ "initialRegression": {
+ "passed": 1,
+ "failed": 21
+ },
+ "reentrantCleanupBefore": {
+ "passed": 29,
+ "failed": 2
+ },
+ "formatterShutdownBefore": {
+ "passed": 29,
+ "failed": 2
+ },
+ "nodeFinal": 116,
+ "nodeWithThreeArtifacts": 266,
+ "mainBrowser": 84,
+ "legacyBrowser": 84,
+ "ci": {
+ "unit": 661,
+ "engineering": 14,
+ "baseline": 37
+ }
+ },
+ "localEvidence": "Raw logs and browser reports/traces remain local cache. Committed hashes locate evidence but are not uploaded traces; rerun on a fresh machine."
+}
diff --git a/refactor/changes/2026-09-12-PKG-DASH-03-runtime.md b/refactor/changes/2026-09-12-PKG-DASH-03-runtime.md
new file mode 100644
index 000000000..7ecdc4b9f
--- /dev/null
+++ b/refactor/changes/2026-09-12-PKG-DASH-03-runtime.md
@@ -0,0 +1,36 @@
+# PKG-DASH-03:两代 SDK 适配、菜单与生命周期拆分
+
+起点 `762cc634`。把单一入口拆为入口生命周期、SDK 方法适配、selector 模型、菜单所有权
+四个模块。保持 JavaScript 完成本步结构与行为修复,下一步 04 再迁移严格 TS 和公开声明。
+包内 [架构图](../../packages/artplayer-plugin-dash-control/ARCHITECTURE.md) 随实际实现维护。
+
+## 行为修正
+
+- 恢复发布版 dash.js 4.x 方法表,同时保留工作区 5.x 稳定 ID;不使用过滤后的下标选画质。
+- 空拓扑、禁用菜单、渲染失败清理自有 UI;换 SDK/重建/销毁后旧引用无副作用。
+- 销毁释放 ready/restart/destroy 订阅,安装异常回滚;不销毁调用方 SDK,不误删调用方替换的菜单。
+- 当前重复标签保留实际当前 key/track;零 ID、唯一克隆轨道高亮正确,歧义轨道不伪造选中。
+- SDK/getName 重入使旧 revision 失效;回调销毁后停止后续格式化、SDK 写入和提示。
+ 清理中同步重建时只释放旧所有权,不删除重建的新菜单。
+
+公开工厂、同步 update/选中返回、getName 身份/参数、通知顺序、DOM/CSS hooks 与包入口保持。
+错误继续同步传播原对象,次要清理异常不覆盖原错误。上述历史缺陷作为有意修正记录,
+不把冻结旧实现的缺陷观察改成候选期望。README 更新 SDK 适配说明,没有改 demo 或增加依赖。
+
+## 验证
+
+最初 22 项新增回归先运行旧源码;后续重入和格式化关闭的两组红灯另保留。正式三格式加入
+生命周期回归时发现 4 项 Node VM 数组原型比较问题,实际内容相同;用本地 Array.from
+比较原有精确内容,未放宽断言。所有日志和输入指纹见 [执行证据](../baselines/dash-runtime.json)。
+
+最终源码 116 项、包含三格式及两代 SDK 的 Node 266 项通过;正式 main/legacy 浏览器各
+84 项通过。完整 CI 712 项(661 单元、14 工程、37 基线)通过;包三格式构建成功,
+计划/风险/本地链接校验通过。没有重试或跳过。DASH-LIFE-01 关闭,SDK/实际轨道风险仍 open。
+当前 217 项:73 done、3 doing、141 todo。已完成结构步,不计为 TS 迁移或发布完成。
+
+真实 DOM 浏览器测试扩充到两种候选 SDK、两代核心及空拓扑/销毁:每格式 84 项。
+仍是可控 SDK 方法与原生 MP4,不是实际 dash.js/MPD/ABR 验收。SDK/状态的真实媒体风险
+继续由 05 接续;04 精确类型、06 分发、真机和全局发布复盘没有被本步替代。
+
+本步通过标准构建脚本生成 dist/docs compiled,未手改产物。独立本地提交;不推送、不发布。
+回退本提交同时撤回结构、行为修正、回归和产物,恢复重构前仅支持 5.x 的源码;该限制需知悉。
diff --git a/refactor/dash-validation.md b/refactor/dash-validation.md
index 2cc60f621..0f34d4b34 100644
--- a/refactor/dash-validation.md
+++ b/refactor/dash-validation.md
@@ -1,30 +1,34 @@
# DASH Control 验证与维护入口
-PKG-DASH-02 保护真实 npm 1.1.0 的既有调用契约,以及重构前工作区的 SDK 5.x 稳定 ID
-修正。版本来源见 [发布契约](baselines/dash-control-contract.md)。本步没有更改生产实现。
+测试保护真实 npm 1.1.0 的既有调用契约,以及重构前工作区的 SDK 5.x 稳定 ID 修正。
+版本来源见 [发布契约](baselines/dash-control-contract.md)。02 建立基线,03 已改造生产实现;
+实际模块地图与生命周期规则见 [包内架构](../packages/artplayer-plugin-dash-control/ARCHITECTURE.md)。
## 可重跑的测试
-- `yarn test:dash-control`:原有 5 项过滤/稳定 ID 回归和新增 69 项行为测试,共 74 项。
+- `yarn test:dash-control`:原有 5 项过滤/稳定 ID 回归、80 项行为/历史观察、31 项候选生命周期,共 116 项。
该脚本和 `test:unit` 都保留旧测试,完整 CI 自动执行。
-- `yarn test:browser test/browser/dash-control.spec.js --trace on`:48 项,两个核心、两个插件、
- 两类菜单、两种选择、Chromium/Firefox/WebKit。浏览器套件串行运行,避免共享端口和报告覆盖。
+- `yarn test:browser test/browser/dash-control.spec.js --trace on`:84 项,两个核心、发布插件 SDK 4/
+ 候选插件 SDK 4/5、两类菜单、两种选择及候选清理、Chromium/Firefox/WebKit。
+ 浏览器套件串行运行,避免共享端口和报告覆盖。
- `yarn ci:check`:工具链、计划、lint、声明漂移、严格类型和 Node/基线测试。
Node helper 分别加载当前源码、固定 Git 提交的旧工作区 UMD、实际 npm main/legacy/module。
Git 内容以 LF 指纹核对,发布归档与成员以原始字节指纹核对。`ARTPLAYER_TEST_DASH` 可指定
-待测产物,以系统路径分隔符连接;未设置时不会把源码测试冒充三种候选产物验证。
+待测产物,以系统路径分隔符连接;同时指定 main/legacy/module 时共 266 项,包含两代 SDK 的
+行为与生命周期。未设置时不会把源码测试冒充三种候选产物验证。
`test/helpers/dash-control.js` 只提供可控 SDK 方法和组件注册表,检查 this、类型参数、
原始轨道身份、调用顺序、错误身份、ABR 配置保留、换 SDK、多实例和关闭后的引用。
-真实旧实现的缺陷观察保持冻结;03 修复时为候选实现新增正确行为断言,不把历史缺陷当作兼容要求。
+真实旧实现的缺陷观察保持冻结;候选生命周期新增正确行为断言,不把历史缺陷当作兼容要求。
浏览器使用真实发布/候选 ArtPlayer DOM、真实发布/候选插件和本地原生 MP4。SDK 方法可控,
断言真实点击触发的参数及控制栏/设置同步,再通过真实播放按钮验证视频时钟推进、无媒体错误。
**没有加载 dash.js,没有验证 MPD、ABR 自动切换或自适应流解码。** 这些仍是 05 必须完成的
-固定 dash.js 4.5.2 / 5.2.1 集成验证。`ARTPLAYER_DASH_ARTIFACT` 可替换插件脚本;本次没有使用。
+固定 dash.js 4.5.2 / 5.2.1 集成验证。`ARTPLAYER_DASH_ARTIFACT` 可替换插件脚本;02 未设置,
+03 分别使用正式 main 与 legacy 文件。核心仍按服务端 manifest 加载发布/候选源码构建。
-## 首次失败与证据
+## 02 首次失败与证据
首次浏览器运行 24 通过、24 失败;全部失败位于设置选项文字定位。trace 中选项确实已渲染,
整行文本包含图标 SVG 的换行,`^fr$`/`^720p$` 不能匹配。改为当前面板内专用
@@ -35,11 +39,11 @@ Git 内容以 LF 指纹核对,发布归档与成员以原始字节指纹核对
[本步执行记录](baselines/dash-validation.json)。缓存不入 Git,永久保存的是测试、来源和结果摘要;
换机器应按上述命令重新取得执行证据,不能声称本地 trace 已上传。
-## 下一步缺陷与边界
+## 03 修正与后续边界
-- DASH-SDK-01:两代旧实现分别在另一代 SDK 方法表上失败。03 通过能力适配恢复旧用法。
-- DASH-LIFE-01:空列表/禁用后 UI 残留,旧回调仍写旧 SDK,销毁后 update 和订阅仍有效。
-- DASH-STATE-01:克隆当前轨道、重复标签、数字零 representation ID 会丢失高亮。
+- DASH-SDK-01:通过能力适配恢复两代方法表,实际 SDK/媒体仍由 05 验证。
+- DASH-LIFE-01:空列表/禁用后 UI、旧回调、销毁后 update/订阅已修正,补充重入和错误回归。
+- DASH-STATE-01:唯一克隆当前轨道、重复标签、数字零 ID 已修正;实际 SDK 轨道身份仍由 05 核验。
-这些受控复现仍是 open,不能以本步绿色测试关闭。03 处理结构、清理与正确当前项,04 完成公开
-类型兼容,05 完成真实 SDK/媒体/组合验证,06 完成包分发验收。物理设备和 npm 发布准入仍待完成。
+状态以 risks.json 为准;受控修复不替代真实 SDK 验收。04 完成严格 TS/公开类型兼容,05 完成
+真实 SDK/媒体/组合验证,06 完成包分发验收。物理设备和 npm 发布准入仍待完成。
diff --git a/refactor/plan.md b/refactor/plan.md
index ba8b69941..871963c9d 100644
--- a/refactor/plan.md
+++ b/refactor/plan.md
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 217 项,范围 22 个包及工作区/示例。
-状态:todo 142 / doing 3 / blocked 0 / done 72 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
+状态:todo 141 / doing 3 / blocked 0 / done 73 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -203,7 +203,7 @@
| --- | --- | --- | --- | --- | --- | --- |
| PKG-DASH-01 | artplayer-plugin-dash-control
核对包契约与历史用法 | BASE-05 | quality/audio、representation ID/Auto、getName 和 update | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-DASH-02 | artplayer-plugin-dash-control
建立特有行为与错误测试 | PKG-DASH-01, ENG-03, ENG-05 | 保留已有 5 项稳定 ID 回归,补发布版 SDK 4.x/当前 5.x、音轨/空列表/过滤/换源与关闭引用 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
-| PKG-DASH-03 | artplayer-plugin-dash-control
整理内部职责与资源 | PKG-DASH-02, CORE-11, CORE-14 | 能力适配保留 SDK 4.x 与 5.x,稳定 ID 映射、ABR 状态、UI 清理和生命周期职责分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
+| PKG-DASH-03 | artplayer-plugin-dash-control
整理内部职责与资源 | PKG-DASH-02, CORE-11, CORE-14 | 能力适配保留 SDK 4.x 与 5.x,稳定 ID 映射、ABR 状态、UI 清理和生命周期职责分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-DASH-04 | artplayer-plugin-dash-control
迁移自有源码和公开类型 | PKG-DASH-03, ENG-04, ENG-06, CORE-07 | dash.js adapter、selector 和回调类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| PKG-DASH-05 | artplayer-plugin-dash-control
验证新旧核心和组合 | PKG-DASH-04, CORE-22 | 固定 dash.js 4.5.2/5.2.1 与本地 DASH 实际清晰度/音轨、高亮、Auto 和换源组合 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-DASH-06 | artplayer-plugin-dash-control
验证分发并同步文档 | PKG-DASH-05, ENG-07 | dash.control.js、支持的 dash.js 版本与产物 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
@@ -481,3 +481,4 @@
- PKG-HLS-SDK-01: [记录](changes/2026-09-12-PKG-HLS-SDK-01-integration.md) [记录](baselines/hls-sdk-validation.json) [记录](baselines/hls-sdk-diagnostics.json)
- PKG-DASH-01: [记录](changes/2026-09-12-PKG-DASH-01-contract.md) [记录](baselines/dash-control-contract.md) [记录](baselines/dash-control-release.json)
- PKG-DASH-02: [记录](changes/2026-09-12-PKG-DASH-02-tests.md) [记录](dash-validation.md) [记录](baselines/dash-validation.json)
+- PKG-DASH-03: [记录](changes/2026-09-12-PKG-DASH-03-runtime.md) [记录](baselines/dash-runtime.json) [记录](dash-validation.md)
diff --git a/refactor/progress.md b/refactor/progress.md
index ebcfa9d4b..1a83c02f1 100644
--- a/refactor/progress.md
+++ b/refactor/progress.md
@@ -1,5 +1,20 @@
# 进度与证据
+## 最新完成:DASH-03 SDK 适配与生命周期源码改造
+
+DASH 源码已拆为入口生命周期、SDK 适配、selector 模型、菜单所有权四个 JS 模块。
+恢复发布版 4.x 方法表,保留 5.x 稳定 ID;修复空菜单残留、过期引用、销毁/安装异常清理、
+重复标签/零 ID/唯一克隆轨道高亮,以及清理重入误删新 UI 和销毁后继续格式化的问题。
+
+源码 116 项、三格式两代 SDK Node 266 项通过;正式插件 main/legacy 三引擎、新旧核心
+DOM/原生 MP4 各 84 项通过。完整 CI 712 项通过,实际 SDK/MPD/ABR 与设备验证仍未完成。
+初始 21 项失败和两组重入/格式化失败、VM 数组比较修正证据均保留,未以重试跳过制造通过。
+DASH-LIFE-01 关闭;DASH-SDK-01/DASH-STATE-01 仍需 05 实际 SDK 验证。
+
+当前 217 项:73 done、3 doing、141 todo。下一步 DASH-04 迁移这四个模块和公开类型到严格 TS。
+详见 [源码变更](changes/2026-09-12-PKG-DASH-03-runtime.md)、[执行证据](baselines/dash-runtime.json)
+及 [包内架构](../packages/artplayer-plugin-dash-control/ARCHITECTURE.md)。独立本地提交,不推送/发布。
+
## 最新完成:DASH-02 行为测试与真实 DOM 交互
保留原有 5 项稳定 ID 回归,新增发布三格式/冻结工作区/候选源码契约测试,定向 74 项通过。
diff --git a/refactor/risk-table.md b/refactor/risk-table.md
index af2d04f3d..0df46db58 100644
--- a/refactor/risk-table.md
+++ b/refactor/risk-table.md
@@ -136,7 +136,7 @@
| CHAPTER-LAYOUT-01 | resolved / 已复现 | 长章节标题超出窄播放器进度条宽度 | PKG-CHAPTER-05 |
| CHAPTER-TIMING-01 | open / 已复现 | Windows WebKit 清晰度切换曾在等待窗口内未见 restart,稍后状态恢复 | PKG-CHAPTER-05 |
| DASH-SDK-01 | open / 已复现 | npm 发布版 4.x 与工作区仅 5.x 的 SDK 接口不兼容 | PKG-DASH-02, PKG-DASH-03, PKG-DASH-05 |
-| DASH-LIFE-01 | open / 已复现 | DASH 空拓扑残留 UI,监听和旧选择回调缺乏关闭守卫 | PKG-DASH-02, PKG-DASH-03 |
+| DASH-LIFE-01 | resolved / 已复现 | DASH 空拓扑残留 UI,监听和旧选择回调缺乏关闭守卫 | PKG-DASH-02, PKG-DASH-03 |
| DASH-STATE-01 | open / 已复现 | DASH 当前项高亮使用真值 ID、音轨身份和标签去重,边界待核实 | PKG-DASH-02, PKG-DASH-03, PKG-DASH-05 |
| DASH-TYPE-01 | open / 源码/产物事实 | DASH 类型未表达默认工厂参数、SDK 回调对象和现代模块入口 | PKG-DASH-04 |
| DASH-DEMO-01 | open / 源码/产物事实 | DASH 示例仅新 SDK 且重复安装会累积销毁回调 | PKG-DASH-06 |
diff --git a/refactor/risks.json b/refactor/risks.json
index 861bf99a1..5cd84de15 100644
--- a/refactor/risks.json
+++ b/refactor/risks.json
@@ -2848,7 +2848,9 @@
"packages/artplayer-plugin-dash-control/types/artplayer-plugin-dash-control.d.ts",
"docs/assets/example/dash.control.js",
"test/dash-contract.test.js",
- "refactor/dash-validation.md"
+ "refactor/dash-validation.md",
+ "test/dash-lifecycle.test.js",
+ "refactor/baselines/dash-runtime.json"
],
"compatibleResolution": "按方法能力适配两代已存在用法,保留 v5 稳定 ID,不能要求旧消费者改用新 SDK。",
"closureCriteria": "归档旧用法、v4/v5 受控及实际媒体矩阵通过,清晰度键不混用。"
@@ -2857,7 +2859,7 @@
"id": "DASH-LIFE-01",
"title": "DASH 空拓扑残留 UI,监听和旧选择回调缺乏关闭守卫",
"confirmation": "reproduced",
- "status": "open",
+ "status": "resolved",
"owners": [
"PKG-DASH-02",
"PKG-DASH-03"
@@ -2869,10 +2871,18 @@
"packages/artplayer-plugin-dash-control/types/artplayer-plugin-dash-control.d.ts",
"docs/assets/example/dash.control.js",
"test/dash-contract.test.js",
- "refactor/dash-validation.md"
+ "refactor/dash-validation.md",
+ "test/dash-lifecycle.test.js",
+ "refactor/baselines/dash-runtime.json"
],
"compatibleResolution": "仅释放插件自有 ready/restart/destroy 订阅和 UI,SDK 仍由调用方拥有;替换后旧回调不写旧实例。",
- "closureCriteria": "空列表、换源/SDK、销毁后引用、安装异常有回归,其他实例与 SDK 不被误释放。"
+ "closureCriteria": "空列表、换源/SDK、销毁后引用、安装异常有回归,其他实例与 SDK 不被误释放。",
+ "resolutionEvidence": [
+ "refactor/changes/2026-09-12-PKG-DASH-03-runtime.md",
+ "refactor/baselines/dash-runtime.json",
+ "packages/artplayer-plugin-dash-control/ARCHITECTURE.md"
+ ],
+ "resolutionRationale": "空拓扑/禁用/失败/销毁清理自有 UI 与订阅,旧 revision/SDK 引用无副作用;先红后绿、两代方法表及三格式 Node 生命周期、新旧核心 main/legacy 各 84 项真实 DOM 通过。只关闭插件所有权和引用缺陷,不替代实际 SDK/MPD、demo 或设备验证。"
},
{
"id": "DASH-STATE-01",
@@ -2891,7 +2901,9 @@
"packages/artplayer-plugin-dash-control/types/artplayer-plugin-dash-control.d.ts",
"docs/assets/example/dash.control.js",
"test/dash-contract.test.js",
- "refactor/dash-validation.md"
+ "refactor/dash-validation.md",
+ "test/dash-lifecycle.test.js",
+ "refactor/baselines/dash-runtime.json"
],
"compatibleResolution": "验证零 ID、重复标签和克隆音轨当前项,在保持原展示语义时按实际可选项定位高亮。",
"closureCriteria": "有受控和真实 SDK 证据说明选中的 representation/track 与 UI 一致。"
diff --git a/refactor/tasks.json b/refactor/tasks.json
index 777e2919f..b896ab4e8 100644
--- a/refactor/tasks.json
+++ b/refactor/tasks.json
@@ -2014,11 +2014,15 @@
"CORE-11",
"CORE-14"
],
- "status": "todo",
+ "status": "done",
"risk": "H",
"deliverable": "能力适配保留 SDK 4.x 与 5.x,稳定 ID 映射、ABR 状态、UI 清理和生命周期职责分离",
"acceptance": "结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过",
- "evidence": []
+ "evidence": [
+ "changes/2026-09-12-PKG-DASH-03-runtime.md",
+ "baselines/dash-runtime.json",
+ "dash-validation.md"
+ ]
},
{
"id": "PKG-DASH-04",
diff --git a/test/browser/dash-control.spec.js b/test/browser/dash-control.spec.js
index e986a0669..5ecda9c0f 100644
--- a/test/browser/dash-control.spec.js
+++ b/test/browser/dash-control.spec.js
@@ -10,22 +10,14 @@ let sourceCode
let evidence
test.beforeAll(async () => {
const { release } = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/dash-control-release.json', import.meta.url)))
-
const member = 'package/dist/artplayer-plugin-dash-control.js'
-
const bytes = readMember(await ensureArchive(release), member)
-
assert.equal(hash(bytes), release.files[member])
-
publishedCode = bytes.toString()
-
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'Controlled DASH source tests do not represent installed SDK/package acceptance')
-
sourceCode = process.env.ARTPLAYER_DASH_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_DASH_ARTIFACT, 'utf8') : await compilePackage(release.name, 'umd')
-
evidence = { published: release, sourceSHA256: hash(sourceCode), candidate: process.env.ARTPLAYER_DASH_ARTIFACT || 'workspace source build', scope: 'Real ArtPlayer DOM and native MP4 with controlled SDK methods. No dash.js instance, MPD, ABR or adaptive media validation.' }
})
-
test.afterEach(async ({ page }, testInfo) => {
const state = await page.evaluate(() => ({
calls: window.dashCalls,
@@ -36,23 +28,20 @@ test.afterEach(async ({ page }, testInfo) => {
audio: window.art?.controls['dash-audio']?.textContent,
settingRows: [...document.querySelectorAll('.art-setting-panel.art-current .art-setting-item')].map(row => ({ text: row.textContent, label: row.querySelector('.art-setting-item-left-text')?.textContent })),
})).catch(error => ({ error: error.message }))
-
await testInfo.attach('controlled-dash-state', { contentType: 'application/json', body: JSON.stringify(state) })
-
- if (!page.isClosed())
- await page.evaluate(() => window.art?.destroy())
+ if (!page.isClosed()) {
+ await page.evaluate(() => {
+ if (window.art && !window.art.isDestroy)
+ window.art.destroy()
+ })
+ }
})
-
-async function openDash(page, core, plugin, testInfo) {
+async function openDash(page, core, plugin, sdk, testInfo) {
await testInfo.attach('controlled-dash-inputs', { contentType: 'application/json', body: JSON.stringify(evidence) })
-
await page.goto(`/test/player.html?core=${core}`)
-
await page.addScriptTag({ content: plugin === 'published' ? publishedCode : sourceCode })
-
await page.evaluate((sdk) => {
window.dashCalls = []
-
window.art = new window.Artplayer({
container: '.player',
url: '/test/pattern.mp4',
@@ -62,13 +51,9 @@ async function openDash(page, core, plugin, testInfo) {
const levels = sdk === 4
? [{ qualityIndex: 0, height: 360 }, { qualityIndex: 1, height: 720 }]
: [{ id: 'low', height: 360 }, { id: 'high', height: 720 }]
-
const tracks = [{ id: 'en', lang: 'en' }, { id: 'fr', lang: 'fr' }]
-
window.dashState = { levels, tracks, track: tracks[0], current: levels[0], quality: 0, settings: { streaming: { abr: { autoSwitchBitrate: { video: true } } } } }
-
const state = window.dashState
-
art.dash = {
getVideoElement: () => art.template.$video,
getSettings: () => state.settings,
@@ -83,7 +68,6 @@ async function openDash(page, core, plugin, testInfo) {
window.dashCalls.push(['track', track.id, tracks.includes(track)])
},
}
-
if (sdk === 4) {
Object.assign(art.dash, {
getBitrateInfoListFor: () => levels,
@@ -104,68 +88,83 @@ async function openDash(page, core, plugin, testInfo) {
},
})
}
-
return window.artplayerPluginDashControl({ quality: { control: true, setting: true }, audio: { control: true, setting: true } })(art)
}],
})
-
document.querySelector('#play').onclick = () => window.art.play()
-
document.querySelector('#pause').onclick = () => window.art.pause()
- }, plugin === 'published' ? 4 : 5)
-
+ }, sdk)
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
-
await expect(page.locator('.art-control-dash-quality .art-selector-value')).toHaveText('Auto')
-
await expect(page.locator('.art-control-dash-audio .art-selector-value')).toHaveText('en')
}
-
for (const core of ['published', 'candidate']) {
- for (const plugin of ['published', 'candidate']) {
+ for (const [plugin, sdk] of [['published', 4], ['candidate', 4], ['candidate', 5]]) {
for (const surface of ['control', 'setting']) {
for (const type of ['quality', 'audio']) {
- test(`${core} core + ${plugin} DASH: controlled SDK ${type} selection through real ${surface} DOM`, async ({ page }, testInfo) => {
- await openDash(page, core, plugin, testInfo)
-
+ test(`${core} core + ${plugin} DASH SDK ${sdk}: controlled SDK ${type} selection through real ${surface} DOM`, async ({ page }, testInfo) => {
+ await openDash(page, core, plugin, sdk, testInfo)
const text = type === 'quality' ? '720p' : 'fr'
-
if (surface === 'control') {
const control = page.locator(`.art-control-dash-${type}`)
-
await control.hover()
-
await expect(control.locator('.art-selector-list')).toHaveCSS('opacity', '1')
-
await control.locator('.art-selector-item').filter({ hasText: new RegExp(`^${text}$`) }).click()
}
else {
await page.locator('.art-control-setting').click()
-
await page.locator(`[data-name="dash-${type}"]`).click()
-
await page.locator('.art-setting-panel.art-current .art-setting-item-left-text').filter({ hasText: new RegExp(`^${text}$`) }).click()
}
-
const expected = type === 'quality'
- ? [['auto', false], plugin === 'published' ? ['qualityIndex', 'video', 1] : ['representationId', 'video', 'high']]
+ ? [['auto', false], sdk === 4 ? ['qualityIndex', 'video', 1] : ['representationId', 'video', 'high']]
: [['track', 'fr', true]]
-
await expect.poll(() => page.evaluate(() => window.dashCalls)).toEqual(expected)
-
await expect(page.locator(`.art-control-dash-${type} .art-selector-value`)).toHaveText(text)
-
expect(await page.evaluate(type => window.art.setting.find(`dash-${type}`).tooltip, type)).toBe(text)
-
await testInfo.attach('controlled-dash-selection', { contentType: 'image/png', body: await page.screenshot() })
-
await page.locator('#play').click()
-
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.2)
-
expect(await page.evaluate(() => window.art.video.error)).toBeNull()
})
}
}
}
}
+for (const core of ['published', 'candidate']) {
+ for (const sdk of [4, 5]) {
+ test(`${core} core + candidate DASH SDK ${sdk}: empty topology and destroy clear real menus and retained callbacks`, async ({ page }, testInfo) => {
+ await openDash(page, core, 'candidate', sdk, testInfo)
+ const result = await page.evaluate((sdk) => {
+ const art = window.art
+ const plugin = art.plugins.artplayerPluginDashControl
+ const quality = art.controls.cache.get('dash-quality').option
+ const audio = art.setting.find('dash-audio')
+ const state = window.dashState
+ const levels = state.levels
+ const tracks = state.tracks
+ state.levels.splice(0)
+ state.tracks = []
+ plugin.update()
+ const cleared = !document.querySelector('.art-control-dash-quality, .art-control-dash-audio') && !art.setting.find('dash-quality') && !art.setting.find('dash-audio')
+ window.dashCalls.length = 0
+ quality.onSelect(quality.selector[0])
+ audio.onSelect(audio.selector[1])
+ const staleWrites = window.dashCalls.length
+ // Replace the lists after the empty-topology observation.
+ state.levels.push(...(sdk === 4 ? [{ qualityIndex: 0, height: 360 }, { qualityIndex: 1, height: 720 }] : [{ id: 'low', height: 360 }, { id: 'high', height: 720 }]))
+ state.tracks = tracks
+ plugin.update()
+ const restored = Boolean(art.controls['dash-quality'] && art.controls['dash-audio'])
+ const retained = art.controls.cache.get('dash-quality').option
+ art.destroy(false)
+ window.dashCalls.length = 0
+ retained.onSelect(retained.selector[0])
+ plugin.update()
+ return { cleared, staleWrites, restored, afterDestroyWrites: window.dashCalls.length, sameList: state.levels === levels }
+ }, sdk)
+ expect(result).toEqual({ cleared: true, staleWrites: 0, restored: true, afterDestroyWrites: 0, sameList: true })
+ await testInfo.attach('controlled-dash-cleanup', { contentType: 'application/json', body: JSON.stringify(result) })
+ })
+ }
+}
diff --git a/test/dash-lifecycle.test.js b/test/dash-lifecycle.test.js
new file mode 100644
index 000000000..a25669340
--- /dev/null
+++ b/test/dash-lifecycle.test.js
@@ -0,0 +1,259 @@
+import assert from 'node:assert/strict'
+// eslint-disable-next-line test/no-import-node-test -- Runtime lifecycle contracts use Node's built-in runner.
+import test from 'node:test'
+import { bothMenus, dashHost, dashImplementations } from './helpers/dash-control.js'
+
+const candidates = (await dashImplementations()).filter(item => item.name.startsWith('source') || item.name.startsWith('artifact'))
+const { factory } = candidates[0]
+for (const { sdk, name, factory } of candidates) {
+ test(`DASH ${sdk} (${name}): supports the original SDK method generation and stable selection keys`, () => {
+ const host = dashHost(sdk)
+ factory(bothMenus())(host.art).update()
+ const menu = host.controls.get('dash-quality')
+ const selected = menu.selector.find(item => item.html === '720p')
+ host.calls.length = 0
+ assert.equal(menu.onSelect(selected), '720p')
+ assert.deepEqual(host.calls[1], sdk === 4 ? ['setQualityFor', 'video', 1] : ['setRepresentationForTypeById', 'video', 'high'])
+ })
+
+ test(`DASH ${sdk} (${name}): empty lists clear owned UI and invalidate retained selectors`, () => {
+ const host = dashHost(sdk)
+ const plugin = factory(bothMenus())(host.art)
+ plugin.update()
+ const menu = host.controls.get('dash-quality')
+ const { levels, tracks } = host.state
+ host.state.levels = []
+ host.state.tracks = []
+ plugin.update()
+ assert.equal(host.controls.size + host.settings.size, 0)
+ host.calls.length = 0
+ assert.equal(menu.onSelect(menu.selector[0]), '1080p')
+ assert.equal(host.calls.length, 0)
+ host.state.levels = levels
+ host.state.tracks = tracks
+ host.art.emit('restart')
+ assert.equal(host.controls.size + host.settings.size, 4)
+ })
+
+ test(`DASH ${sdk} (${name}): disabling one surface removes only that surface`, () => {
+ const host = dashHost(sdk)
+ const options = bothMenus()
+ const plugin = factory(options)(host.art)
+ plugin.update()
+ options.quality.control = false
+ options.audio.setting = false
+ plugin.update()
+ assert.deepEqual([...host.controls.keys()], ['dash-audio'])
+ assert.deepEqual([...host.settings.keys()], ['dash-quality'])
+ })
+
+ test(`DASH ${sdk} (${name}): destroy unsubscribes and makes update/retained callbacks inert without owning SDK`, () => {
+ const host = dashHost(sdk)
+ const other = dashHost(sdk)
+ factory(bothMenus())(other.art).update()
+ const plugin = factory(bothMenus())(host.art)
+ plugin.update()
+ const menu = host.controls.get('dash-audio')
+ host.art.destroy()
+ assert.equal(host.controls.size + host.settings.size, 0)
+ assert.equal([...host.listeners.values()].reduce((sum, set) => sum + set.size, 0), 0)
+ host.calls.length = 0
+ plugin.update()
+ host.art.emit('ready')
+ host.art.emit('restart')
+ host.art.destroy()
+ assert.equal(menu.onSelect(menu.selector[1]), 'fr')
+ assert.equal(host.calls.length, 0)
+ assert.equal(other.controls.size + other.settings.size, 4)
+ })
+
+ test(`DASH ${sdk} (${name}): replacing SDK immediately silences old callbacks before and after update`, () => {
+ const host = dashHost(sdk)
+ const next = dashHost(sdk)
+ const plugin = factory(bothMenus())(host.art)
+ plugin.update()
+ const menu = host.controls.get('dash-quality')
+ next.state.video = host.video
+ host.art.dash = next.dash
+ host.calls.length = 0
+ menu.onSelect(menu.selector[0])
+ assert.equal(host.calls.length, 0)
+ plugin.update()
+ host.calls.length = 0
+ menu.onSelect(menu.selector[0])
+ assert.equal(host.calls.length, 0)
+ assert.equal(next.calls.length, 0)
+ })
+
+ test(`DASH ${sdk} (${name}): duplicate labels preserve the current selectable object`, () => {
+ const host = dashHost(sdk)
+ host.state.settings.streaming.abr.autoSwitchBitrate.video = false
+ host.state.levels[1].height = 360
+ host.state.tracks[1].lang = 'en'
+ host.state.currentTrack = host.state.tracks[1]
+ factory(bothMenus())(host.art).update()
+ const quality = host.controls.get('dash-quality')
+ const selected = quality.selector.filter(item => item.default)
+ assert.equal(selected.length, 1)
+ host.calls.length = 0
+ quality.onSelect(selected[0])
+ assert.deepEqual(host.calls[1], sdk === 4 ? ['setQualityFor', 'video', 1] : ['setRepresentationForTypeById', 'video', 'high'])
+ const audio = host.controls.get('dash-audio').selector.filter(item => item.default)
+ assert.equal(audio.length, 1)
+ assert.equal(audio[0].value, host.state.tracks[1])
+ })
+
+ test(`DASH ${sdk} (${name}): cloned current track matches a unique track but never fabricates ambiguous selection`, () => {
+ const host = dashHost(sdk)
+ const plugin = factory(bothMenus())(host.art)
+ host.state.currentTrack = { ...host.state.tracks[1] }
+ plugin.update()
+ assert.equal(host.controls.get('dash-audio').selector.find(item => item.default).value, host.state.tracks[1])
+ host.state.tracks.push({ ...host.state.tracks[1], lang: 'fr-duplicate' })
+ host.state.currentTrack = { id: 'fr', index: 1 }
+ plugin.update()
+ assert.equal(host.controls.get('dash-audio').selector.some(item => item.default), false)
+ })
+
+ test(`DASH ${sdk} (${name}): reentrant getName destruction cannot repopulate menus`, () => {
+ const host = dashHost(sdk)
+ const options = bothMenus()
+ let calls = 0
+ options.quality.getName = (level) => {
+ calls++
+ host.art.destroy()
+ return `${level.height}p`
+ }
+ factory(options)(host.art).update()
+ assert.equal(host.controls.size + host.settings.size, 0)
+ assert.equal(host.calls.length, 0)
+ assert.equal(calls, 1)
+ })
+
+ test(`DASH ${sdk} (${name}): synchronous SDK destruction stops later selection writes and notifications`, () => {
+ const host = dashHost(sdk)
+ factory(bothMenus())(host.art).update()
+ const menu = host.controls.get('dash-quality')
+ const original = host.dash.updateSettings
+ host.dash.updateSettings = (value) => {
+ original.call(host.dash, value)
+ host.art.destroy()
+ }
+ host.calls.length = 0
+ assert.equal(menu.onSelect(menu.selector[0]), '1080p')
+ assert.equal(host.calls.some(call => /^set(?:Quality|Representation)/.test(call[0])), false)
+ assert.equal(host.calls.some(call => call[0] === 'notice' || call[0].endsWith('.check')), false)
+ })
+
+ test(`DASH ${sdk} (${name}): rendering errors clear partial own menus and allow an explicit retry`, () => {
+ const host = dashHost(sdk)
+ const failure = new Error('setting rendering failed')
+ const original = host.art.setting.update
+ host.art.setting.update = (option) => {
+ original(option)
+ throw failure
+ }
+ const plugin = factory(bothMenus())(host.art)
+ assert.throws(plugin.update, error => error === failure)
+ assert.equal(host.controls.size + host.settings.size, 0)
+ host.art.setting.update = original
+ plugin.update()
+ assert.equal(host.controls.size + host.settings.size, 4)
+ })
+
+ test(`DASH ${sdk} (${name}): nested update from a formatter keeps the latest render`, () => {
+ const host = dashHost(sdk)
+ const options = bothMenus()
+ const plugin = factory(options)(host.art)
+ let nested = false
+ options.quality.getName = (level) => {
+ if (!nested) {
+ nested = true
+ host.state.levels = [host.state.levels[2]]
+ plugin.update()
+ }
+ return `${level.height}p`
+ }
+ plugin.update()
+ assert.deepEqual(Array.from(host.controls.get('dash-quality').selector, item => item.html), ['1080p', 'Auto'])
+ assert.equal(host.controls.size + host.settings.size, 4)
+ })
+
+ test(`DASH ${sdk} (${name}): a cleanup-triggered restart does not remove newly installed menus`, () => {
+ const host = dashHost(sdk)
+ const plugin = factory(bothMenus())(host.art)
+ plugin.update()
+ const { levels } = host.state
+ host.state.levels = []
+ const remove = host.art.controls.remove
+ host.art.controls.remove = (name) => {
+ remove(name)
+ host.state.levels = levels
+ plugin.update()
+ }
+ plugin.update()
+ assert.equal(host.controls.size + host.settings.size, 4)
+ })
+
+ test(`DASH ${sdk} (${name}): clearing a caller-replaced registry entry preserves the caller entry`, () => {
+ const host = dashHost(sdk)
+ host.art.controls.cache = { get: name => ({ option: host.controls.get(name) }) }
+ host.art.setting.find = name => host.settings.get(name)
+ factory(bothMenus())(host.art).update()
+ const replacement = { onSelect() {} }
+ host.controls.set('dash-quality', replacement)
+ host.settings.set('dash-audio', replacement)
+ host.art.destroy()
+ assert.equal(host.controls.get('dash-quality'), replacement)
+ assert.equal(host.settings.get('dash-audio'), replacement)
+ assert.equal(host.controls.size + host.settings.size, 2)
+ })
+
+ test(`DASH ${sdk} (${name}): cleanup failure does not retain other surfaces or listeners`, () => {
+ const host = dashHost(sdk)
+ factory(bothMenus())(host.art).update()
+ const failure = new Error('control cleanup failed')
+ const remove = host.art.controls.remove
+ host.art.controls.remove = (name) => {
+ remove(name)
+ throw failure
+ }
+ assert.throws(() => host.art.destroy(), error => error === failure)
+ assert.equal(host.controls.size + host.settings.size, 0)
+ assert.equal([...host.listeners.values()].reduce((sum, set) => sum + set.size, 0), 0)
+ host.art.destroy()
+ })
+}
+
+test('DASH v5: a numeric zero ID is a valid manually selected representation', () => {
+ const host = dashHost(5)
+ host.state.levels[1].id = 0
+ host.state.settings.streaming.abr.autoSwitchBitrate.video = false
+ factory(bothMenus())(host.art).update()
+ assert.deepEqual(host.controls.get('dash-quality').selector.filter(item => item.default).map(item => item.id), [0])
+})
+
+test('DASH installation failure removes even the listener registered by a throwing host', () => {
+ const host = dashHost()
+ const failure = new Error('subscription failed')
+ const on = host.art.on
+ host.art.on = (name, callback) => {
+ on(name, callback)
+ if (name === 'destroy')
+ throw failure
+ }
+ assert.throws(() => factory(bothMenus())(host.art), error => error === failure)
+ assert.equal([...host.listeners.values()].reduce((sum, set) => sum + set.size, 0), 0)
+})
+
+test('DASH reentrant destruction during subscription cannot install later listeners', () => {
+ const host = dashHost()
+ const on = host.art.on
+ host.art.on = (name, callback) => {
+ on(name, callback)
+ if (name === 'destroy')
+ callback()
+ }
+ factory()(host.art)
+ assert.equal([...host.listeners.values()].reduce((sum, set) => sum + set.size, 0), 0)
+})
diff --git a/test/helpers/dash-control.js b/test/helpers/dash-control.js
index 6dffe4307..44c67f7fa 100644
--- a/test/helpers/dash-control.js
+++ b/test/helpers/dash-control.js
@@ -22,6 +22,7 @@ export async function dashImplementations() {
const { release } = baseline
const archive = await ensureArchive(release)
const implementations = [{ name: 'source', sdk: 5, factory: (await loadPackage(release.name)).default }]
+ implementations.push({ name: 'source-v4', sdk: 4, factory: implementations[0].factory })
const historicalFile = `packages/${release.name}/dist/artplayer-plugin-dash-control.js`
const historical = execFileSync('git', ['show', `${baseline.sourceCommit}:${historicalFile}`])
assert.equal(hash(historical.toString().replace(/\r\n/g, '\n')), baseline.source[historicalFile])
@@ -34,7 +35,9 @@ export async function dashImplementations() {
}
for (const filename of (process.env.ARTPLAYER_TEST_DASH || '').split(path.delimiter).filter(Boolean)) {
const bytes = fs.readFileSync(filename)
- implementations.push({ name: `artifact-${path.basename(filename)}`, sdk: 5, factory: await factoryFromBytes(bytes, filename.endsWith('.mjs')) })
+ const factory = await factoryFromBytes(bytes, filename.endsWith('.mjs'))
+ for (const sdk of [4, 5])
+ implementations.push({ name: `artifact-${path.basename(filename)}-v${sdk}`, sdk, factory })
}
return implementations
}