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

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

No files matched your search

@@ -0,0 +1,80 @@
# DASH Control implementation
The factory remains `artplayerPluginDashControl(option = {})(art)`. Its result has
`name` and synchronous `update(): void`. Installation does not require `art.dash`
yet; `ready`, `restart`, or explicit `update()` reads and validates that instance.
The caller creates, attaches, replaces, and destroys the SDK. This plugin never
destroys it, changes its source, or adds a runtime dependency on dash.js.
## Module map
| File | Responsibility |
| --- | --- |
| `src/index.js` | Deferred installation, media identity, update revisions, event subscription and cleanup |
| `src/sdk.js` | 4.x qualityIndex vs 5.x representation ID access and manual/Auto selection |
| `src/mapping.js` | Names, current item matching, duplicate labels and selector ordering |
| `src/menu.js` | Existing control/setting registries, menu ownership and guarded selection callbacks |
| `src/audio.svg`, `src/quality.svg` | Existing setting icons |
| `types/artplayer-plugin-dash-control.d.ts` | Existing public declarations; precise TS migration follows in PKG-DASH-04 |
Dependencies flow from the entry into mapping/menu, and from mapping into the SDK
adapter. Mapping has no DOM dependency. Menu receives a model and validity callback;
it does not choose SDK versions. This is the structural step, still in JavaScript.
## Selection and lifetime
An update validates the captured media element and starts a new revision. A callback
from an older update, replaced SDK, or destroyed player returns its previous label
without mutating the SDK or UI. This includes references retained by consumers.
Every caller formatter and SDK operation is followed by a validity check before
subsequent effects. A nested update wins over its older caller.
Manual quality first disables video ABR, then uses the stored SDK selection key.
Auto only enables video ABR. Audio passes the original track object to the SDK.
Successful selection retains the existing notice, controls.check, setting.check
order and synchronous label return. SDK/getName exceptions remain synchronous with
their original identity. Other ABR settings are not overwritten.
Duplicate labels still occupy one row. When a duplicate is current, that row keeps
the selected item's key/object so its highlight and subsequent click agree. Numeric
zero is a valid representation ID; a representation named `auto` is still manual
because the synthetic Auto row is distinguished by its value. Audio prefers object
identity, then a unique match on the current track's available id/index/lang fields.
Ambiguous matches remain unselected instead of selecting an arbitrary track.
Only menus actually installed by this instance are removed on empty topology,
disabled surfaces, failed rendering, or destruction. Where registry lookup exists,
the saved callback identity protects an entry replaced by the caller. Cleanup takes
a snapshot so a synchronous restart cannot cause old cleanup to remove new menus.
Cleanup attempts all owned surfaces/subscriptions even if one removal fails.
Installation failure rolls back subscriptions, including one installed by a throwing
host. The primary error is preserved; secondary cleanup errors are reported.
Keep `dash-quality`/`dash-audio`, selector shape, right control placement/padding,
setting width/icons, defaults and unbound getName calls compatible. Do not use a
filtered representation array index or absoluteIndex as the SDK 5 selection key.
## Validation and next work
Run at the workspace root with pinned Node and Yarn:
```sh
yarn test:dash-control
yarn build artplayer-plugin-dash-control
yarn test:browser test/browser/dash-control.spec.js --trace on
yarn ci:check
```
`ARTPLAYER_TEST_DASH` adds main/legacy/module files to both SDK generations of Node
contracts and lifecycle tests; use the operating system's path delimiter.
`ARTPLAYER_DASH_ARTIFACT` selects the browser plugin artifact. Archive browser report
and result/trace directory before running another suite because the output is shared.
Tests use frozen npm 1.1.0, a frozen pre-refactor 5.x implementation, and candidates.
Browser tests exercise real old/new core DOM, menu clicks, native MP4 and cleanup,
with controlled SDK methods. They do not load dash.js or test MPD/ABR decoding.
PKG-DASH-05 must verify pinned dash.js 4.5.2 and 5.2.1 with real adaptive media and
review SDK-driven refresh/source events and the existing demo lifecycle. Physical
devices and installed package acceptance remain separate release gates.
See [refactor validation](../../refactor/dash-validation.md) for baseline failures
and [task plan](../../refactor/plan.md) for remaining type and release work.
@@ -2,7 +2,9 @@
Dash control plugin for ArtPlayer
Requires dash.js 5.x. dash.js 4.x is not supported.
Accepts a caller-owned dash.js instance at `art.dash`. Quality selection adapts to the
dash.js 4.x quality methods or 5.x representation methods. Version 5 selection uses
representation IDs so bitrate filtering does not change the selected quality.
## Demo
@@ -11,3 +13,8 @@ Requires dash.js 5.x. dash.js 4.x is not supported.
## License
MIT © Harvey Zhao
## Maintenance
See [ARCHITECTURE.md](ARCHITECTURE.md) for module ownership, compatibility rules,
test commands and the remaining real SDK playback validation scope.
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";function t(t,e){const o=new Map;return t.filter(t=>{const n=t[e];return void 0===n||!o.has(n)&&o.set(n,1)})}return function(e={}){return o=>{const{$video:n}=o.template,{errorHandle:i}=o.constructor.utils;function a(){i(o.dash.getVideoElement()===n,'Cannot find instance of DASH from "art.dash"'),function(n){const i=n.getRepresentationsByType("video");if(!i||!i.length)return;const a=e.quality||{},l=a.auto||"Auto",s=a.title||"Quality",r=a.getName||(t=>`${t.height}p`),c=n.getCurrentRepresentationForType("video"),u=c&&c.id,d=n.getSettings().streaming.abr.autoSwitchBitrate.video,h=!d&&c?r(c):l,g=t(i.map((t,e)=>({html:r(t),value:e,id:t.id,default:!d&&!!u&&t.id===u})),"html").sort((t,e)=>e.value-t.value);g.push({html:l,value:"auto",default:d});const p=t=>("auto"===t.value?n.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!0}}}}):(n.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!1}}}}),n.setRepresentationForTypeById("video",t.id)),o.notice.show=`${s}: ${t.html}`,a.control&&o.controls.check(t),a.setting&&o.setting.check(t),t.html);a.control&&o.controls.update({name:"dash-quality",position:"right",html:h,style:{padding:"0 10px"},selector:g,onSelect:p}),a.setting&&o.setting.update({name:"dash-quality",tooltip:h,html:s,icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>',width:200,selector:g,onSelect:p})}(o.dash),function(n){const i=n.getTracksFor("audio");if(!i||!i.length)return;const a=e.audio||{},l=a.auto||"Auto",s=a.title||"Audio",r=a.getName||(t=>t.lang||t.id),c=n.getCurrentTrackFor("audio")||i[0],u=c?r(c):l,d=t(i.map(t=>({html:r(t),value:t,default:c===t})),"html"),h=t=>(n.setCurrentTrack(t.value),o.notice.show=`${s}: ${t.html}`,a.control&&o.controls.check(t),a.setting&&o.setting.check(t),t.html);a.control&&o.controls.update({name:"dash-audio",position:"right",html:u,style:{padding:"0 10px"},selector:d,onSelect:h}),a.setting&&o.setting.update({name:"dash-audio",tooltip:u,html:s,icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>',width:200,selector:d,onSelect:h})}(o.dash)}return o.on("ready",a),o.on("restart",a),{name:"artplayerPluginDashControl",update:a}}}});
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function e(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){if(!o||r.get(n)!==o)return;r.delete(n);const l=t[n];"controls"===n&&l.cache?.get&&l.cache.get(e)?.option?.onSelect!==o||"setting"===n&&l.find&&l.find(e)?.onSelect!==o||l.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,s,u){if(!s)return void i();const a={};o=a;const f=()=>a===o&&u(),d=e=>f()?(s.select(e,f),f()?(t.notice.show=`${s.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:s.html,style:{padding:"0 10px"},selector:s.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:s.html,html:s.title,icon:n,width:200,selector:s.selector,onSelect:d})):l("setting"))},clear:i}}return function(n={}){return o=>{const{$video:r}=o.template,{errorHandle:l}=o.constructor.utils;let i=!1,c=0;const s=e(o,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),u=e(o,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),a=[];function f(t=()=>!0){let e;for(const o of[s.clear,u.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function d(){if(i||o.isDestroy)return;const e=++c,a=o.dash,d=()=>!i&&!o.isDestroy&&e===c&&o.dash===a,h=()=>d()&&a.getVideoElement()===r&&d();try{if(l(a.getVideoElement()===r,'Cannot find instance of DASH from "art.dash"'),!d())return;const e=n.quality||{},o=function(e,n,o){const r=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(e),l=r.levels();if(!o()||!l?.length)return null;const i=n.auto||"Auto",c=n.getName||(t=>`${t.height}p`),s=r.current(l);if(!o())return null;const u=e.getSettings().streaming.abr.autoSwitchBitrate.video;if(!o())return null;const a=!u&&s?c(s):i;if(!o())return null;const f=[];for(const[t,h]of l.entries()){const e=c(h);if(!o())return null;f.push({html:e,...r.item(h,t,s,u)})}const d=t(f).sort((t,e)=>e.value-t.value);return d.push({html:i,value:"auto",default:u}),{html:a,title:n.title||"Quality",selector:d,select:r.select}}(a,e,h);if(!h())return;if(s.update(e,o,h),!h())return;const i=n.audio||{},c=function(e,n,o){const r=e.getTracksFor("audio");if(!o()||!r?.length)return null;const l=n.auto||"Auto",i=n.getName||(t=>t.lang||t.id),c=e.getCurrentTrackFor("audio")||r[0];if(!o())return null;const s=c?i(c):l;if(!o())return null;const u=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=t.filter(t=>["id","index","lang"].every(n=>null==e[n]||t[n]===e[n]));return 1===n.length?n[0]:void 0}(r,c),a=[];for(const t of r){const e=i(t);if(!o())return null;a.push({html:e,value:t,default:t===u})}const f=t(a);return{html:s,title:n.title||"Audio",selector:f,select:t=>e.setCurrentTrack(t.value)}}(a,i,h);h()&&u.update(i,c,h)}catch(g){if(d()){const t=++c;try{f(()=>c===t)}catch(p){console.warn("ArtPlayer DASH cleanup failed:",p)}}throw g}}function h(){if(i)return;let t;i=!0,c++;const e=[f,...a.splice(0).map(([t,e])=>()=>o.off(t,e))Line truncated
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";function t(t,e){const o=new Map;return t.filter(t=>{const n=t[e];return void 0===n||!o.has(n)&&o.set(n,1)})}return function(e={}){return o=>{const{$video:n}=o.template,{errorHandle:i}=o.constructor.utils;function a(){i(o.dash.getVideoElement()===n,'Cannot find instance of DASH from "art.dash"'),function(n){const i=n.getRepresentationsByType("video");if(!i||!i.length)return;const a=e.quality||{},l=a.auto||"Auto",s=a.title||"Quality",r=a.getName||(t=>`${t.height}p`),c=n.getCurrentRepresentationForType("video"),u=c&&c.id,d=n.getSettings().streaming.abr.autoSwitchBitrate.video,h=!d&&c?r(c):l,g=t(i.map((t,e)=>({html:r(t),value:e,id:t.id,default:!d&&!!u&&t.id===u})),"html").sort((t,e)=>e.value-t.value);g.push({html:l,value:"auto",default:d});const p=t=>("auto"===t.value?n.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!0}}}}):(n.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:!1}}}}),n.setRepresentationForTypeById("video",t.id)),o.notice.show=`${s}: ${t.html}`,a.control&&o.controls.check(t),a.setting&&o.setting.check(t),t.html);a.control&&o.controls.update({name:"dash-quality",position:"right",html:h,style:{padding:"0 10px"},selector:g,onSelect:p}),a.setting&&o.setting.update({name:"dash-quality",tooltip:h,html:s,icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>',width:200,selector:g,onSelect:p})}(o.dash),function(n){const i=n.getTracksFor("audio");if(!i||!i.length)return;const a=e.audio||{},l=a.auto||"Auto",s=a.title||"Audio",r=a.getName||(t=>t.lang||t.id),c=n.getCurrentTrackFor("audio")||i[0],u=c?r(c):l,d=t(i.map(t=>({html:r(t),value:t,default:c===t})),"html"),h=t=>(n.setCurrentTrack(t.value),o.notice.show=`${s}: ${t.html}`,a.control&&o.controls.check(t),a.setting&&o.setting.check(t),t.html);a.control&&o.controls.update({name:"dash-audio",position:"right",html:u,style:{padding:"0 10px"},selector:d,onSelect:h}),a.setting&&o.setting.update({name:"dash-audio",tooltip:u,html:s,icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>',width:200,selector:d,onSelect:h})}(o.dash)}return o.on("ready",a),o.on("restart",a),{name:"artplayerPluginDashControl",update:a}}}});
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginDashControl=e(),define(function(){return t.artplayerPluginDashControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginDashControl=e()}(this,function(){"use strict";var t=Object.defineProperty,e=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,r=(e,n,o)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):e[n]=o,l=(t,l)=>{for(var i in l||(l={}))n.call(l,i)&&r(t,i,l[i]);if(e)for(var i of e(l))o.call(l,i)&&r(t,i,l[i]);return t};function i(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const n=e.get(t.html);return n?(t.default&&Object.assign(n,t),!1):(e.set(t.html,t),!0)})}function c(t,e,n){let o;const r=new Map;function l(n,o=r.get(n)){var l,i,c,u;if(!o||r.get(n)!==o)return;r.delete(n);const s=t[n];"controls"===n&&(null==(l=s.cache)?void 0:l.get)&&(null==(c=null==(i=s.cache.get(e))?void 0:i.option)?void 0:c.onSelect)!==o||"setting"===n&&s.find&&(null==(u=s.find(e))?void 0:u.onSelect)!==o||s.remove(e)}function i(){let t;o=void 0;for(const[n,o]of[...r])try{l(n,o)}catch(e){t||(t=e)}if(t)throw t}return{update:function(c,u,s){if(!u)return void i();const a={};o=a;const f=()=>a===o&&s(),d=e=>f()?(u.select(e,f),f()?(t.notice.show=`${u.title}: ${e.html}`,f()&&c.control&&t.controls.check(e),f()&&c.setting&&t.setting.check(e),e.html):e.html):e.html;c.control?(r.set("controls",d),t.controls.update({name:e,position:"right",html:u.html,style:{padding:"0 10px"},selector:u.selector,onSelect:d})):l("controls"),f()&&(c.setting?(r.set("setting",d),t.setting.update({name:e,tooltip:u.html,html:u.title,icon:n,width:200,selector:u.selector,onSelect:d})):l("setting"))},clear:i}}return function(t={}){return e=>{const{$video:n}=e.template,{errorHandle:o}=e.constructor.utils;let r=!1,u=0;const s=c(e,"dash-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),a=c(e,"dash-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),f=[];function d(t=()=>!0){let e;for(const o of[s.clear,a.clear]){if(!t())break;try{o()}catch(n){e||(e=n)}}if(e)throw e}function h(){if(r||e.isDestroy)return;const c=++u,f=e.dash,h=()=>!r&&!e.isDestroy&&c===u&&e.dash===f,p=()=>h()&&f.getVideoElement()===n&&h();try{if(o(f.getVideoElement()===n,'Cannot find instance of DASH from "art.dash"'),!h())return;const e=t.quality||{},r=function(t,e,n){const o=function(t){const e="function"===typeof t.getRepresentationsByType;return{levels:()=>e?t.getRepresentationsByType("video"):t.getBitrateInfoListFor("video"),current(n){if(e)return t.getCurrentRepresentationForType("video");const o=t.getQualityFor("video");return n.find(t=>t.qualityIndex===o)},item:(t,n,o,r)=>e?{value:n,id:t.id,default:!r&&null!=o&&t.id===o.id}:{value:t.qualityIndex,default:!r&&null!=o&&t.qualityIndex===o.qualityIndex},select(n,o){const r="auto"===n.value;t.updateSettings({streaming:{abr:{autoSwitchBitrate:{video:r}}}}),!r&&o()&&(e?t.setRepresentationForTypeById("video",n.id):t.setQualityFor("video",n.value))}}}(t),r=o.levels();if(!n()||!(null==r?void 0:r.length))return null;const c=e.auto||"Auto",u=e.getName||(t=>`${t.height}p`),s=o.current(r);if(!n())return null;const a=t.getSettings().streaming.abr.autoSwitchBitrate.video;if(!n())return null;const f=!a&&s?u(s):c;if(!n())return null;const d=[];for(const[i,p]of r.entries()){const t=u(p);if(!n())return null;d.push(l({html:t},o.item(p,i,s,a)))}const h=i(d).sort((t,e)=>e.value-t.value);return h.push({html:c,value:"auto",default:a}),{html:f,title:e.title||"Quality",selector:h,select:o.select}}(f,e,p);if(!p())return;if(s.update(e,r,p),!p())return;const c=t.audio||{},u=function(t,e,n){const o=t.getTracksFor("audio");if(!n()||!(null==o?void 0:o.length))return null;const r=e.auto||"Auto",l=e.getName||(t=>t.lang||t.id),c=t.getCurrentTrackFor("audio")||o[0];if(!n())return null;const u=c?l(c):r;if(!n())return null;const s=function(t,e){if(t.includes(e))return e;if(null==e.id&&null==e.index)return;const n=t.filter(t=>["id","index","lang"].every(n=>null==e[n]||t[n]===e[n]));return 1===n.lLine truncated
@@ -5,163 +5,260 @@
* Released under the MIT License.
*/
const $audio = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>';
const $quality = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
function uniqBy(array, property) {
const seen = /* @__PURE__ */ new Map();
return array.filter((item) => {
const key = item[property];
if (key === void 0) {
return true;
function qualityAdapter(dash) {
const modern = typeof dash.getRepresentationsByType === "function";
return {
levels: () => modern ? dash.getRepresentationsByType("video") : dash.getBitrateInfoListFor("video"),
current(levels) {
if (modern)
return dash.getCurrentRepresentationForType("video");
const quality = dash.getQualityFor("video");
return levels.find((level) => level.qualityIndex === quality);
},
item(level, index, selected, automatic) {
return modern ? { value: index, id: level.id, default: !automatic && selected != null && level.id === selected.id } : { value: level.qualityIndex, default: !automatic && selected != null && level.qualityIndex === selected.qualityIndex };
},
select(item, valid) {
const automatic = item.value === "auto";
dash.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: automatic } } } });
if (!automatic && valid()) {
if (modern)
dash.setRepresentationForTypeById("video", item.id);
else
dash.setQualityFor("video", item.value);
}
}
return !seen.has(key) && seen.set(key, 1);
};
}
function uniqueLabels(items) {
const seen = /* @__PURE__ */ new Map();
return items.filter((item) => {
if (item.html === void 0)
return true;
const first = seen.get(item.html);
if (first) {
if (item.default)
Object.assign(first, item);
return false;
}
seen.set(item.html, item);
return true;
});
}
function qualityModel(dash, config, active) {
const adapter = qualityAdapter(dash);
const levels = adapter.levels();
if (!active() || !levels?.length)
return null;
const auto = config.auto || "Auto";
const getName = config.getName || ((level) => `${level.height}p`);
const selected = adapter.current(levels);
if (!active())
return null;
const automatic = dash.getSettings().streaming.abr.autoSwitchBitrate.video;
if (!active())
return null;
const html = !automatic && selected ? getName(selected) : auto;
if (!active())
return null;
const items = [];
for (const [index, level] of levels.entries()) {
const label = getName(level);
if (!active())
return null;
items.push({ html: label, ...adapter.item(level, index, selected, automatic) });
}
const selector = uniqueLabels(items).sort((left, right) => right.value - left.value);
selector.push({ html: auto, value: "auto", default: automatic });
return { html, title: config.title || "Quality", selector, select: adapter.select };
}
function selectedTrack(tracks, current) {
if (tracks.includes(current))
return current;
if (current.id == null && current.index == null)
return void 0;
const matching = tracks.filter((track) => ["id", "index", "lang"].every((key) => current[key] == null || track[key] === current[key]));
return matching.length === 1 ? matching[0] : void 0;
}
function audioModel(dash, config, active) {
const tracks = dash.getTracksFor("audio");
if (!active() || !tracks?.length)
return null;
const auto = config.auto || "Auto";
const getName = config.getName || ((track) => track.lang || track.id);
const current = dash.getCurrentTrackFor("audio") || tracks[0];
if (!active())
return null;
const html = current ? getName(current) : auto;
if (!active())
return null;
const selected = selectedTrack(tracks, current);
const items = [];
for (const track of tracks) {
const label = getName(track);
if (!active())
return null;
items.push({ html: label, value: track, default: track === selected });
}
const selector = uniqueLabels(items);
return { html, title: config.title || "Audio", selector, select: (item) => dash.setCurrentTrack(item.value) };
}
function createMenu(art, name, icon) {
let current;
const owned = /* @__PURE__ */ new Map();
function remove(surface, callback = owned.get(surface)) {
if (!callback || owned.get(surface) !== callback)
return;
owned.delete(surface);
const registry = art[surface];
if (surface === "controls" && registry.cache?.get && registry.cache.get(name)?.option?.onSelect !== callback)
return;
if (surface === "setting" && registry.find && registry.find(name)?.onSelect !== callback)
return;
registry.remove(name);
}
function clear() {
current = void 0;
let failure;
for (const [surface, callback] of [...owned]) {
try {
remove(surface, callback);
} catch (error) {
failure || (failure = error);
}
}
if (failure)
throw failure;
}
function update(config, model, active) {
if (!model) {
clear();
return;
}
const state = {};
current = state;
const valid = () => state === current && active();
const onSelect = (item) => {
if (!valid())
return item.html;
model.select(item, valid);
if (!valid())
return item.html;
art.notice.show = `${model.title}: ${item.html}`;
if (valid() && config.control)
art.controls.check(item);
if (valid() && config.setting)
art.setting.check(item);
return item.html;
};
if (config.control) {
owned.set("controls", onSelect);
art.controls.update({ name, position: "right", html: model.html, style: { padding: "0 10px" }, selector: model.selector, onSelect });
} else {
remove("controls");
}
if (!valid())
return;
if (config.setting) {
owned.set("setting", onSelect);
art.setting.update({ name, tooltip: model.html, html: model.title, icon, width: 200, selector: model.selector, onSelect });
} else {
remove("setting");
}
}
return { update, clear };
}
const $quality = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
function artplayerPluginDashControl(option = {}) {
return (art) => {
const { $video } = art.template;
const { errorHandle } = art.constructor.utils;
function updateQuality(dash) {
const qualities = dash.getRepresentationsByType("video");
if (!qualities || !qualities.length)
return;
const config = option.quality || {};
const auto = config.auto || "Auto";
const title = config.title || "Quality";
const getName = config.getName || ((level) => `${level.height}p`);
const currentRepresentation = dash.getCurrentRepresentationForType("video");
const currentId = currentRepresentation && currentRepresentation.id;
const currentAuto = dash.getSettings().streaming.abr.autoSwitchBitrate.video;
const defaultHtml = !currentAuto && currentRepresentation ? getName(currentRepresentation) : auto;
const selector = uniqBy(
qualities.map((item, index) => {
return {
html: getName(item),
value: index,
id: item.id,
default: !currentAuto && !!currentId && item.id === currentId
};
}),
"html"
).sort((a, b) => b.value - a.value);
selector.push({
html: auto,
value: "auto",
default: currentAuto
});
const onSelect = (item) => {
if (item.value === "auto") {
dash.updateSettings({
streaming: {
abr: {
autoSwitchBitrate: {
video: true
}
}
}
});
} else {
dash.updateSettings({
streaming: {
abr: {
autoSwitchBitrate: {
video: false
}
}
}
});
dash.setRepresentationForTypeById("video", item.id);
let closed = false;
let revision = 0;
const quality = createMenu(art, "dash-quality", $quality);
const audio = createMenu(art, "dash-audio", $audio);
const subscriptions = [];
function clear(current = () => true) {
let failure;
for (const cleanup of [quality.clear, audio.clear]) {
if (!current())
break;
try {
cleanup();
} catch (error) {
failure || (failure = error);
}
art.notice.show = `${title}: ${item.html}`;
if (config.control)
art.controls.check(item);
if (config.setting)
art.setting.check(item);
return item.html;
};
if (config.control) {
art.controls.update({
name: "dash-quality",
position: "right",
html: defaultHtml,
style: { padding: "0 10px" },
selector,
onSelect
});
}
if (config.setting) {
art.setting.update({
name: "dash-quality",
tooltip: defaultHtml,
html: title,
icon: $quality,
width: 200,
selector,
onSelect
});
}
}
function updateAudio(dash) {
const audioTracks = dash.getTracksFor("audio");
if (!audioTracks || !audioTracks.length)
return;
const config = option.audio || {};
const auto = config.auto || "Auto";
const title = config.title || "Audio";
const getName = config.getName || ((track) => track.lang || track.id);
const currentTrack = dash.getCurrentTrackFor("audio") || audioTracks[0];
const defaultHtml = currentTrack ? getName(currentTrack) : auto;
const selector = uniqBy(
audioTracks.map((item) => {
return {
html: getName(item),
value: item,
default: currentTrack === item
};
}),
"html"
);
const onSelect = (item) => {
dash.setCurrentTrack(item.value);
art.notice.show = `${title}: ${item.html}`;
if (config.control)
art.controls.check(item);
if (config.setting)
art.setting.check(item);
return item.html;
};
if (config.control) {
art.controls.update({
name: "dash-audio",
position: "right",
html: defaultHtml,
style: { padding: "0 10px" },
selector,
onSelect
});
}
if (config.setting) {
art.setting.update({
name: "dash-audio",
tooltip: defaultHtml,
html: title,
icon: $audio,
width: 200,
selector,
onSelect
});
}
if (failure)
throw failure;
}
function update() {
errorHandle(art.dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"');
updateQuality(art.dash);
updateAudio(art.dash);
if (closed || art.isDestroy)
return;
const version = ++revision;
const dash = art.dash;
const current = () => !closed && !art.isDestroy && version === revision && art.dash === dash;
const valid = () => current() && dash.getVideoElement() === $video && current();
try {
errorHandle(dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"');
if (!current())
return;
const qualityConfig = option.quality || {};
const qualities = qualityModel(dash, qualityConfig, valid);
if (!valid())
return;
quality.update(qualityConfig, qualities, valid);
if (!valid())
return;
const audioConfig = option.audio || {};
const tracks = audioModel(dash, audioConfig, valid);
if (valid())
audio.update(audioConfig, tracks, valid);
} catch (error) {
if (current()) {
const cleanupVersion = ++revision;
try {
clear(() => revision === cleanupVersion);
} catch (cleanupError) {
console.warn("ArtPlayer DASH cleanup failed:", cleanupError);
}
}
throw error;
}
}
art.on("ready", update);
art.on("restart", update);
return {
name: "artplayerPluginDashControl",
update
};
function destroy() {
if (closed)
return;
closed = true;
revision++;
let failure;
const actions = [clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))];
for (const action of actions) {
try {
action();
} catch (error) {
failure || (failure = error);
}
}
if (failure)
throw failure;
}
try {
for (const entry of [["ready", update], ["restart", update], ["destroy", destroy]]) {
if (closed)
break;
subscriptions.push(entry);
art.on(...entry);
}
} catch (error) {
try {
destroy();
} catch (cleanupError) {
console.warn("ArtPlayer DASH subscription cleanup failed:", cleanupError);
}
throw error;
}
return { name: "artplayerPluginDashControl", update };
};
}
export {
@@ -1,179 +1,107 @@
import $audio from './audio.svg?raw'
import { audioModel, qualityModel } from './mapping'
import { createMenu } from './menu'
import $quality from './quality.svg?raw'
function uniqBy(array, property) {
const seen = new Map()
return array.filter((item) => {
const key = item[property]
if (key === undefined) {
return true
}
return !seen.has(key) && seen.set(key, 1)
})
}
export default function artplayerPluginDashControl(option = {}) {
return (art) => {
const { $video } = art.template
const { errorHandle } = art.constructor.utils
let closed = false
let revision = 0
const quality = createMenu(art, 'dash-quality', $quality)
const audio = createMenu(art, 'dash-audio', $audio)
const subscriptions = []
function updateQuality(dash) {
const qualities = dash.getRepresentationsByType('video')
if (!qualities || !qualities.length)
return
const config = option.quality || {}
const auto = config.auto || 'Auto'
const title = config.title || 'Quality'
const getName = config.getName || (level => `${level.height}p`)
const currentRepresentation = dash.getCurrentRepresentationForType('video')
const currentId = currentRepresentation && currentRepresentation.id
const currentAuto = dash.getSettings().streaming.abr.autoSwitchBitrate.video
const defaultHtml = !currentAuto && currentRepresentation ? getName(currentRepresentation) : auto
const selector = uniqBy(
qualities.map((item, index) => {
return {
html: getName(item),
value: index,
id: item.id,
default: !currentAuto && !!currentId && item.id === currentId,
}
}),
'html',
).sort((a, b) => b.value - a.value)
selector.push({
html: auto,
value: 'auto',
default: currentAuto,
})
const onSelect = (item) => {
if (item.value === 'auto') {
dash.updateSettings({
streaming: {
abr: {
autoSwitchBitrate: {
video: true,
},
},
},
})
function clear(current = () => true) {
let failure
for (const cleanup of [quality.clear, audio.clear]) {
if (!current())
break
try {
cleanup()
}
else {
dash.updateSettings({
streaming: {
abr: {
autoSwitchBitrate: {
video: false,
},
},
},
})
dash.setRepresentationForTypeById('video', item.id)
catch (error) {
failure ||= error
}
art.notice.show = `${title}: ${item.html}`
if (config.control)
art.controls.check(item)
if (config.setting)
art.setting.check(item)
return item.html
}
if (config.control) {
art.controls.update({
name: 'dash-quality',
position: 'right',
html: defaultHtml,
style: { padding: '0 10px' },
selector,
onSelect,
})
}
if (config.setting) {
art.setting.update({
name: 'dash-quality',
tooltip: defaultHtml,
html: title,
icon: $quality,
width: 200,
selector,
onSelect,
})
}
}
function updateAudio(dash) {
const audioTracks = dash.getTracksFor('audio')
if (!audioTracks || !audioTracks.length)
return
const config = option.audio || {}
const auto = config.auto || 'Auto'
const title = config.title || 'Audio'
const getName = config.getName || (track => track.lang || track.id)
const currentTrack = dash.getCurrentTrackFor('audio') || audioTracks[0]
const defaultHtml = currentTrack ? getName(currentTrack) : auto
const selector = uniqBy(
audioTracks.map((item) => {
return {
html: getName(item),
value: item,
default: currentTrack === item,
}
}),
'html',
)
const onSelect = (item) => {
dash.setCurrentTrack(item.value)
art.notice.show = `${title}: ${item.html}`
if (config.control)
art.controls.check(item)
if (config.setting)
art.setting.check(item)
return item.html
}
if (config.control) {
art.controls.update({
name: 'dash-audio',
position: 'right',
html: defaultHtml,
style: { padding: '0 10px' },
selector,
onSelect,
})
}
if (config.setting) {
art.setting.update({
name: 'dash-audio',
tooltip: defaultHtml,
html: title,
icon: $audio,
width: 200,
selector,
onSelect,
})
}
if (failure)
throw failure
}
function update() {
errorHandle(art.dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"')
updateQuality(art.dash)
updateAudio(art.dash)
if (closed || art.isDestroy)
return
const version = ++revision
const dash = art.dash
const current = () => !closed && !art.isDestroy && version === revision && art.dash === dash
const valid = () => current() && dash.getVideoElement() === $video && current()
try {
errorHandle(dash.getVideoElement() === $video, 'Cannot find instance of DASH from "art.dash"')
if (!current())
return
const qualityConfig = option.quality || {}
const qualities = qualityModel(dash, qualityConfig, valid)
if (!valid())
return
quality.update(qualityConfig, qualities, valid)
if (!valid())
return
const audioConfig = option.audio || {}
const tracks = audioModel(dash, audioConfig, valid)
if (valid())
audio.update(audioConfig, tracks, valid)
}
catch (error) {
if (current()) {
const cleanupVersion = ++revision
try {
clear(() => revision === cleanupVersion)
}
catch (cleanupError) {
console.warn('ArtPlayer DASH cleanup failed:', cleanupError)
}
}
throw error
}
}
art.on('ready', update)
art.on('restart', update)
return {
name: 'artplayerPluginDashControl',
update,
function destroy() {
if (closed)
return
closed = true
revision++
let failure
const actions = [clear, ...subscriptions.splice(0).map(([name, callback]) => () => art.off(name, callback))]
for (const action of actions) {
try {
action()
}
catch (error) {
failure ||= error
}
}
if (failure)
throw failure
}
try {
for (const entry of [['ready', update], ['restart', update], ['destroy', destroy]]) {
if (closed)
break
subscriptions.push(entry)
art.on(...entry)
}
}
catch (error) {
try {
destroy()
}
catch (cleanupError) {
console.warn('ArtPlayer DASH subscription cleanup failed:', cleanupError)
}
throw error
}
return { name: 'artplayerPluginDashControl', update }
}
}
@@ -0,0 +1,79 @@
import { qualityAdapter } from './sdk'
function uniqueLabels(items) {
const seen = new Map()
return items.filter((item) => {
if (item.html === undefined)
return true
const first = seen.get(item.html)
if (first) {
if (item.default)
Object.assign(first, item)
return false
}
seen.set(item.html, item)
return true
})
}
export function qualityModel(dash, config, active) {
const adapter = qualityAdapter(dash)
const levels = adapter.levels()
if (!active() || !levels?.length)
return null
const auto = config.auto || 'Auto'
const getName = config.getName || (level => `${level.height}p`)
const selected = adapter.current(levels)
if (!active())
return null
const automatic = dash.getSettings().streaming.abr.autoSwitchBitrate.video
if (!active())
return null
const html = !automatic && selected ? getName(selected) : auto
if (!active())
return null
const items = []
for (const [index, level] of levels.entries()) {
const label = getName(level)
if (!active())
return null
items.push({ html: label, ...adapter.item(level, index, selected, automatic) })
}
const selector = uniqueLabels(items)
.sort((left, right) => right.value - left.value)
selector.push({ html: auto, value: 'auto', default: automatic })
return { html, title: config.title || 'Quality', selector, select: adapter.select }
}
function selectedTrack(tracks, current) {
if (tracks.includes(current))
return current
if (current.id == null && current.index == null)
return undefined
const matching = tracks.filter(track => ['id', 'index', 'lang'].every(key => current[key] == null || track[key] === current[key]))
return matching.length === 1 ? matching[0] : undefined
}
export function audioModel(dash, config, active) {
const tracks = dash.getTracksFor('audio')
if (!active() || !tracks?.length)
return null
const auto = config.auto || 'Auto'
const getName = config.getName || (track => track.lang || track.id)
const current = dash.getCurrentTrackFor('audio') || tracks[0]
if (!active())
return null
const html = current ? getName(current) : auto
if (!active())
return null
const selected = selectedTrack(tracks, current)
const items = []
for (const track of tracks) {
const label = getName(track)
if (!active())
return null
items.push({ html: label, value: track, default: track === selected })
}
const selector = uniqueLabels(items)
return { html, title: config.title || 'Audio', selector, select: item => dash.setCurrentTrack(item.value) }
}
@@ -0,0 +1,72 @@
export function createMenu(art, name, icon) {
let current
const owned = new Map()
function remove(surface, callback = owned.get(surface)) {
if (!callback || owned.get(surface) !== callback)
return
owned.delete(surface)
const registry = art[surface]
if (surface === 'controls' && registry.cache?.get && registry.cache.get(name)?.option?.onSelect !== callback)
return
if (surface === 'setting' && registry.find && registry.find(name)?.onSelect !== callback)
return
registry.remove(name)
}
function clear() {
current = undefined
let failure
for (const [surface, callback] of [...owned]) {
try {
remove(surface, callback)
}
catch (error) {
failure ||= error
}
}
if (failure)
throw failure
}
function update(config, model, active) {
if (!model) {
clear()
return
}
const state = {}
current = state
const valid = () => state === current && active()
const onSelect = (item) => {
if (!valid())
return item.html
model.select(item, valid)
if (!valid())
return item.html
art.notice.show = `${model.title}: ${item.html}`
if (valid() && config.control)
art.controls.check(item)
if (valid() && config.setting)
art.setting.check(item)
return item.html
}
if (config.control) {
owned.set('controls', onSelect)
art.controls.update({ name, position: 'right', html: model.html, style: { padding: '0 10px' }, selector: model.selector, onSelect })
}
else {
remove('controls')
}
if (!valid())
return
if (config.setting) {
owned.set('setting', onSelect)
art.setting.update({ name, tooltip: model.html, html: model.title, icon, width: 200, selector: model.selector, onSelect })
}
else {
remove('setting')
}
}
return { update, clear }
}
@@ -0,0 +1,27 @@
export function qualityAdapter(dash) {
const modern = typeof dash.getRepresentationsByType === 'function'
return {
levels: () => modern ? dash.getRepresentationsByType('video') : dash.getBitrateInfoListFor('video'),
current(levels) {
if (modern)
return dash.getCurrentRepresentationForType('video')
const quality = dash.getQualityFor('video')
return levels.find(level => level.qualityIndex === quality)
},
item(level, index, selected, automatic) {
return modern
? { value: index, id: level.id, default: !automatic && selected != null && level.id === selected.id }
: { value: level.qualityIndex, default: !automatic && selected != null && level.qualityIndex === selected.qualityIndex }
},
select(item, valid) {
const automatic = item.value === 'auto'
dash.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: automatic } } } })
if (!automatic && valid()) {
if (modern)
dash.setRepresentationForTypeById('video', item.id)
else
dash.setQualityFor('video', item.value)
}
},
}
}