mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
refactor(hls): [PKG-HLS-03] separate menus and SDK lifecycle
This commit is contained in:
1 parent
33f6d19b77
commit
ba93b98be5
24 files changed
+5849
-382
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.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t,e){const l=new Map;return t.filter(t=>{const o=t[e];return void 0===o||!l.has(o)&&l.set(o,1)})}return function(e={}){return l=>{const{$video:o}=l.template,{errorHandle:n}=l.constructor.utils;function i(){n(l.hls?.media===o,'Cannot find instance of HLS from "art.hls"'),function(o){if(!o.levels.length)return;const n=e.quality||{},i=n.auto||"Auto",s=n.title||"Quality",a=n.getName||(t=>t.name||`${t.height}P`),c=o.levels[o.currentLevel],r=c?a(c):i,u=t(o.levels.map((t,e)=>({html:a(t,e),value:e,default:o.currentLevel===e})),"html").sort((t,e)=>e.value-t.value);u.push({html:i,value:-1,default:-1===o.currentLevel});const h=t=>(o.currentLevel=t.value,l.notice.show=`${s}: ${t.html}`,n.control&&l.controls.check(t),n.setting&&l.setting.check(t),t.html);n.control&&l.controls.update({name:"hls-quality",position:"right",html:r,style:{padding:"0 10px"},selector:u,onSelect:h}),n.setting&&l.setting.update({name:"hls-quality",tooltip:r,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:u,onSelect:h})}(l.hls),function(o){if(!o.audioTracks.length)return;const n=e.audio||{},i=n.auto||"Auto",s=n.title||"Audio",a=n.getName||(t=>t.name||t.lang||t.language),c=o.audioTracks[o.audioTrack],r=c?a(c):i,u=t(o.audioTracks.map((t,e)=>({html:a(t,e),value:t.id,default:o.audioTrack===t.id})),"html"),h=t=>(o.audioTrack=t.value,l.notice.show=`${s}: ${t.html}`,n.control&&l.controls.check(t),n.setting&&l.setting.check(t),t.html);n.control&&l.controls.update({name:"hls-audio",position:"right",html:r,style:{padding:"0 10px"},selector:u,onSelect:h}),n.setting&&l.setting.update({name:"hls-audio",tooltip:r,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:u,onSelect:h})}(l.hls)}return l.on("ready",i),l.on("restart",i),{name:"artplayerPluginHlsControl",update:i}}}});
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const o=e.get(t.html);return o?(t.default&&(o.default=!0,o.value=t.value),!1):(e.set(t.html,t),!0)})}function e(t,e,o,n,l,i){let r;const c={control:!1,setting:!1};function s(o){c[o]&&(c[o]=!1,t["control"===o?"controls":"setting"].remove(e))}function u(){r=void 0,s("control"),s("setting")}function a(e,o){for(const t of e.model.selector)t.default=t===o;e.config.control&&t.controls.check(o),e===r&&l(e.hls)&&e.config.setting&&t.setting.check(o)}return{update:function(f,d,h,m){if(!h)return void u();const g=r,v=h.selector.findIndex(t=>t.default);if(!m&&g?.hls===f&&g.model.title===h.title&&(-1!==v||!g.model.selector.some(t=>t.default)&&g.model.html===h.html)&&c.control===Boolean(d.control)&&c.setting===Boolean(d.setting)&&g.model.selector.length===h.selector.length&&g.model.selector.every((t,e)=>t.html===h.selector[e].html&&t.value===h.selector[e].value)){g.config=d;const t=g.model.selector[v];return void(!t||t.default&&g.model.html===h.html||(g.model.html=h.html,a(g,t)))}const p={hls:f,config:d,model:h};r=p;const y=()=>r===p&&l(f),T=e=>i(()=>y()?(f[o]=e.value,y()?(h.html=e.html,t.notice.show=`${h.title}: ${e.html}`,y()&&a(p,e),e.html):e.html):e.html);d.control?(c.control=!0,t.controls.update({name:e,position:"right",html:h.html,style:{padding:"0 10px"},selector:h.selector,onSelect:T})):s("control"),y()&&(d.setting?(c.setting=!0,t.setting.update({name:e,tooltip:h.html,html:h.title,icon:n,width:200,selector:h.selector,onSelect:T})):s("setting"))},clear:u,invalidate:function(){r=void 0}}}return function(o={}){return n=>{const{$video:l}=n.template,{errorHandle:i}=n.constructor.utils;let r,c,s=!1,u=0,a=()=>{},f=0;const d=new WeakSet,h=t=>!s&&!d.has(t)&&n.hls===t&&t.media===l,m=e(n,"hls-quality","currentLevel",'<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>',h,v),g=e(n,"hls-audio","audioTrack",'<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>',h,v);function v(t){f++;let e=!1;try{const o=t();return e=!0,o}finally{if(f--,!f){const t=c;c=void 0,e&&t&&p(t,!1)}}}function p(e,n){if(!h(e))return;const l=++u,i=o.quality||{},r=function(e,o){if(!e.levels.length)return null;const n=o.auto||"Auto",l=o.getName||(t=>t.name||`${t.height}P`),i=function(t){return!0===t.autoLevelEnabled?-1:t.currentLevel}(e),r=e.levels[i],c=r?l(r):n,s=t(e.levels.map((t,e)=>({html:l(t,e),value:e,default:i===e}))).sort((t,e)=>e.value-t.value);return s.push({html:n,value:-1,default:-1===i}),{html:c,title:o.title||"Quality",selector:s}}(e,i);if(!h(e)||l!==u)return;if(m.update(e,i,r,n),!h(e)||l!==u)return;const c=o.audio||{},s=function(e,o){if(!e.audioTracks.length)return null;const n=o.auto||"Auto",l=o.getName||(t=>t.name||t.lang||t.language),i=e.audioTracks[e.audioTrack],r=i?l(i):n,c=t(e.audioTracks.map((t,o)=>({html:l(t,o),value:t.id,default:e.audioTrack===t.id})));return{html:r,title:o.title||"Audio",selector:c}}(e,c);h(e)&&l===u&&g.update(e,c,s,n)}function y(){if(s)return;const t=n.hls;if(i(t?.media===l,'Cannot find instance of HLS from "art.hls"'),!d.has(t)){if(r!==t){a(),r=t;try{a=function(t,e,o){const n=t.constructor?.Events;if(!n||"function"!==typeof t.on||"function"!==typeof t.off)return()=>{};const l=[];let i=!0;function r(){if(i){i=!1;for(const[e,o]of l)t.off(e,o);l.length=0}}try{for(const r of["MANIFEST_PARSED","LEVELS_UPDATED","LEVEL_SWITCHED","AUDIO_TRACKS_UPDATED","AUDIO_TRACK_SWITCHED","DESTROYING"]){const c=n[r];if("string"!==typeof c||l.some(([t])=>t===c))continue;const s=()=>{i&&("DESTROYING"===r?o():e())};l.push([c,s]),t.on(c,s)}}catch(c){throw r(),c}return r}(t,()=>{f?c=t:p(t,!1)},()=>{d.add(t),a(),r===t&&(r=void 0,s||(m.clear(),g.clear()))})}catch(e){throw r=void 0,e}}p(t,!0)}}return n.on("ready",y),n.on("restart",y),n.on("destroy",function t(){s||(s=!0,u++,c=void 0,m.invalidate(),g.invalidate(),a(),n.off("ready",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.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t,e){const l=new Map;return t.filter(t=>{const o=t[e];return void 0===o||!l.has(o)&&l.set(o,1)})}return function(e={}){return l=>{const{$video:o}=l.template,{errorHandle:n}=l.constructor.utils;function i(){var i;n((null==(i=l.hls)?void 0:i.media)===o,'Cannot find instance of HLS from "art.hls"'),function(o){if(!o.levels.length)return;const n=e.quality||{},i=n.auto||"Auto",a=n.title||"Quality",s=n.getName||(t=>t.name||`${t.height}P`),c=o.levels[o.currentLevel],r=c?s(c):i,u=t(o.levels.map((t,e)=>({html:s(t,e),value:e,default:o.currentLevel===e})),"html").sort((t,e)=>e.value-t.value);u.push({html:i,value:-1,default:-1===o.currentLevel});const h=t=>(o.currentLevel=t.value,l.notice.show=`${a}: ${t.html}`,n.control&&l.controls.check(t),n.setting&&l.setting.check(t),t.html);n.control&&l.controls.update({name:"hls-quality",position:"right",html:r,style:{padding:"0 10px"},selector:u,onSelect:h}),n.setting&&l.setting.update({name:"hls-quality",tooltip:r,html:a,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:u,onSelect:h})}(l.hls),function(o){if(!o.audioTracks.length)return;const n=e.audio||{},i=n.auto||"Auto",a=n.title||"Audio",s=n.getName||(t=>t.name||t.lang||t.language),c=o.audioTracks[o.audioTrack],r=c?s(c):i,u=t(o.audioTracks.map((t,e)=>({html:s(t,e),value:t.id,default:o.audioTrack===t.id})),"html"),h=t=>(o.audioTrack=t.value,l.notice.show=`${a}: ${t.html}`,n.control&&l.controls.check(t),n.setting&&l.setting.check(t),t.html);n.control&&l.controls.update({name:"hls-audio",position:"right",html:r,style:{padding:"0 10px"},selector:u,onSelect:h}),n.setting&&l.setting.update({name:"hls-audio",tooltip:r,html:a,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:u,onSelect:h})}(l.hls)}return l.on("ready",i),l.on("restart",i),{name:"artplayerPluginHlsControl",update:i}}}});
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const o=e.get(t.html);return o?(t.default&&(o.default=!0,o.value=t.value),!1):(e.set(t.html,t),!0)})}function e(t,e,o,l,n,i){let r;const c={control:!1,setting:!1};function s(o){c[o]&&(c[o]=!1,t["control"===o?"controls":"setting"].remove(e))}function u(){r=void 0,s("control"),s("setting")}function a(e,o){for(const t of e.model.selector)t.default=t===o;e.config.control&&t.controls.check(o),e===r&&n(e.hls)&&e.config.setting&&t.setting.check(o)}return{update:function(f,d,h,m){if(!h)return void u();const g=r,v=h.selector.findIndex(t=>t.default);if(!m&&(null==g?void 0:g.hls)===f&&g.model.title===h.title&&(-1!==v||!g.model.selector.some(t=>t.default)&&g.model.html===h.html)&&c.control===Boolean(d.control)&&c.setting===Boolean(d.setting)&&g.model.selector.length===h.selector.length&&g.model.selector.every((t,e)=>t.html===h.selector[e].html&&t.value===h.selector[e].value)){g.config=d;const t=g.model.selector[v];return void(!t||t.default&&g.model.html===h.html||(g.model.html=h.html,a(g,t)))}const p={hls:f,config:d,model:h};r=p;const y=()=>r===p&&n(f),T=e=>i(()=>y()?(f[o]=e.value,y()?(h.html=e.html,t.notice.show=`${h.title}: ${e.html}`,y()&&a(p,e),e.html):e.html):e.html);d.control?(c.control=!0,t.controls.update({name:e,position:"right",html:h.html,style:{padding:"0 10px"},selector:h.selector,onSelect:T})):s("control"),y()&&(d.setting?(c.setting=!0,t.setting.update({name:e,tooltip:h.html,html:h.title,icon:l,width:200,selector:h.selector,onSelect:T})):s("setting"))},clear:u,invalidate:function(){r=void 0}}}return function(o={}){return l=>{const{$video:n}=l.template,{errorHandle:i}=l.constructor.utils;let r,c,s=!1,u=0,a=()=>{},f=0;const d=new WeakSet,h=t=>!s&&!d.has(t)&&l.hls===t&&t.media===n,m=e(l,"hls-quality","currentLevel",'<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>',h,v),g=e(l,"hls-audio","audioTrack",'<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>',h,v);function v(t){f++;let e=!1;try{const o=t();return e=!0,o}finally{if(f--,!f){const t=c;c=void 0,e&&t&&p(t,!1)}}}function p(e,l){if(!h(e))return;const n=++u,i=o.quality||{},r=function(e,o){if(!e.levels.length)return null;const l=o.auto||"Auto",n=o.getName||(t=>t.name||`${t.height}P`),i=function(t){return!0===t.autoLevelEnabled?-1:t.currentLevel}(e),r=e.levels[i],c=r?n(r):l,s=t(e.levels.map((t,e)=>({html:n(t,e),value:e,default:i===e}))).sort((t,e)=>e.value-t.value);return s.push({html:l,value:-1,default:-1===i}),{html:c,title:o.title||"Quality",selector:s}}(e,i);if(!h(e)||n!==u)return;if(m.update(e,i,r,l),!h(e)||n!==u)return;const c=o.audio||{},s=function(e,o){if(!e.audioTracks.length)return null;const l=o.auto||"Auto",n=o.getName||(t=>t.name||t.lang||t.language),i=e.audioTracks[e.audioTrack],r=i?n(i):l,c=t(e.audioTracks.map((t,o)=>({html:n(t,o),value:t.id,default:e.audioTrack===t.id})));return{html:r,title:o.title||"Audio",selector:c}}(e,c);h(e)&&n===u&&g.update(e,c,s,l)}function y(){if(s)return;const t=l.hls;if(i((null==t?void 0:t.media)===n,'Cannot find instance of HLS from "art.hls"'),!d.has(t)){if(r!==t){a(),r=t;try{a=function(t,e,o){var l;const n=null==(l=t.constructor)?void 0:l.Events;if(!n||"function"!==typeof t.on||"function"!==typeof t.off)return()=>{};const i=[];let r=!0;function c(){if(r){r=!1;for(const[e,o]of i)t.off(e,o);i.length=0}}try{for(const l of["MANIFEST_PARSED","LEVELS_UPDATED","LEVEL_SWITCHED","AUDIO_TRACKS_UPDATED","AUDIO_TRACK_SWITCHED","DESTROYING"]){const c=n[l];if("string"!==typeof c||i.some(([t])=>t===c))continue;const s=()=>{r&&("DESTROYING"===l?o():e())};i.push([c,s]),t.on(c,s)}}catch(s){throw c(),s}return c}(t,()=>{f?c=t:p(t,!1)},()=>{d.add(t),a(),r===t&&(r=void 0,s||(m.clear(),g.clear()))})}catch(e){throw r=void 0,e}}p(t,!0)}}return l.on("ready",y),l.on("restart",y),l.on("destroy",function t(){s||(s=!0,u++,cLine truncated
|
||||
@@ -5,137 +5,261 @@
|
||||
* 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) {
|
||||
function uniqueLabels(items) {
|
||||
const seen = /* @__PURE__ */ new Map();
|
||||
return array.filter((item) => {
|
||||
const key = item[property];
|
||||
if (key === void 0) {
|
||||
return items.filter((item) => {
|
||||
if (item.html === void 0)
|
||||
return true;
|
||||
const first = seen.get(item.html);
|
||||
if (first) {
|
||||
if (item.default) {
|
||||
first.default = true;
|
||||
first.value = item.value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return !seen.has(key) && seen.set(key, 1);
|
||||
seen.set(item.html, item);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
function qualitySelection(hls) {
|
||||
return hls.autoLevelEnabled === true ? -1 : hls.currentLevel;
|
||||
}
|
||||
function qualityModel(hls, config) {
|
||||
if (!hls.levels.length)
|
||||
return null;
|
||||
const auto = config.auto || "Auto";
|
||||
const getName = config.getName || ((level2) => level2.name || `${level2.height}P`);
|
||||
const selected = qualitySelection(hls);
|
||||
const level = hls.levels[selected];
|
||||
const html = level ? getName(level) : auto;
|
||||
const selector = uniqueLabels(hls.levels.map((item, index) => ({ html: getName(item, index), value: index, default: selected === index }))).sort((a, b) => b.value - a.value);
|
||||
selector.push({ html: auto, value: -1, default: selected === -1 });
|
||||
return { html, title: config.title || "Quality", selector };
|
||||
}
|
||||
function audioModel(hls, config) {
|
||||
if (!hls.audioTracks.length)
|
||||
return null;
|
||||
const auto = config.auto || "Auto";
|
||||
const getName = config.getName || ((track2) => track2.name || track2.lang || track2.language);
|
||||
const track = hls.audioTracks[hls.audioTrack];
|
||||
const html = track ? getName(track) : auto;
|
||||
const selector = uniqueLabels(hls.audioTracks.map((item, index) => ({ html: getName(item, index), value: item.id, default: hls.audioTrack === item.id })));
|
||||
return { html, title: config.title || "Audio", selector };
|
||||
}
|
||||
function createMenu(art, name, field, icon, active, select) {
|
||||
let current;
|
||||
const owned = { control: false, setting: false };
|
||||
function remove(surface) {
|
||||
if (!owned[surface])
|
||||
return;
|
||||
owned[surface] = false;
|
||||
art[surface === "control" ? "controls" : "setting"].remove(name);
|
||||
}
|
||||
function clear() {
|
||||
current = void 0;
|
||||
remove("control");
|
||||
remove("setting");
|
||||
}
|
||||
function check(state, item) {
|
||||
for (const entry of state.model.selector)
|
||||
entry.default = entry === item;
|
||||
if (state.config.control)
|
||||
art.controls.check(item);
|
||||
if (state !== current || !active(state.hls))
|
||||
return;
|
||||
if (state.config.setting)
|
||||
art.setting.check(item);
|
||||
}
|
||||
function update(hls, config, model, force) {
|
||||
if (!model) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
const previous = current;
|
||||
const selectedIndex = model.selector.findIndex((item) => item.default);
|
||||
const reusable = !force && previous?.hls === hls && previous.model.title === model.title && (selectedIndex !== -1 || !previous.model.selector.some((item) => item.default) && previous.model.html === model.html) && owned.control === Boolean(config.control) && owned.setting === Boolean(config.setting) && previous.model.selector.length === model.selector.length && previous.model.selector.every((item, index) => item.html === model.selector[index].html && item.value === model.selector[index].value);
|
||||
if (reusable) {
|
||||
previous.config = config;
|
||||
const target = previous.model.selector[selectedIndex];
|
||||
if (target && (!target.default || previous.model.html !== model.html)) {
|
||||
previous.model.html = model.html;
|
||||
check(previous, target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const state = { hls, config, model };
|
||||
current = state;
|
||||
const valid = () => current === state && active(hls);
|
||||
const onSelect = (item) => select(() => {
|
||||
if (!valid())
|
||||
return item.html;
|
||||
hls[field] = item.value;
|
||||
if (!valid())
|
||||
return item.html;
|
||||
model.html = item.html;
|
||||
art.notice.show = `${model.title}: ${item.html}`;
|
||||
if (valid())
|
||||
check(state, item);
|
||||
return item.html;
|
||||
});
|
||||
if (config.control) {
|
||||
owned.control = true;
|
||||
art.controls.update({ name, position: "right", html: model.html, style: { padding: "0 10px" }, selector: model.selector, onSelect });
|
||||
} else {
|
||||
remove("control");
|
||||
}
|
||||
if (!valid())
|
||||
return;
|
||||
if (config.setting) {
|
||||
owned.setting = true;
|
||||
art.setting.update({ name, tooltip: model.html, html: model.title, icon, width: 200, selector: model.selector, onSelect });
|
||||
} else {
|
||||
remove("setting");
|
||||
}
|
||||
}
|
||||
function invalidate() {
|
||||
current = void 0;
|
||||
}
|
||||
return { update, clear, invalidate };
|
||||
}
|
||||
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 subscribeHls(hls, refresh, destroyed) {
|
||||
const events = hls.constructor?.Events;
|
||||
if (!events || typeof hls.on !== "function" || typeof hls.off !== "function")
|
||||
return () => {
|
||||
};
|
||||
const subscriptions = [];
|
||||
let active = true;
|
||||
function release() {
|
||||
if (!active)
|
||||
return;
|
||||
active = false;
|
||||
for (const [event, callback] of subscriptions)
|
||||
hls.off(event, callback);
|
||||
subscriptions.length = 0;
|
||||
}
|
||||
try {
|
||||
for (const key of ["MANIFEST_PARSED", "LEVELS_UPDATED", "LEVEL_SWITCHED", "AUDIO_TRACKS_UPDATED", "AUDIO_TRACK_SWITCHED", "DESTROYING"]) {
|
||||
const event = events[key];
|
||||
if (typeof event !== "string" || subscriptions.some(([name]) => name === event))
|
||||
continue;
|
||||
const callback = () => {
|
||||
if (!active)
|
||||
return;
|
||||
if (key === "DESTROYING")
|
||||
destroyed();
|
||||
else
|
||||
refresh();
|
||||
};
|
||||
subscriptions.push([event, callback]);
|
||||
hls.on(event, callback);
|
||||
}
|
||||
} catch (error) {
|
||||
release();
|
||||
throw error;
|
||||
}
|
||||
return release;
|
||||
}
|
||||
function artplayerPluginHlsControl(option = {}) {
|
||||
return (art) => {
|
||||
const { $video } = art.template;
|
||||
const { errorHandle } = art.constructor.utils;
|
||||
function updateQuality(hls) {
|
||||
if (!hls.levels.length)
|
||||
return;
|
||||
const config = option.quality || {};
|
||||
const auto = config.auto || "Auto";
|
||||
const title = config.title || "Quality";
|
||||
const getName = config.getName || ((level) => level.name || `${level.height}P`);
|
||||
const defaultLevel = hls.levels[hls.currentLevel];
|
||||
const defaultHtml = defaultLevel ? getName(defaultLevel) : auto;
|
||||
const selector = uniqBy(
|
||||
hls.levels.map((item, index) => {
|
||||
return {
|
||||
html: getName(item, index),
|
||||
value: index,
|
||||
default: hls.currentLevel === index
|
||||
};
|
||||
}),
|
||||
"html"
|
||||
).sort((a, b) => b.value - a.value);
|
||||
selector.push({
|
||||
html: auto,
|
||||
value: -1,
|
||||
default: hls.currentLevel === -1
|
||||
});
|
||||
const onSelect = (item) => {
|
||||
hls.currentLevel = 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: "hls-quality",
|
||||
position: "right",
|
||||
html: defaultHtml,
|
||||
style: { padding: "0 10px" },
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: "hls-quality",
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $quality,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
let closed = false;
|
||||
let revision = 0;
|
||||
let engine;
|
||||
let unsubscribe = () => {
|
||||
};
|
||||
let selecting = 0;
|
||||
let pending;
|
||||
const retired = /* @__PURE__ */ new WeakSet();
|
||||
const active = (hls) => !closed && !retired.has(hls) && art.hls === hls && hls.media === $video;
|
||||
const quality = createMenu(art, "hls-quality", "currentLevel", $quality, active, select);
|
||||
const audio = createMenu(art, "hls-audio", "audioTrack", $audio, active, select);
|
||||
function select(callback) {
|
||||
selecting++;
|
||||
let succeeded = false;
|
||||
try {
|
||||
const result = callback();
|
||||
succeeded = true;
|
||||
return result;
|
||||
} finally {
|
||||
selecting--;
|
||||
if (!selecting) {
|
||||
const hls = pending;
|
||||
pending = void 0;
|
||||
if (succeeded && hls)
|
||||
refresh(hls, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
function updateAudio(hls) {
|
||||
if (!hls.audioTracks.length)
|
||||
function refresh(hls, force) {
|
||||
if (!active(hls))
|
||||
return;
|
||||
const config = option.audio || {};
|
||||
const auto = config.auto || "Auto";
|
||||
const title = config.title || "Audio";
|
||||
const getName = config.getName || ((track) => track.name || track.lang || track.language);
|
||||
const defaultTrack = hls.audioTracks[hls.audioTrack];
|
||||
const defaultHtml = defaultTrack ? getName(defaultTrack) : auto;
|
||||
const selector = uniqBy(
|
||||
hls.audioTracks.map((item, index) => {
|
||||
return {
|
||||
html: getName(item, index),
|
||||
value: item.id,
|
||||
default: hls.audioTrack === item.id
|
||||
};
|
||||
}),
|
||||
"html"
|
||||
);
|
||||
const onSelect = (item) => {
|
||||
hls.audioTrack = 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: "hls-audio",
|
||||
position: "right",
|
||||
html: defaultHtml,
|
||||
style: { padding: "0 10px" },
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: "hls-audio",
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $audio,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
const version = ++revision;
|
||||
const config = option.quality || {};
|
||||
const model = qualityModel(hls, config);
|
||||
if (!active(hls) || version !== revision)
|
||||
return;
|
||||
quality.update(hls, config, model, force);
|
||||
if (!active(hls) || version !== revision)
|
||||
return;
|
||||
const audioConfig = option.audio || {};
|
||||
const audioView = audioModel(hls, audioConfig);
|
||||
if (active(hls) && version === revision)
|
||||
audio.update(hls, audioConfig, audioView, force);
|
||||
}
|
||||
function update() {
|
||||
errorHandle(art.hls?.media === $video, 'Cannot find instance of HLS from "art.hls"');
|
||||
updateQuality(art.hls);
|
||||
updateAudio(art.hls);
|
||||
if (closed)
|
||||
return;
|
||||
const hls = art.hls;
|
||||
errorHandle(hls?.media === $video, 'Cannot find instance of HLS from "art.hls"');
|
||||
if (retired.has(hls))
|
||||
return;
|
||||
if (engine !== hls) {
|
||||
unsubscribe();
|
||||
engine = hls;
|
||||
try {
|
||||
unsubscribe = subscribeHls(hls, () => {
|
||||
if (selecting)
|
||||
pending = hls;
|
||||
else
|
||||
refresh(hls, false);
|
||||
}, () => {
|
||||
retired.add(hls);
|
||||
unsubscribe();
|
||||
if (engine === hls) {
|
||||
engine = void 0;
|
||||
if (!closed) {
|
||||
quality.clear();
|
||||
audio.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
engine = void 0;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
refresh(hls, true);
|
||||
}
|
||||
function destroy() {
|
||||
if (closed)
|
||||
return;
|
||||
closed = true;
|
||||
revision++;
|
||||
pending = void 0;
|
||||
quality.invalidate();
|
||||
audio.invalidate();
|
||||
unsubscribe();
|
||||
art.off("ready", update);
|
||||
art.off("restart", update);
|
||||
art.off("destroy", destroy);
|
||||
}
|
||||
art.on("ready", update);
|
||||
art.on("restart", update);
|
||||
return {
|
||||
name: "artplayerPluginHlsControl",
|
||||
update
|
||||
};
|
||||
art.on("destroy", destroy);
|
||||
return { name: "artplayerPluginHlsControl", update };
|
||||
};
|
||||
}
|
||||
export {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# HLS control maintenance
|
||||
|
||||
The public entry remains `src/index.js`: `artplayerPluginHlsControl(option = {})` returns a
|
||||
synchronous ArtPlayer plugin with `{ name: 'artplayerPluginHlsControl', update }`. `update()`
|
||||
returns undefined. The consumer creates, attaches and destroys `art.hls`; this plugin does not
|
||||
import Hls.js or own its media engine. Public declarations are still in `types/` pending the
|
||||
separate TypeScript migration (PKG-HLS-04).
|
||||
|
||||
| Module | Responsibility |
|
||||
| --- | --- |
|
||||
| `src/index.js` | Core ready/restart/destroy hooks, current engine identity, refresh generation, selection event barrier |
|
||||
| `src/mapping.js` | Quality/audio labels, duplicate groups, selected item and Auto policy; no ArtPlayer/DOM dependencies |
|
||||
| `src/menu.js` | Existing controls/setting update/remove/check APIs, current callback ownership and stable menu reuse |
|
||||
| `src/sdk-events.js` | Optional SDK event capabilities, six subscriptions, setup rollback and listener release |
|
||||
|
||||
The entry depends on these three modules; they do not import the entry or each other.
|
||||
SVG icons remain local build inputs. There is no cross-instance cache, timer, worker or fetch.
|
||||
|
||||
## Refresh and selection
|
||||
|
||||
Registration does not require an Hls instance yet. The first ready/restart/manual update checks
|
||||
`art.hls.media === art.template.$video` and binds SDK events. A plain compatible host without
|
||||
`constructor.Events`, `on` and `off` retains manual/core-event updates. An engine replacement
|
||||
releases only the plugin's own subscriptions. Core destroy detaches hooks and makes retained
|
||||
updates/selectors inert; core remains responsible for DOM removal or preservation.
|
||||
|
||||
Supported SDK hooks are MANIFEST_PARSED, LEVELS_UPDATED, LEVEL_SWITCHED, AUDIO_TRACKS_UPDATED,
|
||||
AUDIO_TRACK_SWITCHED and DESTROYING, using the SDK's actual exported event names. DESTROYING
|
||||
retires that engine and removes owned menus; a subsequent callback cannot revive it.
|
||||
If subscription setup throws, installed listeners are removed and a later update can retry.
|
||||
|
||||
Explicit update rebuilds selectors using current options. SDK events reuse current selectors
|
||||
when engine, display surfaces, title, labels and values are unchanged, updating selection through
|
||||
the normal core check APIs. An unavailable selection clears obsolete defaults/labels. Empty lists
|
||||
or disabled display surfaces remove their respective entries, and later updates can restore them.
|
||||
|
||||
Selection writes the SDK property, then notice, then controls.check and setting.check, returning
|
||||
the selected HTML. SDK events emitted synchronously during the write are deferred until this
|
||||
operation finishes. A formatter or core callback that replaces the engine, destroys the player or
|
||||
starts a newer refresh cannot commit the older refresh. Formatting/SDK write failures remain
|
||||
synchronous; selection does not introduce a Promise. The SDK's own event exception handling still
|
||||
applies to automatic event-driven formatting.
|
||||
|
||||
## Compatibility and intentional corrections
|
||||
|
||||
- Keep names `hls-quality`/`hls-audio`, right-side controls, existing SVGs, settings width 200,
|
||||
title/tooltip defaults, factory/global name and root/legacy distribution paths.
|
||||
- Pass original SDK objects to getName: selected-label calls have one argument; list-label calls
|
||||
also receive the index. Do not bind a new `this` or claim the index is always present.
|
||||
- Auto uses `autoLevelEnabled === true`; hosts without this capability fall back to currentLevel.
|
||||
Quality's Auto value remains -1. Audio does not acquire a new synthetic Auto option.
|
||||
- Duplicate labels still collapse into one group. If a later duplicate is selected, its value
|
||||
represents that group so the real selection is not lost. Quality sorts by represented index;
|
||||
audio retains group order. Undefined labels remain distinct.
|
||||
- Old SDK callbacks after replacement/destroy, lingering empty menus and an Auto label derived
|
||||
from the currently decoded automatic level were defects, not guarantees to preserve.
|
||||
|
||||
The plugin owns its two reserved menu names. Applications should use distinct names for unrelated
|
||||
entries. Hls instances and third-party SDK listeners remain owned by their callers. No minimum
|
||||
core/SDK version has been newly imposed; the tested historical core is 5.4.0 and SDK is 1.5.17.
|
||||
|
||||
## Validation and next changes
|
||||
|
||||
Use the repository's pinned Node and Yarn:
|
||||
|
||||
```sh
|
||||
node --test test/hls-control.test.js
|
||||
yarn test:browser hls-control.spec.js
|
||||
yarn build artplayer-plugin-hls-control
|
||||
yarn ci:check
|
||||
```
|
||||
|
||||
ARTPLAYER_TEST_HLS accepts platform-delimited paths to built main/legacy/ESM files for additional
|
||||
Node contracts. ARTPLAYER_HLS_ARTIFACT selects a built browser main/legacy file; invalid paths fail,
|
||||
and test attachments record its bytes. These checks do not replace isolated tarball installation.
|
||||
Use the local demo at `http://localhost:8082` with example `hls.control` during example work.
|
||||
|
||||
Windows Playwright WebKit lacks MSE; explicit capability/error cleanup runs there, while playback
|
||||
cases are marked skipped. Safari/native HLS, SDK workers, grouped track changes, SDK version range,
|
||||
updated example and isolated package verification remain PKG-HLS-05/06 work. See
|
||||
[refactor validation](../../refactor/hls-validation.md). Keep these gaps visible in release reviews.
|
||||
@@ -1,5 +1,7 @@
|
||||
# artplayer-plugin-hls-control
|
||||
|
||||
Implementation boundaries and regression commands are documented in [ARCHITECTURE.md](ARCHITECTURE.md).
|
||||
|
||||
Hls control plugin for ArtPlayer
|
||||
|
||||
## Demo
|
||||
|
||||
@@ -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.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t,e){const l=new Map;return t.filter(t=>{const o=t[e];return void 0===o||!l.has(o)&&l.set(o,1)})}return function(e={}){return l=>{const{$video:o}=l.template,{errorHandle:n}=l.constructor.utils;function i(){n(l.hls?.media===o,'Cannot find instance of HLS from "art.hls"'),function(o){if(!o.levels.length)return;const n=e.quality||{},i=n.auto||"Auto",s=n.title||"Quality",a=n.getName||(t=>t.name||`${t.height}P`),c=o.levels[o.currentLevel],r=c?a(c):i,u=t(o.levels.map((t,e)=>({html:a(t,e),value:e,default:o.currentLevel===e})),"html").sort((t,e)=>e.value-t.value);u.push({html:i,value:-1,default:-1===o.currentLevel});const h=t=>(o.currentLevel=t.value,l.notice.show=`${s}: ${t.html}`,n.control&&l.controls.check(t),n.setting&&l.setting.check(t),t.html);n.control&&l.controls.update({name:"hls-quality",position:"right",html:r,style:{padding:"0 10px"},selector:u,onSelect:h}),n.setting&&l.setting.update({name:"hls-quality",tooltip:r,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:u,onSelect:h})}(l.hls),function(o){if(!o.audioTracks.length)return;const n=e.audio||{},i=n.auto||"Auto",s=n.title||"Audio",a=n.getName||(t=>t.name||t.lang||t.language),c=o.audioTracks[o.audioTrack],r=c?a(c):i,u=t(o.audioTracks.map((t,e)=>({html:a(t,e),value:t.id,default:o.audioTrack===t.id})),"html"),h=t=>(o.audioTrack=t.value,l.notice.show=`${s}: ${t.html}`,n.control&&l.controls.check(t),n.setting&&l.setting.check(t),t.html);n.control&&l.controls.update({name:"hls-audio",position:"right",html:r,style:{padding:"0 10px"},selector:u,onSelect:h}),n.setting&&l.setting.update({name:"hls-audio",tooltip:r,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:u,onSelect:h})}(l.hls)}return l.on("ready",i),l.on("restart",i),{name:"artplayerPluginHlsControl",update:i}}}});
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const o=e.get(t.html);return o?(t.default&&(o.default=!0,o.value=t.value),!1):(e.set(t.html,t),!0)})}function e(t,e,o,n,l,i){let r;const c={control:!1,setting:!1};function s(o){c[o]&&(c[o]=!1,t["control"===o?"controls":"setting"].remove(e))}function u(){r=void 0,s("control"),s("setting")}function a(e,o){for(const t of e.model.selector)t.default=t===o;e.config.control&&t.controls.check(o),e===r&&l(e.hls)&&e.config.setting&&t.setting.check(o)}return{update:function(f,d,h,m){if(!h)return void u();const g=r,v=h.selector.findIndex(t=>t.default);if(!m&&g?.hls===f&&g.model.title===h.title&&(-1!==v||!g.model.selector.some(t=>t.default)&&g.model.html===h.html)&&c.control===Boolean(d.control)&&c.setting===Boolean(d.setting)&&g.model.selector.length===h.selector.length&&g.model.selector.every((t,e)=>t.html===h.selector[e].html&&t.value===h.selector[e].value)){g.config=d;const t=g.model.selector[v];return void(!t||t.default&&g.model.html===h.html||(g.model.html=h.html,a(g,t)))}const p={hls:f,config:d,model:h};r=p;const y=()=>r===p&&l(f),T=e=>i(()=>y()?(f[o]=e.value,y()?(h.html=e.html,t.notice.show=`${h.title}: ${e.html}`,y()&&a(p,e),e.html):e.html):e.html);d.control?(c.control=!0,t.controls.update({name:e,position:"right",html:h.html,style:{padding:"0 10px"},selector:h.selector,onSelect:T})):s("control"),y()&&(d.setting?(c.setting=!0,t.setting.update({name:e,tooltip:h.html,html:h.title,icon:n,width:200,selector:h.selector,onSelect:T})):s("setting"))},clear:u,invalidate:function(){r=void 0}}}return function(o={}){return n=>{const{$video:l}=n.template,{errorHandle:i}=n.constructor.utils;let r,c,s=!1,u=0,a=()=>{},f=0;const d=new WeakSet,h=t=>!s&&!d.has(t)&&n.hls===t&&t.media===l,m=e(n,"hls-quality","currentLevel",'<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>',h,v),g=e(n,"hls-audio","audioTrack",'<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>',h,v);function v(t){f++;let e=!1;try{const o=t();return e=!0,o}finally{if(f--,!f){const t=c;c=void 0,e&&t&&p(t,!1)}}}function p(e,n){if(!h(e))return;const l=++u,i=o.quality||{},r=function(e,o){if(!e.levels.length)return null;const n=o.auto||"Auto",l=o.getName||(t=>t.name||`${t.height}P`),i=function(t){return!0===t.autoLevelEnabled?-1:t.currentLevel}(e),r=e.levels[i],c=r?l(r):n,s=t(e.levels.map((t,e)=>({html:l(t,e),value:e,default:i===e}))).sort((t,e)=>e.value-t.value);return s.push({html:n,value:-1,default:-1===i}),{html:c,title:o.title||"Quality",selector:s}}(e,i);if(!h(e)||l!==u)return;if(m.update(e,i,r,n),!h(e)||l!==u)return;const c=o.audio||{},s=function(e,o){if(!e.audioTracks.length)return null;const n=o.auto||"Auto",l=o.getName||(t=>t.name||t.lang||t.language),i=e.audioTracks[e.audioTrack],r=i?l(i):n,c=t(e.audioTracks.map((t,o)=>({html:l(t,o),value:t.id,default:e.audioTrack===t.id})));return{html:r,title:o.title||"Audio",selector:c}}(e,c);h(e)&&l===u&&g.update(e,c,s,n)}function y(){if(s)return;const t=n.hls;if(i(t?.media===l,'Cannot find instance of HLS from "art.hls"'),!d.has(t)){if(r!==t){a(),r=t;try{a=function(t,e,o){const n=t.constructor?.Events;if(!n||"function"!==typeof t.on||"function"!==typeof t.off)return()=>{};const l=[];let i=!0;function r(){if(i){i=!1;for(const[e,o]of l)t.off(e,o);l.length=0}}try{for(const r of["MANIFEST_PARSED","LEVELS_UPDATED","LEVEL_SWITCHED","AUDIO_TRACKS_UPDATED","AUDIO_TRACK_SWITCHED","DESTROYING"]){const c=n[r];if("string"!==typeof c||l.some(([t])=>t===c))continue;const s=()=>{i&&("DESTROYING"===r?o():e())};l.push([c,s]),t.on(c,s)}}catch(c){throw r(),c}return r}(t,()=>{f?c=t:p(t,!1)},()=>{d.add(t),a(),r===t&&(r=void 0,s||(m.clear(),g.clear()))})}catch(e){throw r=void 0,e}}p(t,!0)}}return n.on("ready",y),n.on("restart",y),n.on("destroy",function t(){s||(s=!0,u++,c=void 0,m.invalidate(),g.invalidate(),a(),n.off("ready",Line 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.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t,e){const l=new Map;return t.filter(t=>{const o=t[e];return void 0===o||!l.has(o)&&l.set(o,1)})}return function(e={}){return l=>{const{$video:o}=l.template,{errorHandle:n}=l.constructor.utils;function i(){var i;n((null==(i=l.hls)?void 0:i.media)===o,'Cannot find instance of HLS from "art.hls"'),function(o){if(!o.levels.length)return;const n=e.quality||{},i=n.auto||"Auto",a=n.title||"Quality",s=n.getName||(t=>t.name||`${t.height}P`),c=o.levels[o.currentLevel],r=c?s(c):i,u=t(o.levels.map((t,e)=>({html:s(t,e),value:e,default:o.currentLevel===e})),"html").sort((t,e)=>e.value-t.value);u.push({html:i,value:-1,default:-1===o.currentLevel});const h=t=>(o.currentLevel=t.value,l.notice.show=`${a}: ${t.html}`,n.control&&l.controls.check(t),n.setting&&l.setting.check(t),t.html);n.control&&l.controls.update({name:"hls-quality",position:"right",html:r,style:{padding:"0 10px"},selector:u,onSelect:h}),n.setting&&l.setting.update({name:"hls-quality",tooltip:r,html:a,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:u,onSelect:h})}(l.hls),function(o){if(!o.audioTracks.length)return;const n=e.audio||{},i=n.auto||"Auto",a=n.title||"Audio",s=n.getName||(t=>t.name||t.lang||t.language),c=o.audioTracks[o.audioTrack],r=c?s(c):i,u=t(o.audioTracks.map((t,e)=>({html:s(t,e),value:t.id,default:o.audioTrack===t.id})),"html"),h=t=>(o.audioTrack=t.value,l.notice.show=`${a}: ${t.html}`,n.control&&l.controls.check(t),n.setting&&l.setting.check(t),t.html);n.control&&l.controls.update({name:"hls-audio",position:"right",html:r,style:{padding:"0 10px"},selector:u,onSelect:h}),n.setting&&l.setting.update({name:"hls-audio",tooltip:r,html:a,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:u,onSelect:h})}(l.hls)}return l.on("ready",i),l.on("restart",i),{name:"artplayerPluginHlsControl",update:i}}}});
|
||||
!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginHlsControl=e(),define(function(){return t.artplayerPluginHlsControl})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginHlsControl=e()}(this,function(){"use strict";function t(t){const e=new Map;return t.filter(t=>{if(void 0===t.html)return!0;const o=e.get(t.html);return o?(t.default&&(o.default=!0,o.value=t.value),!1):(e.set(t.html,t),!0)})}function e(t,e,o,l,n,i){let r;const c={control:!1,setting:!1};function s(o){c[o]&&(c[o]=!1,t["control"===o?"controls":"setting"].remove(e))}function u(){r=void 0,s("control"),s("setting")}function a(e,o){for(const t of e.model.selector)t.default=t===o;e.config.control&&t.controls.check(o),e===r&&n(e.hls)&&e.config.setting&&t.setting.check(o)}return{update:function(f,d,h,m){if(!h)return void u();const g=r,v=h.selector.findIndex(t=>t.default);if(!m&&(null==g?void 0:g.hls)===f&&g.model.title===h.title&&(-1!==v||!g.model.selector.some(t=>t.default)&&g.model.html===h.html)&&c.control===Boolean(d.control)&&c.setting===Boolean(d.setting)&&g.model.selector.length===h.selector.length&&g.model.selector.every((t,e)=>t.html===h.selector[e].html&&t.value===h.selector[e].value)){g.config=d;const t=g.model.selector[v];return void(!t||t.default&&g.model.html===h.html||(g.model.html=h.html,a(g,t)))}const p={hls:f,config:d,model:h};r=p;const y=()=>r===p&&n(f),T=e=>i(()=>y()?(f[o]=e.value,y()?(h.html=e.html,t.notice.show=`${h.title}: ${e.html}`,y()&&a(p,e),e.html):e.html):e.html);d.control?(c.control=!0,t.controls.update({name:e,position:"right",html:h.html,style:{padding:"0 10px"},selector:h.selector,onSelect:T})):s("control"),y()&&(d.setting?(c.setting=!0,t.setting.update({name:e,tooltip:h.html,html:h.title,icon:l,width:200,selector:h.selector,onSelect:T})):s("setting"))},clear:u,invalidate:function(){r=void 0}}}return function(o={}){return l=>{const{$video:n}=l.template,{errorHandle:i}=l.constructor.utils;let r,c,s=!1,u=0,a=()=>{},f=0;const d=new WeakSet,h=t=>!s&&!d.has(t)&&l.hls===t&&t.media===n,m=e(l,"hls-quality","currentLevel",'<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>',h,v),g=e(l,"hls-audio","audioTrack",'<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>',h,v);function v(t){f++;let e=!1;try{const o=t();return e=!0,o}finally{if(f--,!f){const t=c;c=void 0,e&&t&&p(t,!1)}}}function p(e,l){if(!h(e))return;const n=++u,i=o.quality||{},r=function(e,o){if(!e.levels.length)return null;const l=o.auto||"Auto",n=o.getName||(t=>t.name||`${t.height}P`),i=function(t){return!0===t.autoLevelEnabled?-1:t.currentLevel}(e),r=e.levels[i],c=r?n(r):l,s=t(e.levels.map((t,e)=>({html:n(t,e),value:e,default:i===e}))).sort((t,e)=>e.value-t.value);return s.push({html:l,value:-1,default:-1===i}),{html:c,title:o.title||"Quality",selector:s}}(e,i);if(!h(e)||n!==u)return;if(m.update(e,i,r,l),!h(e)||n!==u)return;const c=o.audio||{},s=function(e,o){if(!e.audioTracks.length)return null;const l=o.auto||"Auto",n=o.getName||(t=>t.name||t.lang||t.language),i=e.audioTracks[e.audioTrack],r=i?n(i):l,c=t(e.audioTracks.map((t,o)=>({html:n(t,o),value:t.id,default:e.audioTrack===t.id})));return{html:r,title:o.title||"Audio",selector:c}}(e,c);h(e)&&n===u&&g.update(e,c,s,l)}function y(){if(s)return;const t=l.hls;if(i((null==t?void 0:t.media)===n,'Cannot find instance of HLS from "art.hls"'),!d.has(t)){if(r!==t){a(),r=t;try{a=function(t,e,o){var l;const n=null==(l=t.constructor)?void 0:l.Events;if(!n||"function"!==typeof t.on||"function"!==typeof t.off)return()=>{};const i=[];let r=!0;function c(){if(r){r=!1;for(const[e,o]of i)t.off(e,o);i.length=0}}try{for(const l of["MANIFEST_PARSED","LEVELS_UPDATED","LEVEL_SWITCHED","AUDIO_TRACKS_UPDATED","AUDIO_TRACK_SWITCHED","DESTROYING"]){const c=n[l];if("string"!==typeof c||i.some(([t])=>t===c))continue;const s=()=>{r&&("DESTROYING"===l?o():e())};i.push([c,s]),t.on(c,s)}}catch(s){throw c(),s}return c}(t,()=>{f?c=t:p(t,!1)},()=>{d.add(t),a(),r===t&&(r=void 0,s||(m.clear(),g.clear()))})}catch(e){throw r=void 0,e}}p(t,!0)}}return l.on("ready",y),l.on("restart",y),l.on("destroy",function t(){s||(s=!0,u++,cLine truncated
|
||||
+238
-114
@@ -5,137 +5,261 @@
|
||||
* 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) {
|
||||
function uniqueLabels(items) {
|
||||
const seen = /* @__PURE__ */ new Map();
|
||||
return array.filter((item) => {
|
||||
const key = item[property];
|
||||
if (key === void 0) {
|
||||
return items.filter((item) => {
|
||||
if (item.html === void 0)
|
||||
return true;
|
||||
const first = seen.get(item.html);
|
||||
if (first) {
|
||||
if (item.default) {
|
||||
first.default = true;
|
||||
first.value = item.value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return !seen.has(key) && seen.set(key, 1);
|
||||
seen.set(item.html, item);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
function qualitySelection(hls) {
|
||||
return hls.autoLevelEnabled === true ? -1 : hls.currentLevel;
|
||||
}
|
||||
function qualityModel(hls, config) {
|
||||
if (!hls.levels.length)
|
||||
return null;
|
||||
const auto = config.auto || "Auto";
|
||||
const getName = config.getName || ((level2) => level2.name || `${level2.height}P`);
|
||||
const selected = qualitySelection(hls);
|
||||
const level = hls.levels[selected];
|
||||
const html = level ? getName(level) : auto;
|
||||
const selector = uniqueLabels(hls.levels.map((item, index) => ({ html: getName(item, index), value: index, default: selected === index }))).sort((a, b) => b.value - a.value);
|
||||
selector.push({ html: auto, value: -1, default: selected === -1 });
|
||||
return { html, title: config.title || "Quality", selector };
|
||||
}
|
||||
function audioModel(hls, config) {
|
||||
if (!hls.audioTracks.length)
|
||||
return null;
|
||||
const auto = config.auto || "Auto";
|
||||
const getName = config.getName || ((track2) => track2.name || track2.lang || track2.language);
|
||||
const track = hls.audioTracks[hls.audioTrack];
|
||||
const html = track ? getName(track) : auto;
|
||||
const selector = uniqueLabels(hls.audioTracks.map((item, index) => ({ html: getName(item, index), value: item.id, default: hls.audioTrack === item.id })));
|
||||
return { html, title: config.title || "Audio", selector };
|
||||
}
|
||||
function createMenu(art, name, field, icon, active, select) {
|
||||
let current;
|
||||
const owned = { control: false, setting: false };
|
||||
function remove(surface) {
|
||||
if (!owned[surface])
|
||||
return;
|
||||
owned[surface] = false;
|
||||
art[surface === "control" ? "controls" : "setting"].remove(name);
|
||||
}
|
||||
function clear() {
|
||||
current = void 0;
|
||||
remove("control");
|
||||
remove("setting");
|
||||
}
|
||||
function check(state, item) {
|
||||
for (const entry of state.model.selector)
|
||||
entry.default = entry === item;
|
||||
if (state.config.control)
|
||||
art.controls.check(item);
|
||||
if (state !== current || !active(state.hls))
|
||||
return;
|
||||
if (state.config.setting)
|
||||
art.setting.check(item);
|
||||
}
|
||||
function update(hls, config, model, force) {
|
||||
if (!model) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
const previous = current;
|
||||
const selectedIndex = model.selector.findIndex((item) => item.default);
|
||||
const reusable = !force && previous?.hls === hls && previous.model.title === model.title && (selectedIndex !== -1 || !previous.model.selector.some((item) => item.default) && previous.model.html === model.html) && owned.control === Boolean(config.control) && owned.setting === Boolean(config.setting) && previous.model.selector.length === model.selector.length && previous.model.selector.every((item, index) => item.html === model.selector[index].html && item.value === model.selector[index].value);
|
||||
if (reusable) {
|
||||
previous.config = config;
|
||||
const target = previous.model.selector[selectedIndex];
|
||||
if (target && (!target.default || previous.model.html !== model.html)) {
|
||||
previous.model.html = model.html;
|
||||
check(previous, target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const state = { hls, config, model };
|
||||
current = state;
|
||||
const valid = () => current === state && active(hls);
|
||||
const onSelect = (item) => select(() => {
|
||||
if (!valid())
|
||||
return item.html;
|
||||
hls[field] = item.value;
|
||||
if (!valid())
|
||||
return item.html;
|
||||
model.html = item.html;
|
||||
art.notice.show = `${model.title}: ${item.html}`;
|
||||
if (valid())
|
||||
check(state, item);
|
||||
return item.html;
|
||||
});
|
||||
if (config.control) {
|
||||
owned.control = true;
|
||||
art.controls.update({ name, position: "right", html: model.html, style: { padding: "0 10px" }, selector: model.selector, onSelect });
|
||||
} else {
|
||||
remove("control");
|
||||
}
|
||||
if (!valid())
|
||||
return;
|
||||
if (config.setting) {
|
||||
owned.setting = true;
|
||||
art.setting.update({ name, tooltip: model.html, html: model.title, icon, width: 200, selector: model.selector, onSelect });
|
||||
} else {
|
||||
remove("setting");
|
||||
}
|
||||
}
|
||||
function invalidate() {
|
||||
current = void 0;
|
||||
}
|
||||
return { update, clear, invalidate };
|
||||
}
|
||||
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 subscribeHls(hls, refresh, destroyed) {
|
||||
const events = hls.constructor?.Events;
|
||||
if (!events || typeof hls.on !== "function" || typeof hls.off !== "function")
|
||||
return () => {
|
||||
};
|
||||
const subscriptions = [];
|
||||
let active = true;
|
||||
function release() {
|
||||
if (!active)
|
||||
return;
|
||||
active = false;
|
||||
for (const [event, callback] of subscriptions)
|
||||
hls.off(event, callback);
|
||||
subscriptions.length = 0;
|
||||
}
|
||||
try {
|
||||
for (const key of ["MANIFEST_PARSED", "LEVELS_UPDATED", "LEVEL_SWITCHED", "AUDIO_TRACKS_UPDATED", "AUDIO_TRACK_SWITCHED", "DESTROYING"]) {
|
||||
const event = events[key];
|
||||
if (typeof event !== "string" || subscriptions.some(([name]) => name === event))
|
||||
continue;
|
||||
const callback = () => {
|
||||
if (!active)
|
||||
return;
|
||||
if (key === "DESTROYING")
|
||||
destroyed();
|
||||
else
|
||||
refresh();
|
||||
};
|
||||
subscriptions.push([event, callback]);
|
||||
hls.on(event, callback);
|
||||
}
|
||||
} catch (error) {
|
||||
release();
|
||||
throw error;
|
||||
}
|
||||
return release;
|
||||
}
|
||||
function artplayerPluginHlsControl(option = {}) {
|
||||
return (art) => {
|
||||
const { $video } = art.template;
|
||||
const { errorHandle } = art.constructor.utils;
|
||||
function updateQuality(hls) {
|
||||
if (!hls.levels.length)
|
||||
return;
|
||||
const config = option.quality || {};
|
||||
const auto = config.auto || "Auto";
|
||||
const title = config.title || "Quality";
|
||||
const getName = config.getName || ((level) => level.name || `${level.height}P`);
|
||||
const defaultLevel = hls.levels[hls.currentLevel];
|
||||
const defaultHtml = defaultLevel ? getName(defaultLevel) : auto;
|
||||
const selector = uniqBy(
|
||||
hls.levels.map((item, index) => {
|
||||
return {
|
||||
html: getName(item, index),
|
||||
value: index,
|
||||
default: hls.currentLevel === index
|
||||
};
|
||||
}),
|
||||
"html"
|
||||
).sort((a, b) => b.value - a.value);
|
||||
selector.push({
|
||||
html: auto,
|
||||
value: -1,
|
||||
default: hls.currentLevel === -1
|
||||
});
|
||||
const onSelect = (item) => {
|
||||
hls.currentLevel = 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: "hls-quality",
|
||||
position: "right",
|
||||
html: defaultHtml,
|
||||
style: { padding: "0 10px" },
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: "hls-quality",
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $quality,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
let closed = false;
|
||||
let revision = 0;
|
||||
let engine;
|
||||
let unsubscribe = () => {
|
||||
};
|
||||
let selecting = 0;
|
||||
let pending;
|
||||
const retired = /* @__PURE__ */ new WeakSet();
|
||||
const active = (hls) => !closed && !retired.has(hls) && art.hls === hls && hls.media === $video;
|
||||
const quality = createMenu(art, "hls-quality", "currentLevel", $quality, active, select);
|
||||
const audio = createMenu(art, "hls-audio", "audioTrack", $audio, active, select);
|
||||
function select(callback) {
|
||||
selecting++;
|
||||
let succeeded = false;
|
||||
try {
|
||||
const result = callback();
|
||||
succeeded = true;
|
||||
return result;
|
||||
} finally {
|
||||
selecting--;
|
||||
if (!selecting) {
|
||||
const hls = pending;
|
||||
pending = void 0;
|
||||
if (succeeded && hls)
|
||||
refresh(hls, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
function updateAudio(hls) {
|
||||
if (!hls.audioTracks.length)
|
||||
function refresh(hls, force) {
|
||||
if (!active(hls))
|
||||
return;
|
||||
const config = option.audio || {};
|
||||
const auto = config.auto || "Auto";
|
||||
const title = config.title || "Audio";
|
||||
const getName = config.getName || ((track) => track.name || track.lang || track.language);
|
||||
const defaultTrack = hls.audioTracks[hls.audioTrack];
|
||||
const defaultHtml = defaultTrack ? getName(defaultTrack) : auto;
|
||||
const selector = uniqBy(
|
||||
hls.audioTracks.map((item, index) => {
|
||||
return {
|
||||
html: getName(item, index),
|
||||
value: item.id,
|
||||
default: hls.audioTrack === item.id
|
||||
};
|
||||
}),
|
||||
"html"
|
||||
);
|
||||
const onSelect = (item) => {
|
||||
hls.audioTrack = 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: "hls-audio",
|
||||
position: "right",
|
||||
html: defaultHtml,
|
||||
style: { padding: "0 10px" },
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: "hls-audio",
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $audio,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
const version = ++revision;
|
||||
const config = option.quality || {};
|
||||
const model = qualityModel(hls, config);
|
||||
if (!active(hls) || version !== revision)
|
||||
return;
|
||||
quality.update(hls, config, model, force);
|
||||
if (!active(hls) || version !== revision)
|
||||
return;
|
||||
const audioConfig = option.audio || {};
|
||||
const audioView = audioModel(hls, audioConfig);
|
||||
if (active(hls) && version === revision)
|
||||
audio.update(hls, audioConfig, audioView, force);
|
||||
}
|
||||
function update() {
|
||||
errorHandle(art.hls?.media === $video, 'Cannot find instance of HLS from "art.hls"');
|
||||
updateQuality(art.hls);
|
||||
updateAudio(art.hls);
|
||||
if (closed)
|
||||
return;
|
||||
const hls = art.hls;
|
||||
errorHandle(hls?.media === $video, 'Cannot find instance of HLS from "art.hls"');
|
||||
if (retired.has(hls))
|
||||
return;
|
||||
if (engine !== hls) {
|
||||
unsubscribe();
|
||||
engine = hls;
|
||||
try {
|
||||
unsubscribe = subscribeHls(hls, () => {
|
||||
if (selecting)
|
||||
pending = hls;
|
||||
else
|
||||
refresh(hls, false);
|
||||
}, () => {
|
||||
retired.add(hls);
|
||||
unsubscribe();
|
||||
if (engine === hls) {
|
||||
engine = void 0;
|
||||
if (!closed) {
|
||||
quality.clear();
|
||||
audio.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
engine = void 0;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
refresh(hls, true);
|
||||
}
|
||||
function destroy() {
|
||||
if (closed)
|
||||
return;
|
||||
closed = true;
|
||||
revision++;
|
||||
pending = void 0;
|
||||
quality.invalidate();
|
||||
audio.invalidate();
|
||||
unsubscribe();
|
||||
art.off("ready", update);
|
||||
art.off("restart", update);
|
||||
art.off("destroy", destroy);
|
||||
}
|
||||
art.on("ready", update);
|
||||
art.on("restart", update);
|
||||
return {
|
||||
name: "artplayerPluginHlsControl",
|
||||
update
|
||||
};
|
||||
art.on("destroy", destroy);
|
||||
return { name: "artplayerPluginHlsControl", update };
|
||||
};
|
||||
}
|
||||
export {
|
||||
|
||||
@@ -1,152 +1,113 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
import { subscribeHls } from './sdk-events'
|
||||
|
||||
export default function artplayerPluginHlsControl(option = {}) {
|
||||
return (art) => {
|
||||
const { $video } = art.template
|
||||
const { errorHandle } = art.constructor.utils
|
||||
let closed = false
|
||||
let revision = 0
|
||||
let engine
|
||||
let unsubscribe = () => {}
|
||||
let selecting = 0
|
||||
let pending
|
||||
const retired = new WeakSet()
|
||||
const active = hls => !closed && !retired.has(hls) && art.hls === hls && hls.media === $video
|
||||
const quality = createMenu(art, 'hls-quality', 'currentLevel', $quality, active, select)
|
||||
const audio = createMenu(art, 'hls-audio', 'audioTrack', $audio, active, select)
|
||||
|
||||
function updateQuality(hls) {
|
||||
if (!hls.levels.length)
|
||||
return
|
||||
|
||||
const config = option.quality || {}
|
||||
const auto = config.auto || 'Auto'
|
||||
const title = config.title || 'Quality'
|
||||
const getName = config.getName || (level => level.name || `${level.height}P`)
|
||||
const defaultLevel = hls.levels[hls.currentLevel]
|
||||
const defaultHtml = defaultLevel ? getName(defaultLevel) : auto
|
||||
|
||||
const selector = uniqBy(
|
||||
hls.levels.map((item, index) => {
|
||||
return {
|
||||
html: getName(item, index),
|
||||
value: index,
|
||||
default: hls.currentLevel === index,
|
||||
}
|
||||
}),
|
||||
'html',
|
||||
).sort((a, b) => b.value - a.value)
|
||||
|
||||
selector.push({
|
||||
html: auto,
|
||||
value: -1,
|
||||
default: hls.currentLevel === -1,
|
||||
})
|
||||
|
||||
const onSelect = (item) => {
|
||||
hls.currentLevel = 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
|
||||
function select(callback) {
|
||||
selecting++
|
||||
let succeeded = false
|
||||
try {
|
||||
const result = callback()
|
||||
succeeded = true
|
||||
return result
|
||||
}
|
||||
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: 'hls-quality',
|
||||
position: 'right',
|
||||
html: defaultHtml,
|
||||
style: { padding: '0 10px' },
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: 'hls-quality',
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $quality,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
finally {
|
||||
selecting--
|
||||
if (!selecting) {
|
||||
const hls = pending
|
||||
pending = undefined
|
||||
if (succeeded && hls)
|
||||
refresh(hls, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateAudio(hls) {
|
||||
if (!hls.audioTracks.length)
|
||||
function refresh(hls, force) {
|
||||
if (!active(hls))
|
||||
return
|
||||
|
||||
const config = option.audio || {}
|
||||
const auto = config.auto || 'Auto'
|
||||
const title = config.title || 'Audio'
|
||||
const getName = config.getName || (track => track.name || track.lang || track.language)
|
||||
const defaultTrack = hls.audioTracks[hls.audioTrack]
|
||||
const defaultHtml = defaultTrack ? getName(defaultTrack) : auto
|
||||
|
||||
const selector = uniqBy(
|
||||
hls.audioTracks.map((item, index) => {
|
||||
return {
|
||||
html: getName(item, index),
|
||||
value: item.id,
|
||||
default: hls.audioTrack === item.id,
|
||||
}
|
||||
}),
|
||||
'html',
|
||||
)
|
||||
|
||||
const onSelect = (item) => {
|
||||
hls.audioTrack = 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: 'hls-audio',
|
||||
position: 'right',
|
||||
html: defaultHtml,
|
||||
style: { padding: '0 10px' },
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: 'hls-audio',
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $audio,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
const version = ++revision
|
||||
const config = option.quality || {}
|
||||
const model = qualityModel(hls, config)
|
||||
if (!active(hls) || version !== revision)
|
||||
return
|
||||
quality.update(hls, config, model, force)
|
||||
if (!active(hls) || version !== revision)
|
||||
return
|
||||
const audioConfig = option.audio || {}
|
||||
const audioView = audioModel(hls, audioConfig)
|
||||
if (active(hls) && version === revision)
|
||||
audio.update(hls, audioConfig, audioView, force)
|
||||
}
|
||||
|
||||
function update() {
|
||||
errorHandle(art.hls?.media === $video, 'Cannot find instance of HLS from "art.hls"')
|
||||
updateQuality(art.hls)
|
||||
updateAudio(art.hls)
|
||||
if (closed)
|
||||
return
|
||||
const hls = art.hls
|
||||
errorHandle(hls?.media === $video, 'Cannot find instance of HLS from "art.hls"')
|
||||
if (retired.has(hls))
|
||||
return
|
||||
if (engine !== hls) {
|
||||
unsubscribe()
|
||||
engine = hls
|
||||
try {
|
||||
unsubscribe = subscribeHls(hls, () => {
|
||||
if (selecting)
|
||||
pending = hls
|
||||
else
|
||||
refresh(hls, false)
|
||||
}, () => {
|
||||
retired.add(hls)
|
||||
unsubscribe()
|
||||
if (engine === hls) {
|
||||
engine = undefined
|
||||
if (!closed) {
|
||||
quality.clear()
|
||||
audio.clear()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
engine = undefined
|
||||
throw error
|
||||
}
|
||||
}
|
||||
refresh(hls, true)
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (closed)
|
||||
return
|
||||
closed = true
|
||||
revision++
|
||||
pending = undefined
|
||||
quality.invalidate()
|
||||
audio.invalidate()
|
||||
unsubscribe()
|
||||
art.off('ready', update)
|
||||
art.off('restart', update)
|
||||
art.off('destroy', destroy)
|
||||
}
|
||||
|
||||
art.on('ready', update)
|
||||
art.on('restart', update)
|
||||
|
||||
return {
|
||||
name: 'artplayerPluginHlsControl',
|
||||
update,
|
||||
}
|
||||
art.on('destroy', destroy)
|
||||
return { name: 'artplayerPluginHlsControl', update }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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) {
|
||||
first.default = true
|
||||
first.value = item.value
|
||||
}
|
||||
return false
|
||||
}
|
||||
seen.set(item.html, item)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function qualitySelection(hls) {
|
||||
return hls.autoLevelEnabled === true ? -1 : hls.currentLevel
|
||||
}
|
||||
|
||||
export function qualityModel(hls, config) {
|
||||
if (!hls.levels.length)
|
||||
return null
|
||||
const auto = config.auto || 'Auto'
|
||||
const getName = config.getName || (level => level.name || `${level.height}P`)
|
||||
const selected = qualitySelection(hls)
|
||||
const level = hls.levels[selected]
|
||||
const html = level ? getName(level) : auto
|
||||
const selector = uniqueLabels(hls.levels.map((item, index) => ({ html: getName(item, index), value: index, default: selected === index })))
|
||||
.sort((a, b) => b.value - a.value)
|
||||
selector.push({ html: auto, value: -1, default: selected === -1 })
|
||||
return { html, title: config.title || 'Quality', selector }
|
||||
}
|
||||
|
||||
export function audioModel(hls, config) {
|
||||
if (!hls.audioTracks.length)
|
||||
return null
|
||||
const auto = config.auto || 'Auto'
|
||||
const getName = config.getName || (track => track.name || track.lang || track.language)
|
||||
const track = hls.audioTracks[hls.audioTrack]
|
||||
const html = track ? getName(track) : auto
|
||||
const selector = uniqueLabels(hls.audioTracks.map((item, index) => ({ html: getName(item, index), value: item.id, default: hls.audioTrack === item.id })))
|
||||
return { html, title: config.title || 'Audio', selector }
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
export function createMenu(art, name, field, icon, active, select) {
|
||||
let current
|
||||
const owned = { control: false, setting: false }
|
||||
|
||||
function remove(surface) {
|
||||
if (!owned[surface])
|
||||
return
|
||||
owned[surface] = false
|
||||
art[surface === 'control' ? 'controls' : 'setting'].remove(name)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
current = undefined
|
||||
remove('control')
|
||||
remove('setting')
|
||||
}
|
||||
|
||||
function check(state, item) {
|
||||
for (const entry of state.model.selector)
|
||||
entry.default = entry === item
|
||||
if (state.config.control)
|
||||
art.controls.check(item)
|
||||
if (state !== current || !active(state.hls))
|
||||
return
|
||||
if (state.config.setting)
|
||||
art.setting.check(item)
|
||||
}
|
||||
|
||||
function update(hls, config, model, force) {
|
||||
if (!model) {
|
||||
clear()
|
||||
return
|
||||
}
|
||||
const previous = current
|
||||
const selectedIndex = model.selector.findIndex(item => item.default)
|
||||
const reusable = !force && previous?.hls === hls && previous.model.title === model.title
|
||||
&& (selectedIndex !== -1 || (!previous.model.selector.some(item => item.default) && previous.model.html === model.html))
|
||||
&& owned.control === Boolean(config.control) && owned.setting === Boolean(config.setting)
|
||||
&& previous.model.selector.length === model.selector.length
|
||||
&& previous.model.selector.every((item, index) => item.html === model.selector[index].html && item.value === model.selector[index].value)
|
||||
if (reusable) {
|
||||
previous.config = config
|
||||
const target = previous.model.selector[selectedIndex]
|
||||
if (target && (!target.default || previous.model.html !== model.html)) {
|
||||
previous.model.html = model.html
|
||||
check(previous, target)
|
||||
}
|
||||
return
|
||||
}
|
||||
const state = { hls, config, model }
|
||||
current = state
|
||||
const valid = () => current === state && active(hls)
|
||||
const onSelect = item => select(() => {
|
||||
if (!valid())
|
||||
return item.html
|
||||
hls[field] = item.value
|
||||
if (!valid())
|
||||
return item.html
|
||||
model.html = item.html
|
||||
art.notice.show = `${model.title}: ${item.html}`
|
||||
if (valid())
|
||||
check(state, item)
|
||||
return item.html
|
||||
})
|
||||
if (config.control) {
|
||||
owned.control = true
|
||||
art.controls.update({ name, position: 'right', html: model.html, style: { padding: '0 10px' }, selector: model.selector, onSelect })
|
||||
}
|
||||
else {
|
||||
remove('control')
|
||||
}
|
||||
if (!valid())
|
||||
return
|
||||
if (config.setting) {
|
||||
owned.setting = true
|
||||
art.setting.update({ name, tooltip: model.html, html: model.title, icon, width: 200, selector: model.selector, onSelect })
|
||||
}
|
||||
else {
|
||||
remove('setting')
|
||||
}
|
||||
}
|
||||
|
||||
function invalidate() {
|
||||
current = undefined
|
||||
}
|
||||
return { update, clear, invalidate }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
export function subscribeHls(hls, refresh, destroyed) {
|
||||
const events = hls.constructor?.Events
|
||||
if (!events || typeof hls.on !== 'function' || typeof hls.off !== 'function')
|
||||
return () => {}
|
||||
const subscriptions = []
|
||||
let active = true
|
||||
function release() {
|
||||
if (!active)
|
||||
return
|
||||
active = false
|
||||
for (const [event, callback] of subscriptions)
|
||||
hls.off(event, callback)
|
||||
subscriptions.length = 0
|
||||
}
|
||||
try {
|
||||
for (const key of ['MANIFEST_PARSED', 'LEVELS_UPDATED', 'LEVEL_SWITCHED', 'AUDIO_TRACKS_UPDATED', 'AUDIO_TRACK_SWITCHED', 'DESTROYING']) {
|
||||
const event = events[key]
|
||||
if (typeof event !== 'string' || subscriptions.some(([name]) => name === event))
|
||||
continue
|
||||
const callback = () => {
|
||||
if (!active)
|
||||
return
|
||||
if (key === 'DESTROYING')
|
||||
destroyed()
|
||||
else
|
||||
refresh()
|
||||
}
|
||||
subscriptions.push([event, callback])
|
||||
hls.on(event, callback)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
release()
|
||||
throw error
|
||||
}
|
||||
return release
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,48 @@
|
||||
# PKG-HLS-03:HLS 映射、菜单与订阅分层
|
||||
|
||||
## 结构调整
|
||||
|
||||
入口只协调核心生命周期、实例绑定和刷新代次;mapping.js 负责标签/选择模型,menu.js 负责
|
||||
既有 controls/setting API 与回调归属,sdk-events.js 负责 SDK 能力探测、订阅与解绑。
|
||||
没有引入 Hls 运行时依赖或新公开参数/方法。自有源码仍是 JS,严格 TS 和公开声明迁移继续
|
||||
由 PKG-HLS-04 完成,不能把本步的源码拆分计入 TS 文件数。
|
||||
|
||||
## 缺陷修正与兼容处理
|
||||
|
||||
- HLS-UI-01:空 levels/audioTracks 或关闭显示开关时删除相应旧菜单,再恢复时可以重建。
|
||||
- HLS-LIFE-01:旧菜单回调检查当前实例/菜单归属;核心销毁解绑自身 core/SDK 监听器,保留
|
||||
update 调用不再触碰 SDK/UI。SDK DESTROYING 清理自身菜单、永久拒绝该实例重新绑定。
|
||||
- HLS-AUTO-01:自动策略与实际播放档位分开;真实 Auto 播放的标签保持 Auto,手动选择与
|
||||
外部音轨切换通过 SDK 事件同步。无 autoLevelEnabled 能力的旧适配对象保留 currentLevel fallback。
|
||||
- 重复标签合并时,如果实际选中项位于后面的重复项,用该项的 value 代表同名组,避免丢失
|
||||
default;这一内部代表项修正单独测试,不将它称为完全无行为变化的文件移动。
|
||||
- SDK 事件优先复用未变化的菜单,不在 ABR 事件中反复销毁控件;当前音轨未知时清除旧高亮。
|
||||
同步 SDK 事件延后至本次选择操作完成,保留 write→notice→checks→同步返回顺序。
|
||||
- formatter 销毁/替换实例或嵌套 update 时,旧刷新不能覆盖较新的 UI;SDK 注册异常撤回已经
|
||||
安装的监听器并允许重试。没有延后 SDK 写入或将 update 改成 Promise。
|
||||
|
||||
公开工厂、name/update、配置、原回调对象与可选索引、控件名、SVG、布局和分发路径保持。
|
||||
getName 的旧声明缺陷仍属 04;自动模式不再为错误的当前档位标签调用一次 selected getName,
|
||||
列表命名仍按原对象与索引执行。参见 [包内实际架构](../../packages/artplayer-plugin-hls-control/ARCHITECTURE.md)。
|
||||
|
||||
## 验证与范围
|
||||
|
||||
证据见 [冻结记录](../baselines/hls-modules-validation.json)。Node 专项含共享发布契约和候选修复;
|
||||
额外读取本次 build 生成的 main/legacy/ESM 运行同一组正常契约。浏览器使用明确的构建文件,
|
||||
覆盖新旧核心/插件组合及真实 SDK 外部事件的选中态、菜单节点身份、清理等行为。
|
||||
Hls 1.5.17、Chromium/Firefox、Windows WebKit 的不支持能力边界与旧版测试分开保留。
|
||||
|
||||
本步构建产物及 docs/compiled 副本由正式 build 生成;未手改分发文件。真实 Safari/native、
|
||||
SDK worker/版本/分组轨道、官方示例和隔离 tarball 消费仍为 05/06,SDK-01/HLS-ENV-01 不关闭。
|
||||
完整 ci:check 检查类型、lint、工程与旧基线;没有把源码浏览器或工作区 dist 当作 npm 安装通过。
|
||||
本任务没有改变核心源码,不重复全量核心浏览器矩阵。
|
||||
|
||||
最终 Node 专项 56 项通过;加入三个构建格式后 83 项通过。modern/legacy 浏览器各 35 passed、
|
||||
16 skipped、0 failed/retry;跳过仅 Windows WebKit 的无 MSE 播放场景,能力/失败清理仍执行。
|
||||
完整 CI 533 项通过(496 单元、11 工程、26 基线),249 个既有生产 TS 文件严格检查通过。
|
||||
modern 为 5257 raw / 2536 gzip9 字节,发布基线为 3156 / 1439;增加订阅与生命周期成本,
|
||||
不称为体积优化。package/dist 与 docs/compiled 三格式逐字一致,源/产物/报告指纹已核对。
|
||||
既有生成 editor 声明的 unused eslint-disable warning 沿用,不手改生成文件。
|
||||
|
||||
回退本任务的独立 commit 可恢复单文件实现及其生成产物,保留 01/02 冻结来源和缺陷观察。
|
||||
下一步 PKG-HLS-04,迁移已拆分模块与兼容公开类型,同样独立提交。
|
||||
@@ -1,5 +1,14 @@
|
||||
# HLS control 测试与环境边界
|
||||
|
||||
PKG-HLS-03 已在入口、映射、菜单和 SDK 订阅四个模块实施修复,实际地图见
|
||||
[包内架构](../packages/artplayer-plugin-hls-control/ARCHITECTURE.md),本轮证据见
|
||||
[模块验证](baselines/hls-modules-validation.json)。下方 02 的数量与缺陷观察属于冻结的迁移前基线。
|
||||
新版用例增加事件同步、菜单节点复用和候选生命周期断言,公开 TS 迁移仍是 04。
|
||||
|
||||
新增构建文件入口:ARTPLAYER_TEST_HLS 以平台路径分隔符接受 main/legacy/ESM,用于同一 Node
|
||||
契约;ARTPLAYER_HLS_ARTIFACT 选择实际 main/legacy 浏览器文件。测试记录实际 hash,错误路径
|
||||
明确失败。这是工作区构建验证;隔离安装以及整套 ARTPLAYER_BROWSER_ARTIFACTS 映射仍由 06 完成。
|
||||
|
||||
PKG-HLS-02 建立迁移前基线。源码目前仍为 src/index.js,不能把测试任务当作 TS 迁移已完成。
|
||||
来源见 [发布契约](baselines/hls-control-contract.md)、[SDK 固定记录](baselines/hls-sdk.json)、
|
||||
[本轮证据](baselines/hls-validation.json)。
|
||||
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 215 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 152 / doing 0 / blocked 0 / done 63 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 151 / doing 0 / blocked 0 / done 64 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-HLS-01 | artplayer-plugin-hls-control<br>核对包契约与历史用法 | BASE-05 | quality/audio 配置、getName、update、控件名及 Hls 实例绑定 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
|
||||
| PKG-HLS-02 | artplayer-plugin-hls-control<br>建立特有行为与错误测试 | PKG-HLS-01, ENG-03, ENG-05 | Auto/手动实际 level、音轨、去重/过滤和拓扑变化 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
|
||||
| PKG-HLS-03 | artplayer-plugin-hls-control<br>整理内部职责与资源 | PKG-HLS-02, CORE-11, CORE-14 | 映射计算与 selector 更新/删除、Hls 事件订阅分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
|
||||
| PKG-HLS-03 | artplayer-plugin-hls-control<br>整理内部职责与资源 | PKG-HLS-02, CORE-11, CORE-14 | 映射计算与 selector 更新/删除、Hls 事件订阅分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
|
||||
| PKG-HLS-04 | artplayer-plugin-hls-control<br>迁移自有源码和公开类型 | PKG-HLS-03, ENG-04, ENG-06, CORE-07 | HLS 能力适配类型、回调上下文和旧 getName 参数 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
|
||||
| PKG-HLS-05 | artplayer-plugin-hls-control<br>验证新旧核心和组合 | PKG-HLS-04, CORE-22 | 本地多码率 HLS、换成无轨道来源、旧核心和最终核心 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
| PKG-HLS-06 | artplayer-plugin-hls-control<br>验证分发并同步文档 | PKG-HLS-05, ENG-07 | hls.control.js、依赖范围与回退记录 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
|
||||
@@ -467,3 +467,4 @@
|
||||
- PKG-CHAPTER-04: [记录](changes/2026-09-10-PKG-CHAPTER-04-public-types.md) [记录](baselines/chapter-types-validation.json)
|
||||
- PKG-HLS-01: [记录](baselines/hls-control-contract.md) [记录](baselines/hls-control-release.json) [记录](changes/2026-09-11-PKG-HLS-01-contract.md)
|
||||
- PKG-HLS-02: [记录](hls-validation.md) [记录](baselines/hls-sdk.json) [记录](baselines/hls-validation.json) [记录](changes/2026-09-11-PKG-HLS-02-tests.md)
|
||||
- PKG-HLS-03: [记录](changes/2026-09-12-PKG-HLS-03-modules.md) [记录](baselines/hls-modules-validation.json) [记录](hls-validation.md)
|
||||
@@ -1,5 +1,13 @@
|
||||
# 进度与证据
|
||||
|
||||
## 最新完成:PKG-HLS-03 源码分层与生命周期修复
|
||||
|
||||
HLS control 已拆为入口、选择映射、菜单管理、SDK 订阅四个 JS 模块,保持公开入口和同步调用;修复空轨道旧菜单、过期回调、Auto 标签、重复组丢失选中态,并补事件同步、销毁/订阅失败恢复及重入保护。公开类型和严格 TS 迁移属于下一步 04。
|
||||
|
||||
56 项 Node 专项通过,加入三种构建格式后 83 项通过;完整 CI 533 项、249 个既有生产 TS 文件通过。modern/legacy 浏览器各 35 passed、16 skipped,无失败/重试;Windows WebKit 的 MSE 缺口未关闭。三份 package/dist 与 docs/compiled 一致,modern gzip 为 2536 字节(发布基线 1439),不声称体积优化。
|
||||
|
||||
见 [交付记录](changes/2026-09-12-PKG-HLS-03-modules.md)、[冻结证据](baselines/hls-modules-validation.json) 与包内 ARCHITECTURE.md。HLS-UI-01/LIFE-01/AUTO-01 已按范围关闭,SDK-01/ENV-01 与其余发布门槛仍 open。64 done、151 todo;本任务独立本地 commit,核实后进入 PKG-HLS-04。没有 push/tag/publish/merge。
|
||||
|
||||
## 最新完成:PKG-HLS-02 行为与 SDK 基线
|
||||
|
||||
45 项 HLS Node 契约通过,覆盖源码及发布 main/legacy/ESM;完整 CI 522 项、249 个生产 TS 文件通过。真实 Hls.js 1.5.17 与本地双档位/双音轨流验证四组合的解码、实际选择、pause/seek、切源、失败恢复和销毁。
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
| VENDOR-06 | open / 待取证 | monaco-static-assets 来源、版本与许可闭环 | SITE-01, SITE-05 |
|
||||
| VENDOR-07 | open / 待取证 | vconsole 来源、版本与许可闭环 | SITE-01 |
|
||||
| VENDOR-08 | open / 待取证 | console-bundle 来源、版本与许可闭环 | SITE-01 |
|
||||
| SDK-01 | open / 源码/产物事实 | hls.js 实际集成验证范围 | PKG-HLS-03, PKG-HLS-05, EX-03 |
|
||||
| SDK-01 | open / 源码/产物事实 | hls.js 实际集成验证范围 | PKG-HLS-05, EX-03 |
|
||||
| SDK-02 | open / 待取证 | dash.js 实际集成验证范围 | PKG-DASH-01, EX-03 |
|
||||
| SDK-03 | open / 待取证 | flv.js 实际集成验证范围 | EX-03 |
|
||||
| SDK-04 | open / 待取证 | mpegts.js 实际集成验证范围 | EX-03 |
|
||||
@@ -120,7 +120,7 @@
|
||||
| BASE-LIFE-47 | resolved / 已复现 | AirPlay methods can call a picker or publish events and notices after destruction | CORE-20 |
|
||||
| BASE-LIFE-48 | resolved / 已复现 | Proxy-time destruction cannot clean a template before its public assignment | CORE-20 |
|
||||
| BASE-LIFE-49 | resolved / 已复现 | Constructor and Player initialization continue after a callback destroys the instance | CORE-20 |
|
||||
| HLS-UI-01 | open / 已复现 | 空轨道和关闭配置仍残留 HLS 菜单 | PKG-HLS-03 |
|
||||
| HLS-LIFE-01 | open / 已复现 | 过期 HLS 选择回调与销毁后 update 仍工作 | PKG-HLS-03 |
|
||||
| HLS-AUTO-01 | open / 已复现 | 真实 ABR 播放的 Auto 标签在 update 后丢失 | PKG-HLS-03, PKG-HLS-05 |
|
||||
| HLS-UI-01 | resolved / 已复现 | 空轨道和关闭配置仍残留 HLS 菜单 | PKG-HLS-03 |
|
||||
| HLS-LIFE-01 | resolved / 已复现 | 过期 HLS 选择回调与销毁后 update 仍工作 | PKG-HLS-03 |
|
||||
| HLS-AUTO-01 | resolved / 已复现 | 真实 ABR 播放的 Auto 标签在 update 后丢失 | PKG-HLS-03, PKG-HLS-05 |
|
||||
| HLS-ENV-01 | open / 已复现 | Windows WebKit 缺失 MSE,HLS 播放矩阵尚缺 Safari 证据 | PKG-HLS-05, REL-09, REVIEW-02 |
|
||||
+24
-7
@@ -606,7 +606,6 @@
|
||||
"confirmation": "source-observed",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-HLS-03",
|
||||
"PKG-HLS-05",
|
||||
"EX-03"
|
||||
],
|
||||
@@ -2527,7 +2526,7 @@
|
||||
"id": "HLS-UI-01",
|
||||
"title": "空轨道和关闭配置仍残留 HLS 菜单",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-HLS-03"
|
||||
],
|
||||
@@ -2537,13 +2536,19 @@
|
||||
"test/browser/hls-control.spec.js"
|
||||
],
|
||||
"compatibleResolution": "使用既有 remove API 清理不再可用的 UI,保留菜单名称及选择语义。",
|
||||
"closureCriteria": "候选空轨道、关闭开关及恢复测试通过。"
|
||||
"closureCriteria": "候选空轨道、关闭开关及恢复测试通过。",
|
||||
"resolutionEvidence": [
|
||||
"refactor/baselines/hls-modules-validation.json",
|
||||
"refactor/changes/2026-09-12-PKG-HLS-03-modules.md",
|
||||
"packages/artplayer-plugin-hls-control/ARCHITECTURE.md"
|
||||
],
|
||||
"resolutionRationale": "PKG-HLS-03 候选回归与新旧核心/实际 Hls.js 1.5.17 验证:空菜单、过期回调/订阅、Auto 及音轨同步。环境/SDK 扩展范围仍由 SDK-01 与 HLS-ENV-01 保持 open,不据此放行发布。"
|
||||
},
|
||||
{
|
||||
"id": "HLS-LIFE-01",
|
||||
"title": "过期 HLS 选择回调与销毁后 update 仍工作",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-HLS-03"
|
||||
],
|
||||
@@ -2553,13 +2558,19 @@
|
||||
"test/browser/hls-control.spec.js"
|
||||
],
|
||||
"compatibleResolution": "限制过期实例回调,整理监听器归属,Hls 仍由调用方销毁。",
|
||||
"closureCriteria": "替换、销毁、重入及 SDK 订阅解绑的候选回归通过。"
|
||||
"closureCriteria": "替换、销毁、重入及 SDK 订阅解绑的候选回归通过。",
|
||||
"resolutionEvidence": [
|
||||
"refactor/baselines/hls-modules-validation.json",
|
||||
"refactor/changes/2026-09-12-PKG-HLS-03-modules.md",
|
||||
"packages/artplayer-plugin-hls-control/ARCHITECTURE.md"
|
||||
],
|
||||
"resolutionRationale": "PKG-HLS-03 候选回归与新旧核心/实际 Hls.js 1.5.17 验证:空菜单、过期回调/订阅、Auto 及音轨同步。环境/SDK 扩展范围仍由 SDK-01 与 HLS-ENV-01 保持 open,不据此放行发布。"
|
||||
},
|
||||
{
|
||||
"id": "HLS-AUTO-01",
|
||||
"title": "真实 ABR 播放的 Auto 标签在 update 后丢失",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"status": "resolved",
|
||||
"owners": [
|
||||
"PKG-HLS-03",
|
||||
"PKG-HLS-05"
|
||||
@@ -2570,7 +2581,13 @@
|
||||
"test/browser/hls-control.spec.js"
|
||||
],
|
||||
"compatibleResolution": "区分自动策略与实际播放档位,保留手动及 Auto 选择值。",
|
||||
"closureCriteria": "实际 SDK 自动/手动切换和事件同步通过。"
|
||||
"closureCriteria": "实际 SDK 自动/手动切换和事件同步通过。",
|
||||
"resolutionEvidence": [
|
||||
"refactor/baselines/hls-modules-validation.json",
|
||||
"refactor/changes/2026-09-12-PKG-HLS-03-modules.md",
|
||||
"packages/artplayer-plugin-hls-control/ARCHITECTURE.md"
|
||||
],
|
||||
"resolutionRationale": "PKG-HLS-03 候选回归与新旧核心/实际 Hls.js 1.5.17 验证:空菜单、过期回调/订阅、Auto 及音轨同步。环境/SDK 扩展范围仍由 SDK-01 与 HLS-ENV-01 保持 open,不据此放行发布。"
|
||||
},
|
||||
{
|
||||
"id": "HLS-ENV-01",
|
||||
|
||||
+6
-2
@@ -1827,11 +1827,15 @@
|
||||
"CORE-11",
|
||||
"CORE-14"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "映射计算与 selector 更新/删除、Hls 事件订阅分离",
|
||||
"acceptance": "结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-12-PKG-HLS-03-modules.md",
|
||||
"baselines/hls-modules-validation.json",
|
||||
"hls-validation.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-HLS-04",
|
||||
|
||||
@@ -1023,16 +1023,18 @@
|
||||
"id": "hls.js",
|
||||
"riskId": "SDK-01",
|
||||
"owners": [
|
||||
"PKG-HLS-03",
|
||||
"PKG-HLS-05",
|
||||
"EX-03"
|
||||
],
|
||||
"sourceFiles": [
|
||||
"packages/artplayer-plugin-hls-control/src/index.js",
|
||||
"packages/artplayer-plugin-hls-control/src/mapping.js",
|
||||
"packages/artplayer-plugin-hls-control/src/menu.js",
|
||||
"packages/artplayer-plugin-hls-control/src/sdk-events.js",
|
||||
"docs/assets/example/hls.control.js"
|
||||
],
|
||||
"contract": "Caller supplies art.hls; homepage demo loads hls.js 1.5.17. Preserve quality/audio selectors and old core integration.",
|
||||
"validationStatus": "Hls.js 1.5.17 frozen in refactor/baselines/hls-sdk.json; Chromium/Firefox local playback baseline verified in PKG-HLS-02. Windows WebKit has no MSE; Safari/native/worker/version and installed-package matrices remain open. See refactor/hls-validation.md.",
|
||||
"validationStatus": "PKG-HLS-03 verifies actual Hls.js 1.5.17 on Chromium/Firefox with source/published core and modern/legacy candidate builds. Auto/topology/lifecycle defects fixed; Windows WebKit has no MSE. Safari/native, detach/reattach, worker, grouped tracks, versions and isolated installation remain HLS-05/06 and EX-03.",
|
||||
"demoPathsSource": "refactor/baselines/demo-inventory.json"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -40,7 +40,9 @@ test.beforeAll(async () => {
|
||||
publishedCode = await code(plugin.release, 'package/dist/artplayer-plugin-hls-control.js')
|
||||
// This task exercises source builds. Installed artifacts are a separate HLS-06 gate.
|
||||
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'HLS source fixture cannot masquerade as an installed plugin')
|
||||
sourceCode = await compilePackage('artplayer-plugin-hls-control', 'umd')
|
||||
sourceCode = process.env.ARTPLAYER_HLS_ARTIFACT
|
||||
? fs.readFileSync(process.env.ARTPLAYER_HLS_ARTIFACT, 'utf8')
|
||||
: await compilePackage('artplayer-plugin-hls-control', 'umd')
|
||||
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
|
||||
media = new Map()
|
||||
for (const [name, expected] of Object.entries(manifest.files)) {
|
||||
@@ -50,7 +52,7 @@ test.beforeAll(async () => {
|
||||
assert.equal(bytes.length, expected.bytes)
|
||||
media.set(name, bytes)
|
||||
}
|
||||
evidence = { sdk: sdk.release, plugin: plugin.release, sourceSHA256: hash(sourceCode), media: manifest, limitations: ['SDK worker disabled', 'local deterministic media', 'source candidate, not installed package', 'no physical device certification'] }
|
||||
evidence = { sdk: sdk.release, plugin: plugin.release, sourceSHA256: hash(sourceCode), candidate: process.env.ARTPLAYER_HLS_ARTIFACT || 'workspace source build', media: manifest, limitations: ['SDK worker disabled', 'local deterministic media', 'not an isolated installed package', 'no physical device certification'] }
|
||||
})
|
||||
|
||||
async function openHls(page, core, plugin, testInfo, manifest = 'master.m3u8') {
|
||||
@@ -211,6 +213,9 @@ test.describe('MSE playback matrix', () => {
|
||||
// Real SDK corroboration of the frozen historical empty-topology defect.
|
||||
await expect(page.locator('.art-control-hls-audio')).toHaveCount(1)
|
||||
}
|
||||
else {
|
||||
await expect(page.locator('.art-control-hls-audio')).toHaveCount(0)
|
||||
}
|
||||
expect(await page.evaluate(() => window.sdkErrors.filter(error => error.fatal))).toEqual([])
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
expect(await page.evaluate(() => window.destroyedEngines)).toBe(2)
|
||||
@@ -232,5 +237,34 @@ test.describe('MSE playback matrix', () => {
|
||||
expect(await page.evaluate(() => window.art.hls.autoLevelEnabled)).toBe(true)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
|
||||
test(`${core} core / candidate HLS: SDK events synchronize Auto and audio without replacing menus`, async ({ page }, testInfo) => {
|
||||
await openHls(page, core, 'candidate', testInfo)
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(0.3)
|
||||
await expect(page.locator('.art-control-hls-quality .art-selector-value')).toHaveText('Auto')
|
||||
await page.evaluate(() => {
|
||||
window.qualityElement = document.querySelector('.art-control-hls-quality')
|
||||
window.audioElement = document.querySelector('.art-control-hls-audio')
|
||||
window.art.hls.currentLevel = 0
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.hls.currentLevel)).toBe(0)
|
||||
await expect(page.locator('.art-control-hls-quality .art-selector-value')).toHaveText('90P')
|
||||
await page.evaluate(() => {
|
||||
window.art.hls.currentLevel = -1
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.hls.currentLevel)).toBeGreaterThanOrEqual(0)
|
||||
await expect(page.locator('.art-control-hls-quality .art-selector-value')).toHaveText('Auto')
|
||||
await page.evaluate(() => {
|
||||
window.art.hls.audioTrack = 1
|
||||
})
|
||||
await expect(page.locator('.art-control-hls-audio .art-selector-value')).toHaveText('French')
|
||||
expect(await page.evaluate(() => ({ quality: window.qualityElement === document.querySelector('.art-control-hls-quality'), audio: window.audioElement === document.querySelector('.art-control-hls-audio') }))).toEqual({ quality: true, audio: true })
|
||||
await page.evaluate(() => window.art.plugins.artplayerPluginHlsControl.update())
|
||||
await expect(page.locator('.art-control-hls-quality .art-selector-value')).toHaveText('Auto')
|
||||
expect(await page.evaluate(() => window.sdkErrors.filter(error => error.fatal))).toEqual([])
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,6 +1,8 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import vm from 'node:vm'
|
||||
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { loadPackage } from './load.js'
|
||||
@@ -26,6 +28,21 @@ export async function hlsImplementations() {
|
||||
assert.equal(typeof factory, 'function')
|
||||
implementations.push({ name: `published-${format}`, factory })
|
||||
}
|
||||
for (const filename of (process.env.ARTPLAYER_TEST_HLS || '').split(path.delimiter).filter(Boolean)) {
|
||||
const bytes = fs.readFileSync(filename)
|
||||
let factory
|
||||
if (filename.endsWith('.mjs')) {
|
||||
factory = (await import(`data:text/javascript;base64,${bytes.toString('base64')}`)).default
|
||||
}
|
||||
else {
|
||||
const context = { module: { exports: {} } }
|
||||
context.exports = context.module.exports
|
||||
vm.runInNewContext(bytes.toString(), context, { timeout: 5000 })
|
||||
factory = context.module.exports
|
||||
}
|
||||
assert.equal(typeof factory, 'function')
|
||||
implementations.push({ name: `artifact-${path.basename(filename)}`, factory })
|
||||
}
|
||||
return implementations
|
||||
}
|
||||
|
||||
@@ -102,3 +119,23 @@ export function hlsHost() {
|
||||
|
||||
export const bothMenus = () => ({ quality: { control: true, setting: true }, audio: { control: true, setting: true } })
|
||||
export const selections = menu => Array.from(menu.selector, item => [item.html, item.value, item.default])
|
||||
|
||||
export function attachHlsEvents(hls) {
|
||||
const listeners = new Map()
|
||||
const Events = Object.fromEntries(['MANIFEST_PARSED', 'LEVELS_UPDATED', 'LEVEL_SWITCHED', 'AUDIO_TRACKS_UPDATED', 'AUDIO_TRACK_SWITCHED', 'DESTROYING'].map(key => [key, key]))
|
||||
hls.constructor = { Events }
|
||||
hls.on = (name, callback) => {
|
||||
if (!listeners.has(name))
|
||||
listeners.set(name, new Set())
|
||||
listeners.get(name).add(callback)
|
||||
}
|
||||
hls.off = (name, callback) => listeners.get(name)?.delete(callback)
|
||||
return {
|
||||
listeners,
|
||||
emit(name) {
|
||||
for (const callback of [...(listeners.get(name) || [])])
|
||||
callback(name, {})
|
||||
},
|
||||
count: () => [...listeners.values()].reduce((total, entries) => total + entries.size, 0),
|
||||
}
|
||||
}
|
||||
+209
-1
@@ -1,9 +1,10 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- The repo uses Node's runner for controlled contracts.
|
||||
import { test } from 'node:test'
|
||||
import { bothMenus, hlsHost, hlsImplementations, selections } from './helpers/hls-control.js'
|
||||
import { attachHlsEvents, bothMenus, hlsHost, hlsImplementations, selections } from './helpers/hls-control.js'
|
||||
|
||||
const implementations = await hlsImplementations()
|
||||
const candidate = implementations.find(item => item.name === 'source').factory
|
||||
for (const { name, factory } of implementations) {
|
||||
test(`${name}: optional factory, synchronous result and deferred core events`, () => {
|
||||
const { art, controls, calls, listeners } = hlsHost()
|
||||
@@ -219,3 +220,210 @@ for (const { name, factory } of implementations.filter(item => item.name.startsW
|
||||
assert.equal(controls.get('hls-quality').selector.some(item => item.default), false)
|
||||
})
|
||||
}
|
||||
|
||||
test('candidate removes empty and disabled menus, then restores them', () => {
|
||||
const { art, hls, controls, settings } = hlsHost()
|
||||
const option = bothMenus()
|
||||
const plugin = candidate(option)(art)
|
||||
plugin.update()
|
||||
hls.levels = []
|
||||
hls.audioTracks = []
|
||||
plugin.update()
|
||||
assert.equal(controls.size, 0)
|
||||
assert.equal(settings.size, 0)
|
||||
hls.levels = [{ height: 240 }]
|
||||
hls.audioTracks = [{ id: 0, name: 'New' }]
|
||||
plugin.update()
|
||||
assert.equal(controls.size, 2)
|
||||
option.quality.control = false
|
||||
option.audio.setting = false
|
||||
plugin.update()
|
||||
assert.deepEqual([...controls.keys()], ['hls-audio'])
|
||||
assert.deepEqual([...settings.keys()], ['hls-quality'])
|
||||
})
|
||||
|
||||
test('candidate ignores superseded callbacks and retained update after destroy', () => {
|
||||
const { art, hls, calls, controls, listeners } = hlsHost()
|
||||
const events = attachHlsEvents(hls)
|
||||
const plugin = candidate(bothMenus())(art)
|
||||
plugin.update()
|
||||
const old = controls.get('hls-quality')
|
||||
const next = hlsHost().hls
|
||||
next.media = art.template.$video
|
||||
art.hls = next
|
||||
calls.length = 0
|
||||
old.onSelect(old.selector[0])
|
||||
assert.equal(hls.currentLevel, -1)
|
||||
assert.equal(calls.length, 0)
|
||||
plugin.update()
|
||||
assert.equal(events.count(), 0)
|
||||
const current = controls.get('hls-quality')
|
||||
art.emit('destroy')
|
||||
calls.length = 0
|
||||
assert.equal(plugin.update(), undefined)
|
||||
current.onSelect(current.selector[0])
|
||||
assert.equal(calls.length, 0)
|
||||
assert.equal([...listeners.values()].every(set => !set.size), true)
|
||||
})
|
||||
|
||||
test('candidate subscriptions update selection without replacing unchanged menus', () => {
|
||||
const { art, hls, calls, controls } = hlsHost()
|
||||
const events = attachHlsEvents(hls)
|
||||
hls.autoLevelEnabled = true
|
||||
hls.currentLevel = 1
|
||||
const plugin = candidate(bothMenus())(art)
|
||||
plugin.update()
|
||||
assert.equal(events.count(), 6)
|
||||
const menu = controls.get('hls-quality')
|
||||
assert.equal(menu.html, 'Auto')
|
||||
assert.equal(menu.selector.at(-1).default, true)
|
||||
hls.currentLevel = 2
|
||||
calls.length = 0
|
||||
events.emit('LEVEL_SWITCHED')
|
||||
assert.equal(controls.get('hls-quality'), menu)
|
||||
assert.equal(calls.length, 0)
|
||||
hls.autoLevelEnabled = false
|
||||
events.emit('LEVEL_SWITCHED')
|
||||
assert.equal(controls.get('hls-quality'), menu)
|
||||
assert.equal(menu.selector[0].default, true)
|
||||
assert.deepEqual(calls.map(call => call[0]), ['controls.check', 'setting.check'])
|
||||
calls.length = 0
|
||||
events.emit('LEVEL_SWITCHED')
|
||||
assert.equal(calls.length, 0)
|
||||
hls.audioTracks = []
|
||||
events.emit('AUDIO_TRACKS_UPDATED')
|
||||
assert.equal(controls.has('hls-audio'), false)
|
||||
events.emit('DESTROYING')
|
||||
assert.equal(events.count(), 0)
|
||||
assert.equal(controls.size, 0)
|
||||
})
|
||||
|
||||
test('candidate duplicate labels retain the actual selected representation', () => {
|
||||
const { art, hls, controls } = hlsHost()
|
||||
hls.levels = [{ height: 720 }, { height: 720 }]
|
||||
hls.currentLevel = 1
|
||||
candidate(bothMenus())(art).update()
|
||||
assert.deepEqual(selections(controls.get('hls-quality')), [['720P', 1, true], ['Auto', -1, false]])
|
||||
})
|
||||
|
||||
test('candidate rolls back failed subscription setup and retries on update', () => {
|
||||
const { art, hls } = hlsHost()
|
||||
const events = attachHlsEvents(hls)
|
||||
const on = hls.on
|
||||
hls.on = (name, callback) => {
|
||||
on(name, callback)
|
||||
if (name === 'LEVEL_SWITCHED')
|
||||
throw new Error('subscription rejected')
|
||||
}
|
||||
const plugin = candidate(bothMenus())(art)
|
||||
assert.throws(() => plugin.update(), /subscription rejected/)
|
||||
assert.equal(events.count(), 0)
|
||||
hls.on = on
|
||||
plugin.update()
|
||||
assert.equal(events.count(), 6)
|
||||
plugin.update()
|
||||
assert.equal(events.count(), 6)
|
||||
art.emit('destroy')
|
||||
assert.equal(events.count(), 0)
|
||||
})
|
||||
|
||||
test('candidate synchronously emitted SDK events preserve selection order', () => {
|
||||
const { art, hls, controls, calls } = hlsHost()
|
||||
const events = attachHlsEvents(hls)
|
||||
let selected = -1
|
||||
Object.defineProperty(hls, 'currentLevel', {
|
||||
get: () => selected,
|
||||
set(value) {
|
||||
calls.push(['currentLevel', value])
|
||||
selected = value
|
||||
events.emit('LEVEL_SWITCHED')
|
||||
},
|
||||
})
|
||||
candidate(bothMenus())(art).update()
|
||||
const menu = controls.get('hls-quality')
|
||||
const item = menu.selector[0]
|
||||
calls.length = 0
|
||||
assert.equal(menu.onSelect(item), item.html)
|
||||
assert.deepEqual(calls, [['currentLevel', 2], ['notice', 'Quality: 1080P'], ['controls.check', item], ['setting.check', item]])
|
||||
})
|
||||
|
||||
test('candidate cancels UI publication when getName destroys or replaces the player engine', () => {
|
||||
for (const action of ['destroy', 'replace']) {
|
||||
const { art, calls } = hlsHost()
|
||||
const option = bothMenus()
|
||||
option.quality.getName = () => {
|
||||
if (action === 'destroy')
|
||||
art.emit('destroy')
|
||||
else
|
||||
art.hls = hlsHost().hls
|
||||
return 'Label'
|
||||
}
|
||||
const plugin = candidate(option)(art)
|
||||
plugin.update()
|
||||
assert.equal(calls.length, 0)
|
||||
}
|
||||
})
|
||||
|
||||
test('candidate removes only its own SDK listeners and never revives a destroying engine', () => {
|
||||
const { art, hls, calls } = hlsHost()
|
||||
const events = attachHlsEvents(hls)
|
||||
const foreign = () => {}
|
||||
hls.on('LEVEL_SWITCHED', foreign)
|
||||
const plugin = candidate(bothMenus())(art)
|
||||
plugin.update()
|
||||
assert.equal(events.count(), 7)
|
||||
events.emit('DESTROYING')
|
||||
assert.equal(events.count(), 1)
|
||||
assert.equal(events.listeners.get('LEVEL_SWITCHED').has(foreign), true)
|
||||
calls.length = 0
|
||||
plugin.update()
|
||||
assert.equal(events.count(), 1)
|
||||
assert.equal(calls.length, 0)
|
||||
})
|
||||
|
||||
test('candidate nested update wins over the outer formatter operation', () => {
|
||||
const { art, controls } = hlsHost()
|
||||
const option = bothMenus()
|
||||
let nested = false
|
||||
let plugin
|
||||
option.quality.getName = () => {
|
||||
if (!nested) {
|
||||
nested = true
|
||||
option.quality.getName = () => 'Latest'
|
||||
plugin.update()
|
||||
}
|
||||
return 'Stale'
|
||||
}
|
||||
plugin = candidate(option)(art)
|
||||
plugin.update()
|
||||
assert.equal(controls.get('hls-quality').selector[0].html, 'Latest')
|
||||
})
|
||||
|
||||
test('candidate clears selection and fallback label while an audio track is temporarily unknown', () => {
|
||||
const { art, hls, controls } = hlsHost()
|
||||
const events = attachHlsEvents(hls)
|
||||
candidate(bothMenus())(art).update()
|
||||
hls.audioTrack = -1
|
||||
events.emit('AUDIO_TRACKS_UPDATED')
|
||||
const menu = controls.get('hls-audio')
|
||||
assert.equal(menu.html, 'Auto')
|
||||
assert.equal(menu.selector.some(item => item.default), false)
|
||||
events.emit('AUDIO_TRACKS_UPDATED')
|
||||
assert.equal(controls.get('hls-audio'), menu)
|
||||
hls.audioTrack = 1
|
||||
events.emit('AUDIO_TRACK_SWITCHED')
|
||||
assert.equal(menu.selector[1].default, true)
|
||||
})
|
||||
|
||||
test('candidate does not repeat UI writes when getName has different current and list labels', () => {
|
||||
const { art, hls, calls } = hlsHost()
|
||||
const events = attachHlsEvents(hls)
|
||||
const options = bothMenus()
|
||||
options.quality.getName = (level, index) => index === undefined ? `Current ${level.height}` : `Choice ${index}`
|
||||
candidate(options)(art).update()
|
||||
hls.currentLevel = 1
|
||||
events.emit('LEVEL_SWITCHED')
|
||||
calls.length = 0
|
||||
events.emit('LEVEL_SWITCHED')
|
||||
assert.equal(calls.length, 0)
|
||||
})
|
||||
Reference in new issue
Block a user