refactor(dash): [PKG-DASH-03] adapt SDK generations and own menu lifecycles

This commit is contained in:
Harvey Zhao committed 2026-09-12 04:59:19 +08:00
1 parent 762cc63431
commit 7de04573b6
24 files changed
+2107 -533

No files matched your search

@@ -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:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>',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:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>',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",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),u=e(o,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),a=[];function f(t=()=>!0){let e;for(const o of[s.clear,u.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function d(){if(i||o.isDestroy)return;const e=++c,a=o.dash,d=()=>!i&&!o.isDestroy&&e===c&&o.dash===a,h=()=>d()&&a.getVideoElement()===r&&d();try{if(l(a.getVideoElement()===r,'Cannot find instance of DASH from "art.dash"'),!d())return;const e=n.quality||{},o=function(e,n,o){const r=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(e),l=r.levels();if(!o()||!l?.length)return null;const i=n.auto||"Auto",c=n.getName||(t=>`${t.height}p`),s=r.current(l);if(!o())return null;const u=e.getSettings().streaming.abr.autoSwitchBitrate.video;if(!o())return null;const a=!u&&s?c(s):i;if(!o())return null;const f=[];for(const[t,h]of l.entries()){const e=c(h);if(!o())return null;f.push({html:e,...r.item(h,t,s,u)})}const d=t(f).sort((t,e)=>e.value-t.value);return d.push({html:i,value:"auto",default:u}),{html:a,title:n.title||"Quality",selector:d,select:r.select}}(a,e,h);if(!h())return;if(s.update(e,o,h),!h())return;const i=n.audio||{},c=function(e,n,o){const r=e.getTracksFor("audio");if(!o()||!r?.length)return null;const l=n.auto||"Auto",i=n.getName||(t=>t.lang||t.id),c=e.getCurrentTrackFor("audio")||r[0];if(!o())return null;const s=c?i(c):l;if(!o())return null;const u=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=t.filter(t=>["id","index","lang"].every(n=>null==e[n]||t[n]===e[n]));return 1===n.length?n[0]:void 0}(r,c),a=[];for(const t of r){const e=i(t);if(!o())return null;a.push({html:e,value:t,default:t===u})}const f=t(a);return{html:s,title:n.title||"Audio",selector:f,select:t=>e.setCurrentTrack(t.value)}}(a,i,h);h()&&u.update(i,c,h)}catch(g){if(d()){const t=++c;try{f(()=>c===t)}catch(p){console.warn("ArtPlayer DASH cleanup failed:",p)}}throw g}}function h(){if(i)return;let t;i=!0,c++;const e=[f,...a.splice(0).map(([t,e])=>()=>o.off(t,e))Line truncated
@@ -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:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>',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:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>',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",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),a=c(e,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),f=[];function d(t=()=>!0){let e;for(const o of[s.clear,a.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function h(){if(r||e.isDestroy)return;const c=++u,f=e.dash,h=()=>!r&&!e.isDestroy&&c===u&&e.dash===f,p=()=>h()&&f.getVideoElement()===n&&h();try{if(o(f.getVideoElement()===n,'Cannot find instance of DASH from "art.dash"'),!h())return;const e=t.quality||{},r=function(t,e,n){const o=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(t),r=o.levels();if(!n()||!(null==r?void 0:r.length))return null;const c=e.auto||"Auto",u=e.getName||(t=>`${t.height}p`),s=o.current(r);if(!n())return null;const a=t.getSettings().streaming.abr.autoSwitchBitrate.video;if(!n())return null;const f=!a&&s?u(s):c;if(!n())return null;const d=[];for(const[i,p]of r.entries()){const t=u(p);if(!n())return null;d.push(l({html:t},o.item(p,i,s,a)))}const h=i(d).sort((t,e)=>e.value-t.value);return h.push({html:c,value:"auto",default:a}),{html:f,title:e.title||"Quality",selector:h,select:o.select}}(f,e,p);if(!p())return;if(s.update(e,r,p),!p())return;const c=t.audio||{},u=function(t,e,n){const o=t.getTracksFor("audio");if(!n()||!(null==o?void 0:o.length))return null;const r=e.auto||"Auto",l=e.getName||(t=>t.lang||t.id),c=t.getCurrentTrackFor("audio")||o[0];if(!n())return null;const u=c?l(c):r;if(!n())return null;const s=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=t.filter(t=>["id","index","lang"].every(n=>null==e[n]||t[n]===e[n]));return 1===n.lLine truncated
+242 -145
View File
@@ -5,163 +5,260 @@
* Released under the MIT License.
*/
const $audio = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>';
const $quality = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
function 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 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
function artplayerPluginDashControl(option = {}) {
return (art) => {
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 {
+2 -2
View File
@@ -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",
@@ -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.
@@ -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.
@@ -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:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>',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:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>',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",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),u=e(o,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),a=[];function f(t=()=>!0){let e;for(const o of[s.clear,u.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function d(){if(i||o.isDestroy)return;const e=++c,a=o.dash,d=()=>!i&&!o.isDestroy&&e===c&&o.dash===a,h=()=>d()&&a.getVideoElement()===r&&d();try{if(l(a.getVideoElement()===r,'Cannot find instance of DASH from "art.dash"'),!d())return;const e=n.quality||{},o=function(e,n,o){const r=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(e),l=r.levels();if(!o()||!l?.length)return null;const i=n.auto||"Auto",c=n.getName||(t=>`${t.height}p`),s=r.current(l);if(!o())return null;const u=e.getSettings().streaming.abr.autoSwitchBitrate.video;if(!o())return null;const a=!u&&s?c(s):i;if(!o())return null;const f=[];for(const[t,h]of l.entries()){const e=c(h);if(!o())return null;f.push({html:e,...r.item(h,t,s,u)})}const d=t(f).sort((t,e)=>e.value-t.value);return d.push({html:i,value:"auto",default:u}),{html:a,title:n.title||"Quality",selector:d,select:r.select}}(a,e,h);if(!h())return;if(s.update(e,o,h),!h())return;const i=n.audio||{},c=function(e,n,o){const r=e.getTracksFor("audio");if(!o()||!r?.length)return null;const l=n.auto||"Auto",i=n.getName||(t=>t.lang||t.id),c=e.getCurrentTrackFor("audio")||r[0];if(!o())return null;const s=c?i(c):l;if(!o())return null;const u=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=t.filter(t=>["id","index","lang"].every(n=>null==e[n]||t[n]===e[n]));return 1===n.length?n[0]:void 0}(r,c),a=[];for(const t of r){const e=i(t);if(!o())return null;a.push({html:e,value:t,default:t===u})}const f=t(a);return{html:s,title:n.title||"Audio",selector:f,select:t=>e.setCurrentTrack(t.value)}}(a,i,h);h()&&u.update(i,c,h)}catch(g){if(d()){const t=++c;try{f(()=>c===t)}catch(p){console.warn("ArtPlayer DASH cleanup failed:",p)}}throw g}}function h(){if(i)return;let t;i=!0,c++;const e=[f,...a.splice(0).map(([t,e])=>()=>o.off(t,e))Line truncated
@@ -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:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>',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:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>',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",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),a=c(e,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),f=[];function d(t=()=>!0){let e;for(const o of[s.clear,a.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function h(){if(r||e.isDestroy)return;const c=++u,f=e.dash,h=()=>!r&&!e.isDestroy&&c===u&&e.dash===f,p=()=>h()&&f.getVideoElement()===n&&h();try{if(o(f.getVideoElement()===n,'Cannot find instance of DASH from "art.dash"'),!h())return;const e=t.quality||{},r=function(t,e,n){const o=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(t),r=o.levels();if(!n()||!(null==r?void 0:r.length))return null;const c=e.auto||"Auto",u=e.getName||(t=>`${t.height}p`),s=o.current(r);if(!n())return null;const a=t.getSettings().streaming.abr.autoSwitchBitrate.video;if(!n())return null;const f=!a&&s?u(s):c;if(!n())return null;const d=[];for(const[i,p]of r.entries()){const t=u(p);if(!n())return null;d.push(l({html:t},o.item(p,i,s,a)))}const h=i(d).sort((t,e)=>e.value-t.value);return h.push({html:c,value:"auto",default:a}),{html:f,title:e.title||"Quality",selector:h,select:o.select}}(f,e,p);if(!p())return;if(s.update(e,r,p),!p())return;const c=t.audio||{},u=function(t,e,n){const o=t.getTracksFor("audio");if(!n()||!(null==o?void 0:o.length))return null;const r=e.auto||"Auto",l=e.getName||(t=>t.lang||t.id),c=t.getCurrentTrackFor("audio")||o[0];if(!n())return null;const u=c?l(c):r;if(!n())return null;const s=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=t.filter(t=>["id","index","lang"].every(n=>null==e[n]||t[n]===e[n]));return 1===n.lLine truncated
@@ -5,163 +5,260 @@
* Released under the MIT License.
*/
const $audio = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>';
const $quality = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
function 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 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
function artplayerPluginDashControl(option = {}) {
return (art) => {
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 {
@@ -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 }
}
}
@@ -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) }
}
@@ -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 }
}
@@ -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)
}
},
}
}
+854
View File
@@ -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 <laozhaochaguan@gmail.com>",
"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 <laozhaochaguan@gmail.com>",
"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 <laozhaochaguan@gmail.com>",
"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."
}
@@ -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 的源码;该限制需知悉。
+19 -15
View File
@@ -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 发布准入仍待完成。
+3 -2
View File
@@ -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<br>核对包契约与历史用法 | BASE-05 | quality/audio、representation ID/Auto、getName 和 update | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-DASH-02 | artplayer-plugin-dash-control<br>建立特有行为与错误测试 | PKG-DASH-01, ENG-03, ENG-05 | 保留已有 5 项稳定 ID 回归,补发布版 SDK 4.x/当前 5.x、音轨/空列表/过滤/换源与关闭引用 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-DASH-03 | artplayer-plugin-dash-control<br>整理内部职责与资源 | PKG-DASH-02, CORE-11, CORE-14 | 能力适配保留 SDK 4.x 与 5.x,稳定 ID 映射、ABR 状态、UI 清理和生命周期职责分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
| PKG-DASH-03 | artplayer-plugin-dash-control<br>整理内部职责与资源 | PKG-DASH-02, CORE-11, CORE-14 | 能力适配保留 SDK 4.x 与 5.x,稳定 ID 映射、ABR 状态、UI 清理和生命周期职责分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-DASH-04 | artplayer-plugin-dash-control<br>迁移自有源码和公开类型 | PKG-DASH-03, ENG-04, ENG-06, CORE-07 | dash.js adapter、selector 和回调类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| PKG-DASH-05 | artplayer-plugin-dash-control<br>验证新旧核心和组合 | PKG-DASH-04, CORE-22 | 固定 dash.js 4.5.2/5.2.1 与本地 DASH 实际清晰度/音轨、高亮、Auto 和换源组合 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-DASH-06 | artplayer-plugin-dash-control<br>验证分发并同步文档 | PKG-DASH-05, ENG-07 | dash.control.js、支持的 dash.js 版本与产物 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
@@ -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)
+15
View File
@@ -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 项通过。
+1 -1
View File
@@ -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 |
+17 -5
View File
@@ -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 一致。"
+6 -2
View File
@@ -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",
+49 -50
View File
@@ -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) })
})
}
}
+259
View File
@@ -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)
})
+4 -1
View File
@@ -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
}