mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
fix(dash): [PKG-DASH-05] synchronize SDK events and own refresh lifecycle
This commit is contained in:
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
|
||||
@@ -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
@@ -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:
|
||||
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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
@@ -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 与完整安装分发,以及全项目发布复盘继续保留。
|
||||
回退应一起恢复事件模块、入口、测试脚本和构建产物,保留历史失败证据。
|
||||
@@ -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
@@ -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)
|
||||
@@ -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。
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
Reference in new issue
Block a user