fix(dash): [PKG-DASH-05] synchronize SDK events and own refresh lifecycle

This commit is contained in:
Harvey Zhao committed 2026-09-12 06:34:31 +08:00
1 parent 2ad00730ee
commit a9cf50bc4e
24 files changed
+3913 -40

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){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function e(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){if(!o||r.get(n)!==o)return;r.delete(n);const l=t[n];"controls"===n&&l.cache?.get&&l.cache.get(e)?.option?.onSelect!==o||"setting"===n&&l.find&&l.find(e)?.onSelect!==o||l.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,s,u){if(!s)return void i();const a={};o=a;const f=()=>a===o&&u(),d=e=>f()?(s.select(e,f),f()?(t.notice.show=`${s.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:s.html,style:{padding:"0 10px"},selector:s.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:s.html,html:s.title,icon:n,width:200,selector:s.selector,onSelect:d})):l("setting"))},clear:i}}return function(n={}){return o=>{const r=o,{$video:l}=r.template,{errorHandle:i}=r.constructor.utils;let c=!1,s=0;const u=e(r,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),a=e(r,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),f=[];function d(t=()=>!0){let e;for(const o of[u.clear,a.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function h(){if(c||r.isDestroy)return;const e=++s,o=r.dash,f=()=>!c&&!r.isDestroy&&e===s&&r.dash===o,h=()=>f()&&o.getVideoElement()===l&&f();try{if(i(o.getVideoElement()===l,'Cannot find instance of DASH from "art.dash"'),!f())return;const e=n.quality||{},r=function(e,n,o){const r=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(e),l=r.levels();if(!o()||!l?.length)return null;const i=n.auto||"Auto",c=n.getName||(t=>`${t.height}p`),s=r.current(l);if(!o())return null;const u=e.getSettings().streaming.abr.autoSwitchBitrate.video;if(!o())return null;const a=!u&&s?c(s):i;if(!o())return null;const f=[];for(const[t,h]of l.entries()){const e=c(h);if(!o())return null;f.push({html:e,...r.item(h,t,s,u)})}const d=t(f).sort((t,e)=>e.value-t.value);return d.push({html:i,value:"auto",default:u}),{html:a,title:n.title||"Quality",selector:d,select:r.select}}(o,e,h);if(!h())return;if(u.update(e,r,h),!h())return;const c=n.audio||{},s=function(e,n,o){const r=e.getTracksFor("audio");if(!o()||!r?.length)return null;const l=n.auto||"Auto",i=n.getName||(t=>t.lang||t.id),c=e.getCurrentTrackFor("audio")||r[0];if(!o())return null;const s=c?i(c):l;if(!o())return null;const u=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=["id","index","lang"],o=t.filter(t=>n.every(n=>null==e[n]||t[n]===e[n]));return 1===o.length?o[0]:void 0}(r,c),a=[];for(const t of r){const e=i(t);if(!o())return null;a.push({html:e,value:t,default:t===u})}const f=t(a);return{html:s,title:n.title||"Audio",selector:f,select:t=>e.setCurrentTrack(t.value)}}(o,c,h);h()&&a.update(c,s,h)}catch(g){if(f()){const t=++s;try{d(()=>s===t)}catch(p){console.warn("ArtPlayer DASH cleanup failed:",p)}}throw g}}function g(){if(c)return;let t;c=!0,s++;const e=[d,...f.splice(0).map(([t,e])=>()=>r.Line truncated
!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 i(n,o=r.get(n)){if(!o||r.get(n)!==o)return;r.delete(n);const i=t[n];"controls"===n&&i.cache?.get&&i.cache.get(e)?.option?.onSelect!==o||"setting"===n&&i.find&&i.find(e)?.onSelect!==o||i.remove(e)}function c(){let t;o=void 0;for(const[n,o]of[...r])try{i(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(s,l,a){if(!l)return void c();const u={};o=u;const f=()=>u===o&&a(),d=e=>f()?(l.select(e,f),f()?(t.notice.show=`${l.title}: ${e.html}`,f()&&s.control&&t.controls.check(e),f()&&s.setting&&t.setting.check(e),e.html):e.html):e.html;s.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:l.html,style:{padding:"0 10px"},selector:l.selector,onSelect:d})):i("controls"),f()&&(s.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:l.html,html:l.title,icon:n,width:200,selector:l.selector,onSelect:d})):i("setting"))},clear:c}}return function(n={}){return o=>{const r=o,{$video:i}=r.template,{errorHandle:c}=r.constructor.utils;let s=!1,l=0;const a=e(r,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),u=e(r,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),f=[],d=function(t){let e;const n=n=>e===n&&t.active(n.dash),o=t=>t.dash.getSettings().streaming.abr.autoSwitchBitrate.video;function r(){const t=e;if(!t)return;let n;e=void 0,t.epoch++;for(const[e,r]of t.callbacks.splice(0))try{t.off.call(t.dash,e,r)}catch(o){n||(n=o)}if(n)throw n}function i(n,o){if(e===n){try{r()}catch(i){console.warn("ArtPlayer DASH subscription cleanup failed:",i)}if(!e&&t.active(n.dash))try{t.reset()}catch(i){console.warn("ArtPlayer DASH cleanup failed:",i)}}console.warn("ArtPlayer DASH refresh failed:",o)}function c(e){if(!n(e)||e.suspended||e.queued||e.running)return;e.queued=!0;const r=e.epoch;Promise.resolve().then(()=>{if(n(e)&&e.epoch===r&&!e.suspended){e.queued=!1,e.running=!0;try{e.automatic=o(e),n(e)&&t.refresh()}catch(c){i(e,c)}finally{e.running=!1}}})}return{bind:function(s){const l=e;if(l?.dash===s){const t=o(l);return void(n(l)&&(l.automatic=t,l.suspended=!1))}if(r(),e||!t.active(s)||"function"!==typeof s.on||"function"!==typeof s.off)return;const a={dash:s,off:s.off,callbacks:[],epoch:0,queued:!1,running:!1,suspended:!1,automatic:!1};e=a;const u=s.on;try{a.automatic=o(a);const e=["qualityChangeRequested","qualityChangeRendered","trackChangeRendered","streamUpdated","streamInitialized"].map(t=>[t,()=>{"streamUpdated"!==t&&"streamInitialized"!==t||(a.suspended=!1),c(a)}]);e.push(["playbackTimeUpdated",()=>{if(n(a)&&!a.suspended)try{const t=o(a);n(a)&&t!==a.automatic&&(a.automatic=t,c(a))}catch(t){i(a,t)}}]),e.push(["streamTeardownComplete",()=>{if(n(a)){a.epoch++,a.queued=!1,a.suspended=!0;try{t.reset()}catch(e){i(a,e)}}}]);for(const t of e){if(!n(a))break;a.callbacks.push(t),u.call(s,...t)}}catch(f){if(e===a)try{r()}catch(d){console.warn("ArtPlayer DASH subscription cleanup failed:",d)}throw f}},release:r}}({active:t=>!s&&!r.isDestroy&&r.dash===t,refresh:p,reset(){const t=++l;h(()=>t===l)}});function h(t=()=>!0){let e;for(const o of[a.clear,u.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function p(){if(s||r.isDestroy)return;const e=++l,o=r.dash,f=()=>!s&&!r.isDestroy&&e===l&&r.dash===o,p=()=>f()&&o.getVideoElement()===i&&f();try{if(c(o.getVideoElement()===i,'Cannot find instance of DASH from "art.dash"'),!f())return;if(d.bind(o),!p())return;const e=n.quality||{},r=function(e,n,o){const r=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForTyLine truncated
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,r=(e,n,o)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[n]=o,l=(t,l)=>{for(var i in l||(l={}))n.call(l,i)&&r(t,i,l[i]);if(e)for(var i of e(l))o.call(l,i)&&r(t,i,l[i]);return t};function i(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function c(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){var l,i,c,u;if(!o||r.get(n)!==o)return;r.delete(n);const s=t[n];"controls"===n&&(null==(l=s.cache)?void 0:l.get)&&(null==(c=null==(i=s.cache.get(e))?void 0:i.option)?void 0:c.onSelect)!==o||"setting"===n&&s.find&&(null==(u=s.find(e))?void 0:u.onSelect)!==o||s.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,u,s){if(!u)return void i();const a={};o=a;const f=()=>a===o&&s(),d=e=>f()?(u.select(e,f),f()?(t.notice.show=`${u.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:u.html,style:{padding:"0 10px"},selector:u.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:u.html,html:u.title,icon:n,width:200,selector:u.selector,onSelect:d})):l("setting"))},clear:i}}return function(t={}){return e=>{const n=e,{$video:o}=n.template,{errorHandle:r}=n.constructor.utils;let u=!1,s=0;const a=c(n,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),f=c(n,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),d=[];function h(t=()=>!0){let e;for(const o of[a.clear,f.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function p(){if(u||n.isDestroy)return;const e=++s,c=n.dash,d=()=>!u&&!n.isDestroy&&e===s&&n.dash===c,p=()=>d()&&c.getVideoElement()===o&&d();try{if(r(c.getVideoElement()===o,'Cannot find instance of DASH from "art.dash"'),!d())return;const e=t.quality||{},n=function(t,e,n){const o=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(t),r=o.levels();if(!n()||!(null==r?void 0:r.length))return null;const c=e.auto||"Auto",u=e.getName||(t=>`${t.height}p`),s=o.current(r);if(!n())return null;const a=t.getSettings().streaming.abr.autoSwitchBitrate.video;if(!n())return null;const f=!a&&s?u(s):c;if(!n())return null;const d=[];for(const[i,p]of r.entries()){const t=u(p);if(!n())return null;d.push(l({html:t},o.item(p,i,s,a)))}const h=i(d).sort((t,e)=>e.value-t.value);return h.push({html:c,value:"auto",default:a}),{html:f,title:e.title||"Quality",selector:h,select:o.select}}(c,e,p);if(!p())return;if(a.update(e,n,p),!p())return;const u=t.audio||{},s=function(t,e,n){const o=t.getTracksFor("audio");if(!n()||!(null==o?void 0:o.length))return null;const r=e.auto||"Auto",l=e.getName||(t=>t.lang||t.id),c=t.getCurrentTrackFor("audio")||o[0];if(!n())return null;const u=c?l(c):r;if(!n())return null;const s=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=["id","index","lang"],o=t.filter(t=>n.every(n=>null==e[n]||t[n]===e[n]));returLine truncated
!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,i=(t,i)=>{for(var l in i||(i={}))n.call(i,l)&&r(t,l,i[l]);if(e)for(var l of e(i))o.call(i,l)&&r(t,l,i[l]);return t};function l(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 i(n,o=r.get(n)){var i,l,c,a;if(!o||r.get(n)!==o)return;r.delete(n);const s=t[n];"controls"===n&&(null==(i=s.cache)?void 0:i.get)&&(null==(c=null==(l=s.cache.get(e))?void 0:l.option)?void 0:c.onSelect)!==o||"setting"===n&&s.find&&(null==(a=s.find(e))?void 0:a.onSelect)!==o||s.remove(e)}function l(){let t;o=void 0;for(const[n,o]of[...r])try{i(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,a,s){if(!a)return void l();const u={};o=u;const f=()=>u===o&&s(),d=e=>f()?(a.select(e,f),f()?(t.notice.show=`${a.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:a.html,style:{padding:"0 10px"},selector:a.selector,onSelect:d})):i("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:a.html,html:a.title,icon:n,width:200,selector:a.selector,onSelect:d})):i("setting"))},clear:l}}return function(t={}){return e=>{const n=e,{$video:o}=n.template,{errorHandle:r}=n.constructor.utils;let a=!1,s=0;const u=c(n,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),f=c(n,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),d=[],h=function(t){let e;const n=n=>e===n&&t.active(n.dash),o=t=>t.dash.getSettings().streaming.abr.autoSwitchBitrate.video;function r(){const t=e;if(!t)return;let n;e=void 0,t.epoch++;for(const[e,r]of t.callbacks.splice(0))try{t.off.call(t.dash,e,r)}catch(o){n||(n=o)}if(n)throw n}function i(n,o){if(e===n){try{r()}catch(i){console.warn("ArtPlayer DASH subscription cleanup failed:",i)}if(!e&&t.active(n.dash))try{t.reset()}catch(i){console.warn("ArtPlayer DASH cleanup failed:",i)}}console.warn("ArtPlayer DASH refresh failed:",o)}function l(e){if(!n(e)||e.suspended||e.queued||e.running)return;e.queued=!0;const r=e.epoch;Promise.resolve().then(()=>{if(n(e)&&e.epoch===r&&!e.suspended){e.queued=!1,e.running=!0;try{e.automatic=o(e),n(e)&&t.refresh()}catch(l){i(e,l)}finally{e.running=!1}}})}return{bind:function(c){const a=e;if((null==a?void 0:a.dash)===c){const t=o(a);return void(n(a)&&(a.automatic=t,a.suspended=!1))}if(r(),e||!t.active(c)||"function"!==typeof c.on||"function"!==typeof c.off)return;const s={dash:c,off:c.off,callbacks:[],epoch:0,queued:!1,running:!1,suspended:!1,automatic:!1};e=s;const u=c.on;try{s.automatic=o(s);const e=["qualityChangeRequested","qualityChangeRendered","trackChangeRendered","streamUpdated","streamInitialized"].map(t=>[t,()=>{"streamUpdated"!==t&&"streamInitialized"!==t||(s.suspended=!1),l(s)}]);e.push(["playbackTimeUpdated",()=>{if(n(s)&&!s.suspended)try{const t=o(s);n(s)&&t!==s.automatic&&(s.automatic=t,l(s))}catch(t){i(s,t)}}]),e.push(["streamTeardownComplete",()=>{if(n(s)){s.epoch++,s.queued=!1,s.suspended=!0;try{t.reset()}catch(e){i(s,e)}}}]);for(const t of e){if(!n(s))break;s.callbacks.push(t),u.call(c,...t)}}catch(f){if(e===s)try{r()}catch(d){console.warn("ArtPlayer DASH subscription cleanup failed:",d)}throw f}},release:r}}({active:t=>!a&&!n.isDestroy&&n.dash===t,refresh:y,reset(){const t=++s;p(()=>t===s)}});function p(t=()=>!0){let e;for(const o of[u.clear,f.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function y(){if(a||n.isDestroy)return;const e=++s,c=n.dash,d=()=>!a&&!n.isDestLine truncated
+145 -5
View File
@@ -170,6 +170,133 @@ function createMenu(art, name, icon) {
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 observeSDK(options) {
let binding;
const active = (record) => binding === record && options.active(record.dash);
const automatic = (record) => record.dash.getSettings().streaming.abr.autoSwitchBitrate.video;
function release() {
const record = binding;
if (!record)
return;
binding = void 0;
record.epoch++;
let failure;
for (const [name, callback] of record.callbacks.splice(0)) {
try {
record.off.call(record.dash, name, callback);
} catch (error) {
failure || (failure = error);
}
}
if (failure)
throw failure;
}
function fail(record, error) {
if (binding === record) {
try {
release();
} catch (cleanupError) {
console.warn("ArtPlayer DASH subscription cleanup failed:", cleanupError);
}
if (!binding && options.active(record.dash)) {
try {
options.reset();
} catch (cleanupError) {
console.warn("ArtPlayer DASH cleanup failed:", cleanupError);
}
}
}
console.warn("ArtPlayer DASH refresh failed:", error);
}
function schedule(record) {
if (!active(record) || record.suspended || record.queued || record.running)
return;
record.queued = true;
const epoch = record.epoch;
void Promise.resolve().then(() => {
if (!active(record) || record.epoch !== epoch || record.suspended)
return;
record.queued = false;
record.running = true;
try {
record.automatic = automatic(record);
if (active(record))
options.refresh();
} catch (error) {
fail(record, error);
} finally {
record.running = false;
}
});
}
function bind(dash) {
const previous = binding;
if (previous?.dash === dash) {
const value = automatic(previous);
if (active(previous)) {
previous.automatic = value;
previous.suspended = false;
}
return;
}
release();
if (binding || !options.active(dash) || typeof dash.on !== "function" || typeof dash.off !== "function")
return;
const record = { dash, off: dash.off, callbacks: [], epoch: 0, queued: false, running: false, suspended: false, automatic: false };
binding = record;
const on = dash.on;
try {
record.automatic = automatic(record);
const changed = ["qualityChangeRequested", "qualityChangeRendered", "trackChangeRendered", "streamUpdated", "streamInitialized"];
const entries = changed.map((name) => [name, () => {
if (name === "streamUpdated" || name === "streamInitialized")
record.suspended = false;
schedule(record);
}]);
entries.push(["playbackTimeUpdated", () => {
if (!active(record) || record.suspended)
return;
try {
const value = automatic(record);
if (active(record) && value !== record.automatic) {
record.automatic = value;
schedule(record);
}
} catch (error) {
fail(record, error);
}
}]);
entries.push(["streamTeardownComplete", () => {
if (!active(record))
return;
record.epoch++;
record.queued = false;
record.suspended = true;
try {
options.reset();
} catch (error) {
fail(record, error);
}
}]);
for (const entry of entries) {
if (!active(record))
break;
record.callbacks.push(entry);
on.call(dash, ...entry);
}
} catch (error) {
if (binding === record) {
try {
release();
} catch (cleanupError) {
console.warn("ArtPlayer DASH subscription cleanup failed:", cleanupError);
}
}
throw error;
}
}
return { bind, release };
}
function artplayerPluginDashControl(option = {}) {
return (player) => {
const art = player;
@@ -180,6 +307,14 @@ function artplayerPluginDashControl(option = {}) {
const quality = createMenu(art, "dash-quality", $quality);
const audio = createMenu(art, "dash-audio", $audio);
const subscriptions = [];
const observer = observeSDK({
active: (dash) => !closed && !art.isDestroy && art.dash === dash,
refresh: update,
reset() {
const version = ++revision;
clear(() => version === revision);
}
});
function clear(current = () => true) {
let failure;
for (const cleanup of [quality.clear, audio.clear]) {
@@ -205,6 +340,9 @@ function artplayerPluginDashControl(option = {}) {
errorHandle(dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"');
if (!current())
return;
observer.bind(dash);
if (!valid())
return;
const qualityConfig = option.quality || {};
const qualities = qualityModel(dash, qualityConfig, valid);
if (!valid())
@@ -219,10 +357,12 @@ function artplayerPluginDashControl(option = {}) {
} catch (error) {
if (current()) {
const cleanupVersion = ++revision;
try {
clear(() => revision === cleanupVersion);
} catch (cleanupError) {
console.warn("ArtPlayer DASH cleanup failed:", cleanupError);
for (const cleanup of [observer.release, () => clear(() => revision === cleanupVersion)]) {
try {
cleanup();
} catch (cleanupError) {
console.warn("ArtPlayer DASH cleanup failed:", cleanupError);
}
}
}
throw error;
@@ -234,7 +374,7 @@ function artplayerPluginDashControl(option = {}) {
closed = true;
revision++;
let failure;
const actions = [clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))];
const actions = [observer.release, clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))];
for (const action of actions) {
try {
action();
+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-lifecycle.test.js",
"test:dash-control": "node --test test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.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/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:unit": "node --test test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.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",
@@ -12,6 +12,8 @@ destroys it, changes its source, or adds a runtime dependency on dash.js.
| ------------------------------------------ | --------------------------------------------------------------------------------------- |
| `src/index.ts` | Deferred installation, media identity, update revisions, event subscription and cleanup |
| `src/sdk.ts` | 4.x qualityIndex vs 5.x representation ID access and manual/Auto selection |
| `src/sdk-events.ts` | Owned SDK subscriptions, coalesced refresh, teardown and asynchronous errors |
| `src/types.ts` | Narrow internal SDK, host, model and cleanup types |
| `src/mapping.ts` | Names, current item matching, duplicate labels and selector ordering |
| `src/menu.ts` | Existing control/setting registries, menu ownership and guarded selection callbacks |
| `src/audio.svg`, `src/quality.svg` | Existing setting icons |
@@ -19,7 +21,8 @@ destroys it, changes its source, or adds a runtime dependency on dash.js.
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. All five owned modules are checked with strict
it does not choose SDK versions. The event observer receives active/refresh/reset
callbacks and owns no DOM or ArtPlayer object. All six owned modules are checked with strict
TypeScript, noUncheckedIndexedAccess, and skipLibCheck=false; there are no remaining
owned JavaScript modules in this package.
@@ -56,6 +59,29 @@ Keep `dash-quality`/`dash-audio`, selector shape, right control placement/paddin
setting width/icons, defaults and unbound getName calls compatible. Do not use a
filtered representation array index or absoluteIndex as the SDK 5 selection key.
## SDK-driven refresh
After media identity validation, bind seven listeners only when both SDK `on` and
`off` exist. Quality requested/rendered, track rendered and stream updated/initialized
events coalesce into one Promise microtask. This keeps notice/check ordering within
a synchronous menu selection intact. Events emitted by a formatter during refresh
do not schedule recursive refreshes. Unchanged playback-time events only read the
Auto boolean; they do not call formatters or redraw menus. Pure configuration writes
with no subsequent SDK event still need explicit `update()`, especially while paused.
No polling timer or SDK method interception is installed.
Stream teardown invalidates queued work and clears menus without reading detached
media. Stream initialized/updated can restore menus on the same caller-owned SDK.
Replacing `art.dash` requires ready/restart or explicit update to bind the new SDK.
Release invalidates the record before calling the captured `off` method and attempts
every owned removal. Reentrant replacement wins; caller listeners remain installed.
An asynchronous SDK getter/formatter failure stops observation, clears owned menus,
and warns with the original error. Restore the offending formatter/SDK state and
call explicit `update()` to recover. Explicit update and synchronous menu errors keep
their original throwing behavior. Never convert the public update API to a Promise
or destroy an SDK to handle a plugin rendering failure.
## Types and compatibility
`src/types.ts` describes the narrow SDK and registry surfaces, menu models, event
@@ -127,8 +153,9 @@ multi-quality/multi-audio DASH segments. It covers decoding, explicit update aft
external selection, source topology replacement, retained callbacks and SDK ownership.
It also includes a native SDK control without an ArtPlayer instance. Early SDK 4.5.2
paused-seek stalls remain open; later passes do not resolve them. Unsupported MSE is
recorded as a playback capability gap, not acceptance. This plugin currently refreshes
on ready/restart/explicit update, not automatically on external SDK events.
recorded as a playback capability gap, not acceptance. SDK event tests also cover
external selections without explicit update, same-SDK source replacement, asynchronous
formatter error/recovery, and actual setting clicks during SDK refresh.
`refactor/scripts/dash-sdk-types.test.mjs` compares actual SDK-only and plugin
consumers across ten compiler/module configurations, retaining upstream-only
Node10/old DOM diagnostics. The exact peer dependency closure is hash-pinned in
@@ -6,6 +6,14 @@ Accepts a caller-owned dash.js instance at `art.dash`. Quality selection adapts
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.
Quality, track and stream events from the SDK refresh the control and setting menus.
Refreshes are coalesced after the current synchronous selection finishes. During
playback, the plugin also checks for changes to the SDK's Auto setting without
redrawing menus on unchanged time updates. When changing only SDK configuration
while paused (with no SDK event), call the existing synchronous
`art.plugins.artplayerPluginDashControl.update()` to refresh immediately.
The plugin removes only its own SDK listeners and never destroys your SDK instance.
## TypeScript
The formatter receives the original SDK object. For dash.js 5.2.1 types:
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function e(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){if(!o||r.get(n)!==o)return;r.delete(n);const l=t[n];"controls"===n&&l.cache?.get&&l.cache.get(e)?.option?.onSelect!==o||"setting"===n&&l.find&&l.find(e)?.onSelect!==o||l.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,s,u){if(!s)return void i();const a={};o=a;const f=()=>a===o&&u(),d=e=>f()?(s.select(e,f),f()?(t.notice.show=`${s.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:s.html,style:{padding:"0 10px"},selector:s.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:s.html,html:s.title,icon:n,width:200,selector:s.selector,onSelect:d})):l("setting"))},clear:i}}return function(n={}){return o=>{const r=o,{$video:l}=r.template,{errorHandle:i}=r.constructor.utils;let c=!1,s=0;const u=e(r,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),a=e(r,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),f=[];function d(t=()=>!0){let e;for(const o of[u.clear,a.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function h(){if(c||r.isDestroy)return;const e=++s,o=r.dash,f=()=>!c&&!r.isDestroy&&e===s&&r.dash===o,h=()=>f()&&o.getVideoElement()===l&&f();try{if(i(o.getVideoElement()===l,'Cannot find instance of DASH from "art.dash"'),!f())return;const e=n.quality||{},r=function(e,n,o){const r=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(e),l=r.levels();if(!o()||!l?.length)return null;const i=n.auto||"Auto",c=n.getName||(t=>`${t.height}p`),s=r.current(l);if(!o())return null;const u=e.getSettings().streaming.abr.autoSwitchBitrate.video;if(!o())return null;const a=!u&&s?c(s):i;if(!o())return null;const f=[];for(const[t,h]of l.entries()){const e=c(h);if(!o())return null;f.push({html:e,...r.item(h,t,s,u)})}const d=t(f).sort((t,e)=>e.value-t.value);return d.push({html:i,value:"auto",default:u}),{html:a,title:n.title||"Quality",selector:d,select:r.select}}(o,e,h);if(!h())return;if(u.update(e,r,h),!h())return;const c=n.audio||{},s=function(e,n,o){const r=e.getTracksFor("audio");if(!o()||!r?.length)return null;const l=n.auto||"Auto",i=n.getName||(t=>t.lang||t.id),c=e.getCurrentTrackFor("audio")||r[0];if(!o())return null;const s=c?i(c):l;if(!o())return null;const u=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=["id","index","lang"],o=t.filter(t=>n.every(n=>null==e[n]||t[n]===e[n]));return 1===o.length?o[0]:void 0}(r,c),a=[];for(const t of r){const e=i(t);if(!o())return null;a.push({html:e,value:t,default:t===u})}const f=t(a);return{html:s,title:n.title||"Audio",selector:f,select:t=>e.setCurrentTrack(t.value)}}(o,c,h);h()&&a.update(c,s,h)}catch(g){if(f()){const t=++s;try{d(()=>s===t)}catch(p){console.warn("ArtPlayer DASH cleanup failed:",p)}}throw g}}function g(){if(c)return;let t;c=!0,s++;const e=[d,...f.splice(0).map(([t,e])=>()=>r.Line truncated
!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 i(n,o=r.get(n)){if(!o||r.get(n)!==o)return;r.delete(n);const i=t[n];"controls"===n&&i.cache?.get&&i.cache.get(e)?.option?.onSelect!==o||"setting"===n&&i.find&&i.find(e)?.onSelect!==o||i.remove(e)}function c(){let t;o=void 0;for(const[n,o]of[...r])try{i(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(s,l,a){if(!l)return void c();const u={};o=u;const f=()=>u===o&&a(),d=e=>f()?(l.select(e,f),f()?(t.notice.show=`${l.title}: ${e.html}`,f()&&s.control&&t.controls.check(e),f()&&s.setting&&t.setting.check(e),e.html):e.html):e.html;s.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:l.html,style:{padding:"0 10px"},selector:l.selector,onSelect:d})):i("controls"),f()&&(s.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:l.html,html:l.title,icon:n,width:200,selector:l.selector,onSelect:d})):i("setting"))},clear:c}}return function(n={}){return o=>{const r=o,{$video:i}=r.template,{errorHandle:c}=r.constructor.utils;let s=!1,l=0;const a=e(r,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),u=e(r,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),f=[],d=function(t){let e;const n=n=>e===n&&t.active(n.dash),o=t=>t.dash.getSettings().streaming.abr.autoSwitchBitrate.video;function r(){const t=e;if(!t)return;let n;e=void 0,t.epoch++;for(const[e,r]of t.callbacks.splice(0))try{t.off.call(t.dash,e,r)}catch(o){n||(n=o)}if(n)throw n}function i(n,o){if(e===n){try{r()}catch(i){console.warn("ArtPlayer DASH subscription cleanup failed:",i)}if(!e&&t.active(n.dash))try{t.reset()}catch(i){console.warn("ArtPlayer DASH cleanup failed:",i)}}console.warn("ArtPlayer DASH refresh failed:",o)}function c(e){if(!n(e)||e.suspended||e.queued||e.running)return;e.queued=!0;const r=e.epoch;Promise.resolve().then(()=>{if(n(e)&&e.epoch===r&&!e.suspended){e.queued=!1,e.running=!0;try{e.automatic=o(e),n(e)&&t.refresh()}catch(c){i(e,c)}finally{e.running=!1}}})}return{bind:function(s){const l=e;if(l?.dash===s){const t=o(l);return void(n(l)&&(l.automatic=t,l.suspended=!1))}if(r(),e||!t.active(s)||"function"!==typeof s.on||"function"!==typeof s.off)return;const a={dash:s,off:s.off,callbacks:[],epoch:0,queued:!1,running:!1,suspended:!1,automatic:!1};e=a;const u=s.on;try{a.automatic=o(a);const e=["qualityChangeRequested","qualityChangeRendered","trackChangeRendered","streamUpdated","streamInitialized"].map(t=>[t,()=>{"streamUpdated"!==t&&"streamInitialized"!==t||(a.suspended=!1),c(a)}]);e.push(["playbackTimeUpdated",()=>{if(n(a)&&!a.suspended)try{const t=o(a);n(a)&&t!==a.automatic&&(a.automatic=t,c(a))}catch(t){i(a,t)}}]),e.push(["streamTeardownComplete",()=>{if(n(a)){a.epoch++,a.queued=!1,a.suspended=!0;try{t.reset()}catch(e){i(a,e)}}}]);for(const t of e){if(!n(a))break;a.callbacks.push(t),u.call(s,...t)}}catch(f){if(e===a)try{r()}catch(d){console.warn("ArtPlayer DASH subscription cleanup failed:",d)}throw f}},release:r}}({active:t=>!s&&!r.isDestroy&&r.dash===t,refresh:p,reset(){const t=++l;h(()=>t===l)}});function h(t=()=>!0){let e;for(const o of[a.clear,u.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function p(){if(s||r.isDestroy)return;const e=++l,o=r.dash,f=()=>!s&&!r.isDestroy&&e===l&&r.dash===o,p=()=>f()&&o.getVideoElement()===i&&f();try{if(c(o.getVideoElement()===i,'Cannot find instance of DASH from "art.dash"'),!f())return;if(d.bind(o),!p())return;const e=n.quality||{},r=function(e,n,o){const r=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForTyLine truncated
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,r=(e,n,o)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[n]=o,l=(t,l)=>{for(var i in l||(l={}))n.call(l,i)&&r(t,i,l[i]);if(e)for(var i of e(l))o.call(l,i)&&r(t,i,l[i]);return t};function i(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function c(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){var l,i,c,u;if(!o||r.get(n)!==o)return;r.delete(n);const s=t[n];"controls"===n&&(null==(l=s.cache)?void 0:l.get)&&(null==(c=null==(i=s.cache.get(e))?void 0:i.option)?void 0:c.onSelect)!==o||"setting"===n&&s.find&&(null==(u=s.find(e))?void 0:u.onSelect)!==o||s.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,u,s){if(!u)return void i();const a={};o=a;const f=()=>a===o&&s(),d=e=>f()?(u.select(e,f),f()?(t.notice.show=`${u.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:u.html,style:{padding:"0 10px"},selector:u.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:u.html,html:u.title,icon:n,width:200,selector:u.selector,onSelect:d})):l("setting"))},clear:i}}return function(t={}){return e=>{const n=e,{$video:o}=n.template,{errorHandle:r}=n.constructor.utils;let u=!1,s=0;const a=c(n,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),f=c(n,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),d=[];function h(t=()=>!0){let e;for(const o of[a.clear,f.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function p(){if(u||n.isDestroy)return;const e=++s,c=n.dash,d=()=>!u&&!n.isDestroy&&e===s&&n.dash===c,p=()=>d()&&c.getVideoElement()===o&&d();try{if(r(c.getVideoElement()===o,'Cannot find instance of DASH from "art.dash"'),!d())return;const e=t.quality||{},n=function(t,e,n){const o=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(t),r=o.levels();if(!n()||!(null==r?void 0:r.length))return null;const c=e.auto||"Auto",u=e.getName||(t=>`${t.height}p`),s=o.current(r);if(!n())return null;const a=t.getSettings().streaming.abr.autoSwitchBitrate.video;if(!n())return null;const f=!a&&s?u(s):c;if(!n())return null;const d=[];for(const[i,p]of r.entries()){const t=u(p);if(!n())return null;d.push(l({html:t},o.item(p,i,s,a)))}const h=i(d).sort((t,e)=>e.value-t.value);return h.push({html:c,value:"auto",default:a}),{html:f,title:e.title||"Quality",selector:h,select:o.select}}(c,e,p);if(!p())return;if(a.update(e,n,p),!p())return;const u=t.audio||{},s=function(t,e,n){const o=t.getTracksFor("audio");if(!n()||!(null==o?void 0:o.length))return null;const r=e.auto||"Auto",l=e.getName||(t=>t.lang||t.id),c=t.getCurrentTrackFor("audio")||o[0];if(!n())return null;const u=c?l(c):r;if(!n())return null;const s=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=["id","index","lang"],o=t.filter(t=>n.every(n=>null==e[n]||t[n]===e[n]));returLine truncated
!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,i=(t,i)=>{for(var l in i||(i={}))n.call(i,l)&&r(t,l,i[l]);if(e)for(var l of e(i))o.call(i,l)&&r(t,l,i[l]);return t};function l(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 i(n,o=r.get(n)){var i,l,c,a;if(!o||r.get(n)!==o)return;r.delete(n);const s=t[n];"controls"===n&&(null==(i=s.cache)?void 0:i.get)&&(null==(c=null==(l=s.cache.get(e))?void 0:l.option)?void 0:c.onSelect)!==o||"setting"===n&&s.find&&(null==(a=s.find(e))?void 0:a.onSelect)!==o||s.remove(e)}function l(){let t;o=void 0;for(const[n,o]of[...r])try{i(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,a,s){if(!a)return void l();const u={};o=u;const f=()=>u===o&&s(),d=e=>f()?(a.select(e,f),f()?(t.notice.show=`${a.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:a.html,style:{padding:"0 10px"},selector:a.selector,onSelect:d})):i("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:a.html,html:a.title,icon:n,width:200,selector:a.selector,onSelect:d})):i("setting"))},clear:l}}return function(t={}){return e=>{const n=e,{$video:o}=n.template,{errorHandle:r}=n.constructor.utils;let a=!1,s=0;const u=c(n,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),f=c(n,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),d=[],h=function(t){let e;const n=n=>e===n&&t.active(n.dash),o=t=>t.dash.getSettings().streaming.abr.autoSwitchBitrate.video;function r(){const t=e;if(!t)return;let n;e=void 0,t.epoch++;for(const[e,r]of t.callbacks.splice(0))try{t.off.call(t.dash,e,r)}catch(o){n||(n=o)}if(n)throw n}function i(n,o){if(e===n){try{r()}catch(i){console.warn("ArtPlayer DASH subscription cleanup failed:",i)}if(!e&&t.active(n.dash))try{t.reset()}catch(i){console.warn("ArtPlayer DASH cleanup failed:",i)}}console.warn("ArtPlayer DASH refresh failed:",o)}function l(e){if(!n(e)||e.suspended||e.queued||e.running)return;e.queued=!0;const r=e.epoch;Promise.resolve().then(()=>{if(n(e)&&e.epoch===r&&!e.suspended){e.queued=!1,e.running=!0;try{e.automatic=o(e),n(e)&&t.refresh()}catch(l){i(e,l)}finally{e.running=!1}}})}return{bind:function(c){const a=e;if((null==a?void 0:a.dash)===c){const t=o(a);return void(n(a)&&(a.automatic=t,a.suspended=!1))}if(r(),e||!t.active(c)||"function"!==typeof c.on||"function"!==typeof c.off)return;const s={dash:c,off:c.off,callbacks:[],epoch:0,queued:!1,running:!1,suspended:!1,automatic:!1};e=s;const u=c.on;try{s.automatic=o(s);const e=["qualityChangeRequested","qualityChangeRendered","trackChangeRendered","streamUpdated","streamInitialized"].map(t=>[t,()=>{"streamUpdated"!==t&&"streamInitialized"!==t||(s.suspended=!1),l(s)}]);e.push(["playbackTimeUpdated",()=>{if(n(s)&&!s.suspended)try{const t=o(s);n(s)&&t!==s.automatic&&(s.automatic=t,l(s))}catch(t){i(s,t)}}]),e.push(["streamTeardownComplete",()=>{if(n(s)){s.epoch++,s.queued=!1,s.suspended=!0;try{t.reset()}catch(e){i(s,e)}}}]);for(const t of e){if(!n(s))break;s.callbacks.push(t),u.call(c,...t)}}catch(f){if(e===s)try{r()}catch(d){console.warn("ArtPlayer DASH subscription cleanup failed:",d)}throw f}},release:r}}({active:t=>!a&&!n.isDestroy&&n.dash===t,refresh:y,reset(){const t=++s;p(()=>t===s)}});function p(t=()=>!0){let e;for(const o of[u.clear,f.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function y(){if(a||n.isDestroy)return;const e=++s,c=n.dash,d=()=>!a&&!n.isDestLine truncated
@@ -170,6 +170,133 @@ function createMenu(art, name, icon) {
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 observeSDK(options) {
let binding;
const active = (record) => binding === record && options.active(record.dash);
const automatic = (record) => record.dash.getSettings().streaming.abr.autoSwitchBitrate.video;
function release() {
const record = binding;
if (!record)
return;
binding = void 0;
record.epoch++;
let failure;
for (const [name, callback] of record.callbacks.splice(0)) {
try {
record.off.call(record.dash, name, callback);
} catch (error) {
failure || (failure = error);
}
}
if (failure)
throw failure;
}
function fail(record, error) {
if (binding === record) {
try {
release();
} catch (cleanupError) {
console.warn("ArtPlayer DASH subscription cleanup failed:", cleanupError);
}
if (!binding && options.active(record.dash)) {
try {
options.reset();
} catch (cleanupError) {
console.warn("ArtPlayer DASH cleanup failed:", cleanupError);
}
}
}
console.warn("ArtPlayer DASH refresh failed:", error);
}
function schedule(record) {
if (!active(record) || record.suspended || record.queued || record.running)
return;
record.queued = true;
const epoch = record.epoch;
void Promise.resolve().then(() => {
if (!active(record) || record.epoch !== epoch || record.suspended)
return;
record.queued = false;
record.running = true;
try {
record.automatic = automatic(record);
if (active(record))
options.refresh();
} catch (error) {
fail(record, error);
} finally {
record.running = false;
}
});
}
function bind(dash) {
const previous = binding;
if (previous?.dash === dash) {
const value = automatic(previous);
if (active(previous)) {
previous.automatic = value;
previous.suspended = false;
}
return;
}
release();
if (binding || !options.active(dash) || typeof dash.on !== "function" || typeof dash.off !== "function")
return;
const record = { dash, off: dash.off, callbacks: [], epoch: 0, queued: false, running: false, suspended: false, automatic: false };
binding = record;
const on = dash.on;
try {
record.automatic = automatic(record);
const changed = ["qualityChangeRequested", "qualityChangeRendered", "trackChangeRendered", "streamUpdated", "streamInitialized"];
const entries = changed.map((name) => [name, () => {
if (name === "streamUpdated" || name === "streamInitialized")
record.suspended = false;
schedule(record);
}]);
entries.push(["playbackTimeUpdated", () => {
if (!active(record) || record.suspended)
return;
try {
const value = automatic(record);
if (active(record) && value !== record.automatic) {
record.automatic = value;
schedule(record);
}
} catch (error) {
fail(record, error);
}
}]);
entries.push(["streamTeardownComplete", () => {
if (!active(record))
return;
record.epoch++;
record.queued = false;
record.suspended = true;
try {
options.reset();
} catch (error) {
fail(record, error);
}
}]);
for (const entry of entries) {
if (!active(record))
break;
record.callbacks.push(entry);
on.call(dash, ...entry);
}
} catch (error) {
if (binding === record) {
try {
release();
} catch (cleanupError) {
console.warn("ArtPlayer DASH subscription cleanup failed:", cleanupError);
}
}
throw error;
}
}
return { bind, release };
}
function artplayerPluginDashControl(option = {}) {
return (player) => {
const art = player;
@@ -180,6 +307,14 @@ function artplayerPluginDashControl(option = {}) {
const quality = createMenu(art, "dash-quality", $quality);
const audio = createMenu(art, "dash-audio", $audio);
const subscriptions = [];
const observer = observeSDK({
active: (dash) => !closed && !art.isDestroy && art.dash === dash,
refresh: update,
reset() {
const version = ++revision;
clear(() => version === revision);
}
});
function clear(current = () => true) {
let failure;
for (const cleanup of [quality.clear, audio.clear]) {
@@ -205,6 +340,9 @@ function artplayerPluginDashControl(option = {}) {
errorHandle(dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"');
if (!current())
return;
observer.bind(dash);
if (!valid())
return;
const qualityConfig = option.quality || {};
const qualities = qualityModel(dash, qualityConfig, valid);
if (!valid())
@@ -219,10 +357,12 @@ function artplayerPluginDashControl(option = {}) {
} catch (error) {
if (current()) {
const cleanupVersion = ++revision;
try {
clear(() => revision === cleanupVersion);
} catch (cleanupError) {
console.warn("ArtPlayer DASH cleanup failed:", cleanupError);
for (const cleanup of [observer.release, () => clear(() => revision === cleanupVersion)]) {
try {
cleanup();
} catch (cleanupError) {
console.warn("ArtPlayer DASH cleanup failed:", cleanupError);
}
}
}
throw error;
@@ -234,7 +374,7 @@ function artplayerPluginDashControl(option = {}) {
closed = true;
revision++;
let failure;
const actions = [clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))];
const actions = [observer.release, clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))];
for (const action of actions) {
try {
action();
@@ -5,6 +5,7 @@ import $audio from './audio.svg?raw'
import { audioModel, qualityModel } from './mapping'
import { createMenu } from './menu'
import $quality from './quality.svg?raw'
import { observeSDK } from './sdk-events'
export default function artplayerPluginDashControl<Level extends object = QualityLevel, Track extends object = AudioTrack>(option: Option<Level, Track> = {}) {
return (player: Artplayer): Result => {
@@ -17,6 +18,14 @@ export default function artplayerPluginDashControl<Level extends object = Qualit
const quality = createMenu<QualityItem>(art, 'dash-quality', $quality)
const audio = createMenu<AudioItem<Track>>(art, 'dash-audio', $audio)
const subscriptions: [EventName, Cleanup][] = []
const observer = observeSDK<Level, Track>({
active: dash => !closed && !art.isDestroy && art.dash === dash,
refresh: update,
reset() {
const version = ++revision
clear(() => version === revision)
},
})
function clear(current: Valid = () => true): void {
let failure: unknown
@@ -45,6 +54,9 @@ export default function artplayerPluginDashControl<Level extends object = Qualit
errorHandle(dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"')
if (!current())
return
observer.bind(dash)
if (!valid())
return
const qualityConfig = option.quality || {}
const qualities = qualityModel(dash, qualityConfig, valid)
if (!valid())
@@ -60,11 +72,13 @@ export default function artplayerPluginDashControl<Level extends object = Qualit
catch (error) {
if (current()) {
const cleanupVersion = ++revision
try {
clear(() => revision === cleanupVersion)
}
catch (cleanupError) {
console.warn('ArtPlayer DASH cleanup failed:', cleanupError)
for (const cleanup of [observer.release, () => clear(() => revision === cleanupVersion)]) {
try {
cleanup()
}
catch (cleanupError) {
console.warn('ArtPlayer DASH cleanup failed:', cleanupError)
}
}
}
throw error
@@ -77,7 +91,7 @@ export default function artplayerPluginDashControl<Level extends object = Qualit
closed = true
revision++
let failure: unknown
const actions = [clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))]
const actions = [observer.release, clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))]
for (const action of actions) {
try {
action()
@@ -0,0 +1,159 @@
import type { Cleanup, Dash, SDKEventName } from './types'
interface Binding<Level extends object, Track extends object> {
dash: Dash<Level, Track>
off: NonNullable<Dash<Level, Track>['off']>
callbacks: [SDKEventName, Cleanup][]
epoch: number
queued: boolean
running: boolean
suspended: boolean
automatic: boolean
}
export function observeSDK<Level extends object, Track extends object>(options: {
active: (dash: Dash<Level, Track>) => boolean
refresh: Cleanup
reset: Cleanup
}) {
let binding: Binding<Level, Track> | undefined
const active = (record: Binding<Level, Track>) => binding === record && options.active(record.dash)
const automatic = (record: Binding<Level, Track>) => record.dash.getSettings().streaming.abr.autoSwitchBitrate.video
function release(): void {
const record = binding
if (!record)
return
binding = undefined
record.epoch++
let failure: unknown
for (const [name, callback] of record.callbacks.splice(0)) {
try {
record.off.call(record.dash, name, callback)
}
catch (error) {
failure ||= error
}
}
if (failure)
throw failure
}
function fail(record: Binding<Level, Track>, error: unknown): void {
if (binding === record) {
try {
release()
}
catch (cleanupError) {
console.warn('ArtPlayer DASH subscription cleanup failed:', cleanupError)
}
if (!binding && options.active(record.dash)) {
try {
options.reset()
}
catch (cleanupError) {
console.warn('ArtPlayer DASH cleanup failed:', cleanupError)
}
}
}
console.warn('ArtPlayer DASH refresh failed:', error)
}
function schedule(record: Binding<Level, Track>): void {
if (!active(record) || record.suspended || record.queued || record.running)
return
record.queued = true
const epoch = record.epoch
void Promise.resolve().then(() => {
if (!active(record) || record.epoch !== epoch || record.suspended)
return
record.queued = false
record.running = true
try {
record.automatic = automatic(record)
if (active(record))
options.refresh()
}
catch (error) {
fail(record, error)
}
finally {
record.running = false
}
})
}
function bind(dash: Dash<Level, Track>): void {
const previous = binding
if (previous?.dash === dash) {
const value = automatic(previous)
if (active(previous)) {
previous.automatic = value
previous.suspended = false
}
return
}
release()
// An off callback may synchronously install a newer binding.
if (binding || !options.active(dash) || typeof dash.on !== 'function' || typeof dash.off !== 'function')
return
const record: Binding<Level, Track> = { dash, off: dash.off, callbacks: [], epoch: 0, queued: false, running: false, suspended: false, automatic: false }
binding = record
const on = dash.on
try {
record.automatic = automatic(record)
const changed: SDKEventName[] = ['qualityChangeRequested', 'qualityChangeRendered', 'trackChangeRendered', 'streamUpdated', 'streamInitialized']
const entries: [SDKEventName, Cleanup][] = changed.map(name => [name, () => {
if (name === 'streamUpdated' || name === 'streamInitialized')
record.suspended = false
schedule(record)
}])
entries.push(['playbackTimeUpdated', () => {
if (!active(record) || record.suspended)
return
try {
const value = automatic(record)
if (active(record) && value !== record.automatic) {
record.automatic = value
schedule(record)
}
}
catch (error) {
fail(record, error)
}
}])
entries.push(['streamTeardownComplete', () => {
if (!active(record))
return
record.epoch++
record.queued = false
record.suspended = true
try {
options.reset()
}
catch (error) {
fail(record, error)
}
}])
for (const entry of entries) {
if (!active(record))
break
record.callbacks.push(entry)
on.call(dash, ...entry)
}
}
catch (error) {
if (binding === record) {
try {
release()
}
catch (cleanupError) {
console.warn('ArtPlayer DASH subscription cleanup failed:', cleanupError)
}
}
throw error
}
}
return { bind, release }
}
@@ -5,6 +5,7 @@ export type Identifier = string | number
export type Valid = () => boolean
export type Cleanup = () => void
export type EventName = 'ready' | 'restart' | 'destroy'
export type SDKEventName = 'qualityChangeRequested' | 'qualityChangeRendered' | 'trackChangeRendered' | 'streamUpdated' | 'streamInitialized' | 'playbackTimeUpdated' | 'streamTeardownComplete'
export interface QualityFields {
height?: number
@@ -15,6 +16,8 @@ export interface QualityFields {
export type AudioFields = AudioTrack
export interface Dash<Level extends object = object, Track extends object = object> {
on?: (name: SDKEventName, callback: Cleanup) => unknown
off?: (name: SDKEventName, callback: Cleanup) => unknown
getVideoElement: () => object | null
getSettings: () => { streaming: { abr: { autoSwitchBitrate: { video: boolean } } } }
updateSettings: (settings: { streaming: { abr: { autoSwitchBitrate: { video: boolean } } } }) => unknown
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,58 @@
# PKG-DASH-05:SDK 事件刷新与资源清理
起点 `2ad00730`。本批修复外部 SDK 选择后菜单不更新,新增独立 `sdk-events.ts`,
DASH 现在有六个严格 TS 模块。DASH-05 仍 doing;不改变公开工厂、同步 update、
配置类型、DOM/CSS、入口或调用方 SDK 所有权,没有新增依赖或修改版本。
## 行为与兼容边界
原实现只在 ready/restart/显式 update 时重建菜单。固定真实 dash.js 4.5.2/5.2.1、
新旧核心的外部画质/音轨选择复现 8 项失败、4 项无 MSE 跳过,证明 UI 陈旧。
新增观察器在媒体身份校验后订阅质量、音轨、流初始化/更新、时间和 teardown 七类事件。
同轮事件合并到一个微任务,保留选择过程内 setCurrentTrack、notice、controls.check、
setting.check 的同步顺序和返回标签。formatter 中发出的事件不递归排队。
普通 playbackTimeUpdated 只比较 Auto 布尔值,未变化时不重绘、不调用 formatter。
暂停时纯配置写入且 SDK 没发事件,仍通过原有同步 update 即时刷新;没有引入轮询计时器
或拦截 SDK 方法。没有 on/off 的旧宿主继续支持原有显式更新。
teardown 清理菜单并使排队工作失效,同一 SDK 后续 streamInitialized/Updated 可以恢复。
切换 art.dash 后由 ready/restart/update 绑定新实例。解绑先关闭记录再移除自己的监听,
不删除调用方监听,不销毁 SDK;安装部分失败、off 抛错和同步重入都有回归。
异步 getter/formatter 失败释放监听、清理菜单并报告原始 Error;修复后显式 update 恢复。
显式 update 及同步菜单错误仍以原始身份同步抛出,不把公开 API 改为 Promise。
标准构建重新生成 main/legacy/module 与平铺 docs/compiled。体积约从 5.36/5.80/9.58 KiB
增至 7.24/7.70/13.74 KiB,增加来自监听所有权、调度和失败清理;未引入运行时依赖。
内部职责与维护规则见 [包内架构](../../packages/artplayer-plugin-dash-control/ARCHITECTURE.md)。
## 验证与测试修正
- 源码 Node 144 项、三格式 Node 378 项通过,覆盖两代 SDK、历史契约、清理和事件重入。
- 正式 main/legacy 实际 SDK 各 90 项:62 通过、28 无 MSE 跳过;覆盖实际解码、外部选择、
Auto、同 SDK attachSource、拓扑清理以及 formatter 错误/恢复。
- 随后新增真实设置按钮点击:main 连同修正后的原生 SDK 对照 20 通过/10 跳过;
legacy 设置点击 16 通过/8 跳过。已查看 main 设置选择截图,180p 菜单、控制栏和画面一致。
- 正式 main/legacy 的受控 SDK、真实 DOM 与原生 MP4 回归各 84 项通过,没有跳过或重试。
- 完整 `yarn ci:check` 746 项通过(689 单元、14 工程、43 基线),262 个生产 TS 文件严格
检查通过,核心声明 37 个文件无漂移。lint 零错误,保留既有生成声明的 unused-disable 警告。
DASH-STATE-01 已按当前 representation/track 与 UI 一致的证据关闭;它不涵盖下述 seek、
设备或完整分发验收。任务仍为 217 项:74 done、4 doing、139 todo。
完整 spec 现在定义 114 项;上面的 90 项运行发生在新增设置用例和修正原生对照参数之前,
不能表述为最终 114 项完整运行。报告及全部 traces 已分别归档,未并行覆盖共享输出。
第一次 async error 浏览器验证有 4 项 Firefox 失败:ConsoleMessage.text 没展开 Error.message。
改为读取消息参数并验证 message 和页面内 Error 对象身份,保留一次 warning 断言。
第一次三格式 Node 有 8 项失败:UMD 的 VM console 未转发给外部监听。测试 helper 显式
注入宿主 console 后通过;这两项修正都没有修改生产错误处理或放宽原始错误身份断言。
原生 SDK 对照此前额外传 forceReplace=true,与插件的两参数调用不同。本批将对照改为
相同参数后再验证(包括 main 最新 30 项运行),没有修改公开 seek 目标、放宽等待或重试。
此前 DASH-SEEK-01 的 6 秒边界停滞仍未定位;这些后续通过不足以关闭它。
执行指纹与本地归档索引见 [本批证据](../baselines/dash-sdk-events-validation.json)。
缓存不提交,指纹只用于本机证据追踪;换机器按测试入口重新执行。
Windows WebKit 无 MSE、有效设备、demo 与完整安装分发,以及全项目发布复盘继续保留。
回退应一起恢复事件模块、入口、测试脚本和构建产物,保留历史失败证据。
+17 -5
View File
@@ -2,12 +2,12 @@
测试保护真实 npm 1.1.0 的既有调用契约,以及重构前工作区的 SDK 5.x 稳定 ID 修正。
版本来源见 [发布契约](baselines/dash-control-contract.md)。02 建立基线,03 改造运行行为,
04 已把五个自有模块迁移为严格 TS 并补齐公开类型与编辑器生成;
04 已把五个自有模块迁移为严格 TS 并补齐公开类型与编辑器生成;05 新增第六个 SDK 事件模块。
实际模块地图与生命周期规则见 [包内架构](../packages/artplayer-plugin-dash-control/ARCHITECTURE.md)。
## 可重跑的测试
- `yarn test:dash-control`:原有 5 项过滤/稳定 ID 回归、80 项行为/历史观察、33 项候选生命周期,共 118 项。
- `yarn test:dash-control`:原有 118 项与两代 SDK 各 13 项事件/清理/重入回归,共 144 项。
该脚本和 `test:unit` 都保留旧测试,完整 CI 自动执行。
- `yarn test:browser test/browser/dash-control.spec.js --trace on`:84 项,两个核心、发布插件 SDK 4/
候选插件 SDK 4/5、两类菜单、两种选择及候选清理、Chromium/Firefox/WebKit。
@@ -20,7 +20,8 @@
- `yarn test:dash-types-package`:Yarn 打包核心/DASH、工作区外离线安装与冻结锁重装,
检查五种模式、CommonJS callable/命名空间、每模式八条非法调用,并拒绝解析回工作区。
- `yarn test:browser test/browser/dash-sdk.spec.js --trace on`:真实 npm dash.js 4.5.2/5.2.1,
本地多画质/多音轨 MPD,新旧核心/插件、外部同步 update、连续换源和 SDK 原生对照。
本地多画质/多音轨 MPD,新旧核心/插件、外部同步 update、自动事件刷新、同 SDK 换源、
formatter 错误恢复、实际设置点击和 SDK 原生对照。目前完整定义 114 项。
`ARTPLAYER_DASH_ARTIFACT` 可选择正式 main/legacy;未指定则测试源码构建。
无 MSE 的引擎只通过能力探针,播放用例明确跳过,不计为该引擎 DASH 验收。
- `node --test refactor/scripts/dash-sdk.test.mjs`:SDK 归档/成员/许可证来源、媒体指纹与拓扑。
@@ -33,8 +34,8 @@
Node helper 分别加载当前源码、固定 Git 提交的旧工作区 UMD、实际 npm main/legacy/module。
Git 内容以 LF 指纹核对,发布归档与成员以原始字节指纹核对。`ARTPLAYER_TEST_DASH` 可指定
待测产物,以系统路径分隔符连接;同时指定 main/legacy/module 时共 274 项,包含两代 SDK 的
行为与生命周期。未设置时不会把源码测试冒充三种候选产物验证。
待测产物,以系统路径分隔符连接;同时指定 main/legacy/module 时共 378 项,包含两代 SDK 的
行为、生命周期和事件。未设置时不会把源码测试冒充三种候选产物验证。
`test/helpers/dash-control.js` 只提供可控 SDK 方法和组件注册表,检查 this、类型参数、
原始轨道身份、调用顺序、错误身份、ABR 配置保留、换 SDK、多实例和关闭后的引用。
@@ -67,3 +68,14 @@ Git 内容以 LF 指纹核对,发布归档与成员以原始字节指纹核对
状态以 risks.json 为准;受控修复不替代真实 SDK 验收。严格 TS/公开类型已有独立验证,05 完成
真实 SDK/媒体/组合验证,06 完成包分发验收。物理设备和 npm 发布准入仍待完成。
## 05 SDK 事件刷新维护
`test/dash-events.test.js` 覆盖批量事件合并、同步选择顺序、无变化 tick 不渲染、SDK 替换、
teardown/恢复、调用方监听所有权、部分安装/清理失败、formatter 异常、销毁和重入。
UMD 测试 VM 显式注入宿主 console,以检查原始错误身份;没有修改产物的错误处理。
实际浏览器错误身份通过 ConsoleMessage 参数验证,不依赖 Firefox 对 Error 的显示文本。
纯配置修改但没有 SDK 事件时,保留同步 update 为即时刷新入口;播放 tick 仅检查 Auto
布尔值变化,没有新定时器。真实 SDK/媒体验证与剩余 seek/设备边界见
[事件改造记录](changes/2026-09-12-PKG-DASH-05-sdk-events.md)。历史失败证据保留,后续通过不抹去它们。
+1 -1
View File
@@ -483,4 +483,4 @@
- PKG-DASH-02: [记录](changes/2026-09-12-PKG-DASH-02-tests.md) [记录](dash-validation.md) [记录](baselines/dash-validation.json)
- PKG-DASH-03: [记录](changes/2026-09-12-PKG-DASH-03-runtime.md) [记录](baselines/dash-runtime.json) [记录](dash-validation.md)
- PKG-DASH-04: [记录](changes/2026-09-12-PKG-DASH-04-types.md) [记录](baselines/dash-types-validation.json) [记录](dash-validation.md)
- PKG-DASH-05: [记录](changes/2026-09-12-PKG-DASH-05-sdk-checkpoint.md) [记录](baselines/dash-sdk.json) [记录](baselines/dash-sdk-checkpoint.json) [记录](dash-validation.md) [记录](changes/2026-09-12-PKG-DASH-05-sdk-types.md) [记录](baselines/dash-sdk-types-validation.json)
- PKG-DASH-05: [记录](changes/2026-09-12-PKG-DASH-05-sdk-checkpoint.md) [记录](baselines/dash-sdk.json) [记录](baselines/dash-sdk-checkpoint.json) [记录](dash-validation.md) [记录](changes/2026-09-12-PKG-DASH-05-sdk-types.md) [记录](baselines/dash-sdk-types-validation.json) [记录](changes/2026-09-12-PKG-DASH-05-sdk-events.md) [记录](baselines/dash-sdk-events-validation.json)
+18
View File
@@ -1,5 +1,23 @@
# 进度与证据
## 当前实施:DASH-05 SDK 事件刷新源码改造
新增第六个严格 TS 模块 sdk-events:按 SDK 质量/音轨/流事件合并刷新菜单,保留同步
选择与 update,清理仅释放自有监听;覆盖 teardown/恢复、SDK 替换、异常和重入。
播放 tick 只检查 Auto 变化;暂停时纯配置修改且无 SDK 事件仍由显式 update 即时刷新。
源码 Node 144、三格式 378 项通过;正式 main/legacy 实际 SDK 各 62 通过/28 无 MSE
跳过(当时完整 90 项);新增实际设置及修正参数的原生对照 main 20 通过/10 跳过,
legacy 设置 16 通过/8 跳过。main/legacy 受控 SDK 与真实 DOM 各 84 项全部通过。
实际 SDK spec 目前定义 114 项,90 项报告与后续过滤运行的范围分别保留,未冒充整套重跑。
完整本地 CI 746 项通过(689 单元、14 工程、43 基线),262 个生产 TS 严格检查通过。
当前项同步风险 DASH-STATE-01 已关闭;DASH-SEEK-01 仍 open,后续通过不能证明历史
边界停滞已解决。设备、Windows WebKit MSE、demo/完整分发继续推进。217 项仍为
74 done、4 doing、139 todo;本批是待完成 DASH-05 的本地检查点,不推送发布。
见 [本批说明](changes/2026-09-12-PKG-DASH-05-sdk-events.md) 和
[执行指纹](baselines/dash-sdk-events-validation.json)。以下为历史检查点当时状态。
## 当前实施:DASH-05 实际 SDK 类型与 nullable 元数据
实际 dash.js 4.5.2/5.2.1 类型消费者已接入,固定五个 @svta 类型依赖及原始 exports。
+1 -1
View File
@@ -137,7 +137,7 @@
| 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 | 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-STATE-01 | resolved / 已复现 | DASH 当前项高亮使用真值 ID、音轨身份和标签去重,边界待核实 | PKG-DASH-02, PKG-DASH-03, PKG-DASH-05 |
| DASH-TYPE-01 | resolved / 源码/产物事实 | DASH 类型未表达默认工厂参数、SDK 回调对象和现代模块入口 | PKG-DASH-04 |
| DASH-DEMO-01 | open / 源码/产物事实 | DASH 示例仅新 SDK 且重复安装会累积销毁回调 | PKG-DASH-06 |
| DASH-SEEK-01 | open / 已复现 | 真实 dash.js 4.5.2 暂停 seek 到缓冲片段边界后可能持续 waiting | PKG-DASH-05, REVIEW-02 |
+9 -2
View File
@@ -2889,7 +2889,7 @@
"id": "DASH-STATE-01",
"title": "DASH 当前项高亮使用真值 ID、音轨身份和标签去重,边界待核实",
"confirmation": "reproduced",
"status": "open",
"status": "resolved",
"owners": [
"PKG-DASH-02",
"PKG-DASH-03",
@@ -2908,7 +2908,14 @@
"refactor/baselines/dash-sdk-checkpoint.json"
],
"compatibleResolution": "验证零 ID、重复标签和克隆音轨当前项,在保持原展示语义时按实际可选项定位高亮。",
"closureCriteria": "有受控和真实 SDK 证据说明选中的 representation/track 与 UI 一致。"
"closureCriteria": "有受控和真实 SDK 证据说明选中的 representation/track 与 UI 一致。",
"resolutionEvidence": [
"refactor/changes/2026-09-12-PKG-DASH-05-sdk-events.md",
"refactor/baselines/dash-sdk-events-validation.json",
"test/dash-events.test.js",
"test/browser/dash-sdk.spec.js"
],
"resolutionRationale": "受控两代 SDK 保留零 ID、重复标签和唯一克隆轨道匹配;实际固定 SDK 4.5.2/5.2.1、新旧核心、main/legacy 的控制栏/设置、外部选择和 Auto 刷新与实际 representation/track 一致。仅关闭当前项与菜单同步缺陷;暂停无事件的纯配置修改仍用显式 update,seek 停滞、无 MSE、设备与完整分发不在此关闭范围。"
},
{
"id": "DASH-TYPE-01",
+3 -1
View File
@@ -2068,7 +2068,9 @@
"baselines/dash-sdk-checkpoint.json",
"dash-validation.md",
"changes/2026-09-12-PKG-DASH-05-sdk-types.md",
"baselines/dash-sdk-types-validation.json"
"baselines/dash-sdk-types-validation.json",
"changes/2026-09-12-PKG-DASH-05-sdk-events.md",
"baselines/dash-sdk-events-validation.json"
]
},
{
+1 -1
View File
@@ -1054,7 +1054,7 @@
"refactor/scripts/dash-sdk-types.test.mjs"
],
"contract": "Caller owns art.dash. Candidate adapts the released 4.x quality method surface and workspace 5.x stable representation ID surface. Controlled contracts and DOM validation pass; actual pinned 4.5.2/5.2.1 SDK media remains PKG-DASH-05.",
"validationStatus": "Pinned real npm SDK 4.5.2/5.2.1 media exercised; DASH-SEEK-01 and Windows WebKit MSE gap remain. Actual SDK declarations and exact peer closure now tested across ten compiler/module configurations, with no added plugin errors and upstream-only Node10/old DOM limitations. Automatic state refresh, full formats/devices/distribution remain incomplete.",
"validationStatus": "Pinned npm SDK 4.5.2/5.2.1 main/legacy media, automatic state refresh, same-SDK source replacement, settings selection and formatter recovery exercised. DASH-SEEK-01 and Windows WebKit MSE gap remain. Actual SDK declarations and exact peer closure tested across ten compiler/module configurations, with no added plugin errors and upstream-only Node10/old DOM limitations. Devices and full distribution remain incomplete; see refactor/baselines/dash-sdk-events-validation.json.",
"demoPathsSource": "refactor/baselines/demo-inventory.json"
},
{
+99 -3
View File
@@ -81,12 +81,13 @@ async function openDash(page, version, core, plugin, testInfo) {
window.sdkEvents = []
window.mediaEvents = []
window.sdkDestroyed = 0
window.dashOption = { quality: { control: true, setting: true }, audio: { control: true, setting: true } }
window.art = new window.Artplayer({
container: '.player',
url: '/dash-fixture/master.mpd',
muted: true,
setting: true,
plugins: [window.artplayerPluginDashControl({ quality: { control: true, setting: true }, audio: { control: true, setting: true } })],
plugins: [window.artplayerPluginDashControl(window.dashOption)],
customType: { mpd(video, url, art) {
if (art.dash) {
art.dash.destroy()
@@ -215,9 +216,9 @@ for (const version of ['4.5.2', '5.2.1']) {
const dash = window.nativeDash
dash.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: false } } } })
if (dash.setQualityFor)
dash.setQualityFor('video', 1, true)
dash.setQualityFor('video', 1)
else
dash.setRepresentationForTypeById('video', '1', true)
dash.setRepresentationForTypeById('video', '1')
})
await expect.poll(() => page.evaluate(() => window.nativeVideo.videoHeight)).toBe(180)
// Match the failed integrated run: video data ends at the next seek target.
@@ -242,6 +243,101 @@ for (const version of ['4.5.2', '5.2.1']) {
for (const core of ['published', 'candidate']) {
for (const version of ['4.5.2', '5.2.1']) {
for (const type of ['quality', 'audio']) {
test(`${core} core / DASH ${version}: actual settings ${type} selection survives SDK refresh`, async ({ page }, testInfo) => {
await openDash(page, version, core, 'candidate', testInfo)
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
const label = type === 'quality' ? '180p' : 'fr'
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(`^${label}$`) }).click()
if (type === 'quality') {
await expect.poll(() => page.evaluate(() => ({ height: window.art.video.videoHeight, auto: window.art.dash.getSettings().streaming.abr.autoSwitchBitrate.video }))).toEqual({ height: 180, auto: false })
}
else {
await expect.poll(() => page.evaluate(() => window.art.dash.getCurrentTrackFor('audio').lang)).toBe('fr')
}
await expect(page.locator(`.art-control-dash-${type} .art-selector-value`)).toHaveText(label)
expect(await page.evaluate(type => window.art.setting.find(`dash-${type}`).tooltip, type)).toBe(label)
expect(await page.evaluate(() => window.sdkErrors)).toEqual([])
await testInfo.attach('dash-sdk-settings', { contentType: 'image/png', body: await page.screenshot() })
})
}
test(`${core} core / DASH ${version}: same SDK source replacement refreshes topology without plugin update`, async ({ page }, testInfo) => {
await openDash(page, version, core, 'candidate', testInfo)
await expect(page.locator('.art-control-dash-audio .art-selector-value')).toHaveText('en')
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
await page.evaluate(() => window.art.dash.attachSource('/dash-fixture/single.mpd'))
await expect(page.locator('.art-control-dash-audio')).toHaveCount(0)
await expect(page.locator('.art-control-dash-quality .art-selector-item')).toHaveText(['90p', 'Auto'])
await expect.poll(() => page.evaluate(() => window.art.video.readyState)).toBeGreaterThanOrEqual(2)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
await page.evaluate(() => window.art.dash.attachSource('/dash-fixture/master.mpd'))
await expect(page.locator('.art-control-dash-audio .art-selector-value')).toHaveText('en')
await expect(page.locator('.art-control-dash-quality .art-selector-item')).toHaveText(['180p', '90p', 'Auto'])
expect(await page.evaluate(() => window.sdkDestroyed)).toBe(0)
expect(await page.evaluate(() => window.sdkErrors)).toEqual([])
})
test(`${core} core / DASH ${version}: asynchronous formatter error clears menus and explicit update recovers`, async ({ page }, testInfo) => {
const warnings = []
page.on('console', (message) => {
if (message.type() === 'warning')
warnings.push({ text: message.text(), detail: message.text().includes('ArtPlayer DASH refresh failed:') ? message.args()[1].evaluate(error => ({ message: error.message, original: error === window.dashFormatterError })) : Promise.resolve(null) })
})
await openDash(page, version, core, 'candidate', testInfo)
await expect(page.locator('.art-control-dash-audio .art-selector-value')).toHaveText('en')
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
await page.evaluate(() => {
window.dashFormatterError = new Error('Intentional DASH formatter failure')
window.dashOption.audio.getName = () => {
throw window.dashFormatterError
}
const dash = window.art.dash
dash.setCurrentTrack(dash.getTracksFor('audio').find(track => track.lang === 'fr'))
})
await expect(page.locator('.art-control-dash-quality, .art-control-dash-audio')).toHaveCount(0)
const failures = warnings.filter(message => message.text.includes('ArtPlayer DASH refresh failed:'))
expect(failures).toHaveLength(1)
expect(await Promise.all(failures.map(message => message.detail))).toEqual([{ message: 'Intentional DASH formatter failure', original: true }])
await page.evaluate(() => {
window.dashOption.audio.getName = track => track.lang || String(track.id)
window.art.plugins.artplayerPluginDashControl.update()
})
await expect(page.locator('.art-control-dash-audio .art-selector-value')).toHaveText('fr')
await expect(page.locator('.art-control-dash-quality .art-selector-item')).toHaveCount(3)
expect(await page.evaluate(() => window.sdkErrors)).toEqual([])
})
test(`${core} core / DASH ${version}: SDK events refresh external selections without explicit update`, async ({ page }, testInfo) => {
await openDash(page, version, core, 'candidate', testInfo)
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
await page.evaluate(() => {
const dash = window.art.dash
dash.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: false } } } })
if (dash.setQualityFor)
dash.setQualityFor('video', 1)
else
dash.setRepresentationForTypeById('video', '1')
dash.setCurrentTrack(dash.getTracksFor('audio').find(track => track.lang === 'fr'))
})
await expect.poll(() => page.evaluate(() => window.art.video.videoHeight)).toBe(180)
await expect(page.locator('.art-control-dash-quality .art-selector-value')).toHaveText('180p')
await expect(page.locator('.art-control-dash-audio .art-selector-value')).toHaveText('fr')
expect(await page.evaluate(() => ({ quality: window.art.setting.find('dash-quality').tooltip, audio: window.art.setting.find('dash-audio').tooltip }))).toEqual({ quality: '180p', audio: 'fr' })
await page.evaluate(() => window.art.dash.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: true } } } }))
await expect(page.locator('.art-control-dash-quality .art-selector-value')).toHaveText('Auto')
expect(await page.evaluate(() => window.sdkErrors)).toEqual([])
})
test(`${core} core / DASH ${version}: explicit update reflects external SDK selection`, async ({ page }, testInfo) => {
await openDash(page, version, core, 'candidate', testInfo)
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
+271
View File
@@ -0,0 +1,271 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- SDK event ownership uses Node's lifecycle 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 flush = () => Promise.resolve()
function eventHost(version) {
const host = dashHost(version)
const events = new Map()
host.dash.on = (name, callback) => {
if (!events.has(name))
events.set(name, new Set())
events.get(name).add(callback)
}
host.dash.off = (name, callback) => events.get(name)?.delete(callback)
return { ...host, events, fire(name) {
for (const callback of [...(events.get(name) || [])])
callback()
}, listenerCount: () => [...events.values()].reduce((sum, callbacks) => sum + callbacks.size, 0) }
}
for (const { name, sdk, factory } of candidates) {
test(`DASH ${sdk} (${name}): SDK events coalesce without interrupting synchronous selection`, async () => {
const host = eventHost(sdk)
factory(bothMenus())(host.art).update()
assert.equal(host.listenerCount(), 7)
const oldSelect = host.dash.setCurrentTrack
host.dash.setCurrentTrack = (track) => {
oldSelect.call(host.dash, track)
host.fire('trackChangeRendered')
host.fire('streamUpdated')
}
const menu = host.controls.get('dash-audio')
host.calls.length = 0
assert.equal(menu.onSelect(menu.selector[1]), 'fr')
assert.deepEqual(host.calls.map(call => call[0]), ['setCurrentTrack', 'notice', 'controls.check', 'setting.check'])
await flush()
assert.equal(host.controls.get('dash-audio').html, 'fr')
assert.equal(host.calls.filter(call => call[0] === 'controls.update').length, 2)
})
test(`DASH ${sdk} (${name}): time ticks only refresh changed Auto state`, async () => {
const host = eventHost(sdk)
const plugin = factory(bothMenus())(host.art)
plugin.update()
host.calls.length = 0
host.fire('playbackTimeUpdated')
await flush()
assert.equal(host.calls.length, 0)
host.state.settings.streaming.abr.autoSwitchBitrate.video = false
host.fire('playbackTimeUpdated')
host.fire('playbackTimeUpdated')
await flush()
assert.equal(host.controls.get('dash-quality').html, '720p')
assert.equal(host.calls.filter(call => call[0] === 'controls.update').length, 2)
host.state.settings.streaming.abr.autoSwitchBitrate.video = true
plugin.update()
host.calls.length = 0
host.fire('playbackTimeUpdated')
await flush()
assert.equal(host.calls.length, 0)
})
test(`DASH ${sdk} (${name}): changed SDK and destroy invalidate queued work and only remove owned listeners`, async () => {
const host = eventHost(sdk)
const next = eventHost(sdk)
const external = () => {}
host.dash.on('streamUpdated', external)
const plugin = factory(bothMenus())(host.art)
plugin.update()
host.fire('streamUpdated')
next.state.video = host.video
host.art.dash = next.dash
host.calls.length = 0
await flush()
assert.equal(host.calls.length, 0)
plugin.update()
assert.equal(host.listenerCount(), 1)
assert(host.events.get('streamUpdated').has(external))
assert.equal(next.listenerCount(), 7)
next.fire('qualityChangeRendered')
const retained = [...next.events.get('qualityChangeRendered')][0]
host.art.destroy()
host.calls.length = 0
retained()
await flush()
assert.equal(next.listenerCount(), 0)
assert.equal(host.calls.length, 0)
assert(!next.calls.some(call => call[0] === 'dash.destroy'))
})
test(`DASH ${sdk} (${name}): SDK teardown clears menus and source initialization restores them`, async () => {
const host = eventHost(sdk)
factory(bothMenus())(host.art).update()
host.fire('qualityChangeRendered')
host.fire('streamTeardownComplete')
assert.equal(host.controls.size + host.settings.size, 0)
host.calls.length = 0
host.fire('playbackTimeUpdated')
await flush()
assert.equal(host.calls.length, 0)
host.fire('streamInitialized')
await flush()
assert.equal(host.controls.size + host.settings.size, 4)
assert.equal(host.listenerCount(), 7)
})
test(`DASH ${sdk} (${name}): a partially installed SDK subscription is rolled back with its original error`, () => {
const host = eventHost(sdk)
const failure = new Error('SDK subscription failed')
const on = host.dash.on
host.dash.on = (name, callback) => {
on(name, callback)
if (host.listenerCount() === 2)
throw failure
}
assert.throws(() => factory(bothMenus())(host.art).update(), error => error === failure)
assert.equal(host.listenerCount(), 0)
assert.equal(host.controls.size + host.settings.size, 0)
})
test(`DASH ${sdk} (${name}): throwing SDK removal still attempts every owned listener and menu`, () => {
const host = eventHost(sdk)
const failure = new Error('SDK unsubscribe failed')
const off = host.dash.off
let removed = 0
host.dash.off = (name, callback) => {
off(name, callback)
removed++
if (removed === 1)
throw failure
}
factory(bothMenus())(host.art).update()
assert.throws(() => host.art.destroy(), error => error === failure)
assert.equal(removed, 7)
assert.equal(host.listenerCount(), 0)
assert.equal(host.controls.size + host.settings.size, 0)
})
test(`DASH ${sdk} (${name}): asynchronous formatter failure closes observation and reports the original error`, async (t) => {
const host = eventHost(sdk)
const option = bothMenus()
factory(option)(host.art).update()
const failure = new Error('SDK refresh formatter failed')
option.audio.getName = () => {
throw failure
}
const warnings = []
t.mock.method(console, 'warn', (...args) => warnings.push(args))
host.fire('trackChangeRendered')
await flush()
assert.equal(host.listenerCount(), 0)
assert.equal(host.controls.size + host.settings.size, 0)
assert.equal(warnings.length, 1)
assert.equal(warnings[0][1], failure)
})
test(`DASH ${sdk} (${name}): destroy during SDK subscription cannot install later listeners`, () => {
const host = eventHost(sdk)
const on = host.dash.on
host.dash.on = (name, callback) => {
on(name, callback)
host.art.destroy()
}
factory(bothMenus())(host.art).update()
assert.equal(host.listenerCount(), 0)
assert.equal(host.controls.size + host.settings.size, 0)
})
test(`DASH ${sdk} (${name}): SDK events emitted by a formatter cannot create an endless refresh loop`, async () => {
const host = eventHost(sdk)
const option = bothMenus()
let formats = 0
option.audio.getName = (track) => {
formats++
host.fire('streamUpdated')
return track.lang
}
factory(option)(host.art).update()
const initial = formats
await flush()
const refreshed = formats
assert(refreshed > initial)
await flush()
assert.equal(formats, refreshed)
})
test(`DASH ${sdk} (${name}): destruction from a repeated binding snapshot leaves no stale writes`, () => {
const host = eventHost(sdk)
const plugin = factory(bothMenus())(host.art)
plugin.update()
const getSettings = host.dash.getSettings
host.dash.getSettings = () => {
host.art.destroy()
return getSettings.call(host.dash)
}
plugin.update()
assert.equal(host.listenerCount(), 0)
assert.equal(host.controls.size + host.settings.size, 0)
})
test(`DASH ${sdk} (${name}): unsubscribe reentry keeps the newer SDK binding exactly once`, () => {
const host = eventHost(sdk)
const next = eventHost(sdk)
next.state.video = host.video
const off = host.dash.off
let entered = false
let plugin
host.dash.off = (name, callback) => {
off(name, callback)
if (!entered) {
entered = true
plugin.update()
}
}
plugin = factory(bothMenus())(host.art)
plugin.update()
host.art.dash = next.dash
plugin.update()
assert.equal(host.listenerCount(), 0)
assert.equal(next.listenerCount(), 7)
assert.equal(host.controls.size + host.settings.size, 4)
host.art.destroy()
assert.equal(next.listenerCount(), 0)
})
test(`DASH ${sdk} (${name}): failed Auto observation stops reads and an explicit update can recover`, async (t) => {
const host = eventHost(sdk)
const plugin = factory(bothMenus())(host.art)
plugin.update()
const getSettings = host.dash.getSettings
const failure = new Error('SDK settings unavailable')
let reads = 0
host.dash.getSettings = () => {
reads++
throw failure
}
const warnings = []
t.mock.method(console, 'warn', (...args) => warnings.push(args))
host.fire('playbackTimeUpdated')
host.fire('playbackTimeUpdated')
await flush()
assert.equal(reads, 1)
assert.equal(host.listenerCount(), 0)
assert.equal(host.controls.size + host.settings.size, 0)
assert.equal(warnings.length, 1)
assert.equal(warnings[0][1], failure)
host.dash.getSettings = getSettings
plugin.update()
assert.equal(host.listenerCount(), 7)
assert.equal(host.controls.size + host.settings.size, 4)
})
test(`DASH ${sdk} (${name}): explicit update errors stay synchronous when SDK events are supported`, (t) => {
const host = eventHost(sdk)
const option = bothMenus()
const plugin = factory(option)(host.art)
plugin.update()
const failure = new Error('Explicit formatter failure')
option.audio.getName = () => {
throw failure
}
const warnings = []
t.mock.method(console, 'warn', (...args) => warnings.push(args))
assert.throws(() => plugin.update(), error => error === failure)
assert.equal(host.listenerCount(), 0)
assert.equal(host.controls.size + host.settings.size, 0)
assert.equal(warnings.length, 0)
})
}
+2 -1
View File
@@ -11,7 +11,8 @@ import { loadPackage } from './load.js'
async function factoryFromBytes(bytes, module) {
if (module)
return (await import(`data:text/javascript;base64,${Buffer.from(bytes).toString('base64')}`)).default
const context = { module: { exports: {} } }
// VM's implicit console does not forward warnings to the test process.
const context = { module: { exports: {} }, console }
context.exports = context.module.exports
vm.runInNewContext(bytes.toString(), context, { timeout: 5000 })
assert.equal(typeof context.module.exports, 'function')