diff --git a/docs/compiled/artplayer-plugin-hls-control.js b/docs/compiled/artplayer-plugin-hls-control.js
index 975adfd0c..176558e93 100644
--- a/docs/compiled/artplayer-plugin-hls-control.js
+++ b/docs/compiled/artplayer-plugin-hls-control.js
@@ -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:'',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:'',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",'',h,v),g=e(n,"hls-audio","audioTrack",'',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",y),n.off("restart",y),n.off("destroy",t))}),{name:"artplayerPluginHlsControl",update:y}}}});
diff --git a/docs/compiled/artplayer-plugin-hls-control.legacy.js b/docs/compiled/artplayer-plugin-hls-control.legacy.js
index 29de770fa..56bf50dd2 100644
--- a/docs/compiled/artplayer-plugin-hls-control.legacy.js
+++ b/docs/compiled/artplayer-plugin-hls-control.legacy.js
@@ -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:'',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:'',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",'',h,v),g=e(l,"hls-audio","audioTrack",'',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++,c=void 0,m.invalidate(),g.invalidate(),a(),l.off("ready",y),l.off("restart",y),l.off("destroy",t))}),{name:"artplayerPluginHlsControl",update:y}}}});
diff --git a/docs/compiled/artplayer-plugin-hls-control.mjs b/docs/compiled/artplayer-plugin-hls-control.mjs
index 8169373f1..fa68a04f7 100644
--- a/docs/compiled/artplayer-plugin-hls-control.mjs
+++ b/docs/compiled/artplayer-plugin-hls-control.mjs
@@ -5,137 +5,261 @@
* Released under the MIT License.
*/
const $audio = '';
-const $quality = '';
-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 = '';
+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 {
diff --git a/packages/artplayer-plugin-hls-control/ARCHITECTURE.md b/packages/artplayer-plugin-hls-control/ARCHITECTURE.md
new file mode 100644
index 000000000..697f12c55
--- /dev/null
+++ b/packages/artplayer-plugin-hls-control/ARCHITECTURE.md
@@ -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.
diff --git a/packages/artplayer-plugin-hls-control/README.md b/packages/artplayer-plugin-hls-control/README.md
index 8534778df..8fd364060 100644
--- a/packages/artplayer-plugin-hls-control/README.md
+++ b/packages/artplayer-plugin-hls-control/README.md
@@ -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
diff --git a/packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.js b/packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.js
index 975adfd0c..176558e93 100644
--- a/packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.js
+++ b/packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.js
@@ -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:'',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:'',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",'',h,v),g=e(n,"hls-audio","audioTrack",'',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",y),n.off("restart",y),n.off("destroy",t))}),{name:"artplayerPluginHlsControl",update:y}}}});
diff --git a/packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.legacy.js b/packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.legacy.js
index 29de770fa..56bf50dd2 100644
--- a/packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.legacy.js
+++ b/packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.legacy.js
@@ -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:'',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:'',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",'',h,v),g=e(l,"hls-audio","audioTrack",'',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++,c=void 0,m.invalidate(),g.invalidate(),a(),l.off("ready",y),l.off("restart",y),l.off("destroy",t))}),{name:"artplayerPluginHlsControl",update:y}}}});
diff --git a/packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.mjs b/packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.mjs
index 8169373f1..fa68a04f7 100644
--- a/packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.mjs
+++ b/packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.mjs
@@ -5,137 +5,261 @@
* Released under the MIT License.
*/
const $audio = '';
-const $quality = '';
-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 = '';
+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 {
diff --git a/packages/artplayer-plugin-hls-control/src/index.js b/packages/artplayer-plugin-hls-control/src/index.js
index 61913d0d0..db43a43b4 100644
--- a/packages/artplayer-plugin-hls-control/src/index.js
+++ b/packages/artplayer-plugin-hls-control/src/index.js
@@ -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 }
}
}
diff --git a/packages/artplayer-plugin-hls-control/src/mapping.js b/packages/artplayer-plugin-hls-control/src/mapping.js
new file mode 100644
index 000000000..c3f0cc611
--- /dev/null
+++ b/packages/artplayer-plugin-hls-control/src/mapping.js
@@ -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 }
+}
diff --git a/packages/artplayer-plugin-hls-control/src/menu.js b/packages/artplayer-plugin-hls-control/src/menu.js
new file mode 100644
index 000000000..3abdb6f9a
--- /dev/null
+++ b/packages/artplayer-plugin-hls-control/src/menu.js
@@ -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 }
+}
diff --git a/packages/artplayer-plugin-hls-control/src/sdk-events.js b/packages/artplayer-plugin-hls-control/src/sdk-events.js
new file mode 100644
index 000000000..fb7c156a6
--- /dev/null
+++ b/packages/artplayer-plugin-hls-control/src/sdk-events.js
@@ -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
+}
diff --git a/refactor/baselines/hls-modules-validation.json b/refactor/baselines/hls-modules-validation.json
new file mode 100644
index 000000000..37122abb2
--- /dev/null
+++ b/refactor/baselines/hls-modules-validation.json
@@ -0,0 +1,4637 @@
+{
+ "schemaVersion": 1,
+ "task": "PKG-HLS-03",
+ "capturedAt": "2026-09-11T16:15:05.504Z",
+ "sourceHashesLF": {
+ "packages/artplayer-plugin-hls-control/src/index.js": "cf65f92627979ab4ca7952cff4cdc962e996241005ef00a5f72c21b9a80dd23f",
+ "packages/artplayer-plugin-hls-control/src/mapping.js": "da20d5d6ac2073f541731c5143f25d68d83c984a4fe7f19821f62281eaa5a90f",
+ "packages/artplayer-plugin-hls-control/src/menu.js": "3045c4a70e0d882cd3691bf3914f86e06815275f7a29f48a2e36125fbf35cf14",
+ "packages/artplayer-plugin-hls-control/src/sdk-events.js": "075df370e8856f5021ed7ffd314e29512ca801774616493f3b6c2230fb684295",
+ "test/hls-control.test.js": "739fbccdf143ce11b07dc88cc313c9eba985e9ef6ac2991bc046f56c9f6644aa",
+ "test/helpers/hls-control.js": "72dcc20c9af6155a401d882716f91cffe598ebf357ccef2d4654476d3f71a87d",
+ "test/browser/hls-control.spec.js": "3e3f841b1b84ef0d2622be3389af570641a5ccc443fc4dff0995036867ec745a"
+ },
+ "unit": {
+ "passed": 56,
+ "failed": 0,
+ "logSHA256": "4cb87f70949574a80fa5c5de8c2bb24496d8b9c696ecf508a9952422ebb138b5"
+ },
+ "artifactUnit": {
+ "passed": 83,
+ "failed": 0,
+ "logSHA256": "ec903b22266995c55a73e998899b03e42bb3067d34f9e0be8ff2aefb6f588c71",
+ "note": "Includes source/published plus 27 shared cases on three build outputs; candidate-specific cases run on source."
+ },
+ "artifacts": {
+ "artplayer-plugin-hls-control.js": {
+ "sha256": "e6ff00f5d992e80c75b4799d768484f3e5fd80191ebca7db8021fb44551f49f8",
+ "bytes": 5257,
+ "gzip9": 2536,
+ "publishedBytes": 3156,
+ "publishedGzip9": 1439
+ },
+ "artplayer-plugin-hls-control.legacy.js": {
+ "sha256": "acce81bb77ff7f92a9a523c892e398c5db1c4d85610142e858e9ec61575a1c3d",
+ "bytes": 5313,
+ "gzip9": 2564,
+ "publishedBytes": 3182,
+ "publishedGzip9": 1457
+ },
+ "artplayer-plugin-hls-control.mjs": {
+ "sha256": "c0c027376a19f1335d438efb80c92f62ac5987fddc7fbc9d1c2502146b8bfd39",
+ "bytes": 9594,
+ "gzip9": 3102,
+ "publishedBytes": 5015,
+ "publishedGzip9": 1647
+ }
+ },
+ "modern": {
+ "file": "refactor/.cache/hls03-final-modern.json",
+ "sha256": "dceeb50883a9aa00b66d884a10e27e73a26ec6c77f355723bea96a88f0469626",
+ "stats": {
+ "startTime": "2026-09-11T16:11:18.685Z",
+ "duration": 31057.55,
+ "expected": 35,
+ "skipped": 16,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "SDK MSE availability is explicit; unavailable hosts report failure and clean up",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 556,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "SDK MSE availability is explicit; unavailable hosts report failure and clean up",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1937,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "SDK MSE availability is explicit; unavailable hosts report failure and clean up",
+ "project": "webkit",
+ "status": "passed",
+ "retry": 0,
+ "duration": 877,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": false,
+ "mediaSource": "undefined",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "mediaError",
+ "details": "manifestIncompatibleCodecsError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "26.6",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: real decoding, manual quality, Auto and audio",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 6125,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: failed manifest and actual SDK teardown",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 741,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [
+ "Failed to load resource: the server responded with a status of 503 (Service Unavailable)"
+ ],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: source switch drops SDK audio topology",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 883,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: real decoding, manual quality, Auto and audio",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 6076,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: failed manifest and actual SDK teardown",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 820,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [
+ "Failed to load resource: the server responded with a status of 503 (Service Unavailable)"
+ ],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: source switch drops SDK audio topology",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1234,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: real Auto playback loses Auto label on update",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 816,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: SDK events synchronize Auto and audio without replacing menus",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1005,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: real decoding, manual quality, Auto and audio",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 6014,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: failed manifest and actual SDK teardown",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 803,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [
+ "Failed to load resource: the server responded with a status of 503 (Service Unavailable)"
+ ],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: source switch drops SDK audio topology",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 888,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: real decoding, manual quality, Auto and audio",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 6563,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: failed manifest and actual SDK teardown",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 786,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [
+ "Failed to load resource: the server responded with a status of 503 (Service Unavailable)"
+ ],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: source switch drops SDK audio topology",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 898,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: real Auto playback loses Auto label on update",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 839,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: SDK events synchronize Auto and audio without replacing menus",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1059,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: real decoding, manual quality, Auto and audio",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 4712,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: failed manifest and actual SDK teardown",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1122,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: source switch drops SDK audio topology",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1136,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: real decoding, manual quality, Auto and audio",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 4564,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: failed manifest and actual SDK teardown",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1171,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: source switch drops SDK audio topology",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 2301,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: real Auto playback loses Auto label on update",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1082,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: SDK events synchronize Auto and audio without replacing menus",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1345,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: real decoding, manual quality, Auto and audio",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 4643,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: failed manifest and actual SDK teardown",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1021,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: source switch drops SDK audio topology",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1282,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: real decoding, manual quality, Auto and audio",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 5673,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: failed manifest and actual SDK teardown",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1027,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: source switch drops SDK audio topology",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1156,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: real Auto playback loses Auto label on update",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1239,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: SDK events synchronize Auto and audio without replacing menus",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1256,
+ "annotations": [],
+ "inputs": "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: real decoding, manual quality, Auto and audio",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "published core / published HLS: failed manifest and actual SDK teardown",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "published core / published HLS: source switch drops SDK audio topology",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "published core / candidate HLS: real decoding, manual quality, Auto and audio",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "published core / candidate HLS: failed manifest and actual SDK teardown",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "published core / candidate HLS: source switch drops SDK audio topology",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "published core / published HLS: real Auto playback loses Auto label on update",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "published core / candidate HLS: SDK events synchronize Auto and audio without replacing menus",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / published HLS: real decoding, manual quality, Auto and audio",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / published HLS: failed manifest and actual SDK teardown",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / published HLS: source switch drops SDK audio topology",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / candidate HLS: real decoding, manual quality, Auto and audio",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / candidate HLS: failed manifest and actual SDK teardown",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / candidate HLS: source switch drops SDK audio topology",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / published HLS: real Auto playback loses Auto label on update",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / candidate HLS: SDK events synchronize Auto and audio without replacing menus",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "legacy": {
+ "file": "refactor/.cache/hls03-final-legacy.json",
+ "sha256": "3a262f1d43f689e0d07c65bbacb52980d44401a51f570a4d94a5d7305f46e3b9",
+ "stats": {
+ "startTime": "2026-09-11T16:13:10.437Z",
+ "duration": 30307.746,
+ "expected": 35,
+ "skipped": 16,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "SDK MSE availability is explicit; unavailable hosts report failure and clean up",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 557,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "SDK MSE availability is explicit; unavailable hosts report failure and clean up",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1768,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "SDK MSE availability is explicit; unavailable hosts report failure and clean up",
+ "project": "webkit",
+ "status": "passed",
+ "retry": 0,
+ "duration": 781,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": false,
+ "mediaSource": "undefined",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "mediaError",
+ "details": "manifestIncompatibleCodecsError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "26.6",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: real decoding, manual quality, Auto and audio",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 6007,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: failed manifest and actual SDK teardown",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 763,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [
+ "Failed to load resource: the server responded with a status of 503 (Service Unavailable)"
+ ],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: source switch drops SDK audio topology",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 879,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: real decoding, manual quality, Auto and audio",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 6069,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: failed manifest and actual SDK teardown",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 806,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [
+ "Failed to load resource: the server responded with a status of 503 (Service Unavailable)"
+ ],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: source switch drops SDK audio topology",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1283,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: real Auto playback loses Auto label on update",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 828,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: SDK events synchronize Auto and audio without replacing menus",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1002,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: real decoding, manual quality, Auto and audio",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 6091,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: failed manifest and actual SDK teardown",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 772,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [
+ "Failed to load resource: the server responded with a status of 503 (Service Unavailable)"
+ ],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: source switch drops SDK audio topology",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 891,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: real decoding, manual quality, Auto and audio",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 6330,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: failed manifest and actual SDK teardown",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 763,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [
+ "Failed to load resource: the server responded with a status of 503 (Service Unavailable)"
+ ],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: source switch drops SDK audio topology",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 853,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: real Auto playback loses Auto label on update",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 826,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: SDK events synchronize Auto and audio without replacing menus",
+ "project": "chromium",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1004,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "153.0.8010.12",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: real decoding, manual quality, Auto and audio",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 4411,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: failed manifest and actual SDK teardown",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1003,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: source switch drops SDK audio topology",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1094,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: real decoding, manual quality, Auto and audio",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 4677,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: failed manifest and actual SDK teardown",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1051,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: source switch drops SDK audio topology",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 2181,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: real Auto playback loses Auto label on update",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1047,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / candidate HLS: SDK events synchronize Auto and audio without replacing menus",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1288,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: real decoding, manual quality, Auto and audio",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 4603,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: failed manifest and actual SDK teardown",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 984,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: source switch drops SDK audio topology",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1101,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: real decoding, manual quality, Auto and audio",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 5423,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: failed manifest and actual SDK teardown",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1011,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [
+ {
+ "type": "networkError",
+ "details": "manifestLoadError",
+ "fatal": true
+ }
+ ],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: source switch drops SDK audio topology",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1227,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / published HLS: real Auto playback loses Auto label on update",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1195,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "candidate core / candidate HLS: SDK events synchronize Auto and audio without replacing menus",
+ "project": "firefox",
+ "status": "passed",
+ "retry": 0,
+ "duration": 1321,
+ "annotations": [],
+ "inputs": "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3",
+ "state": {
+ "supported": true,
+ "mediaSource": "function",
+ "managedMediaSource": "undefined",
+ "video": "",
+ "readyState": 0,
+ "networkState": 0,
+ "sdkErrors": [],
+ "sdkEvents": [
+ {
+ "name": "MANIFEST_PARSED"
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 0
+ },
+ {
+ "name": "LEVEL_SWITCHED",
+ "level": 1
+ },
+ {
+ "name": "AUDIO_TRACK_SWITCHED",
+ "id": 1
+ }
+ ],
+ "levels": [],
+ "tracks": []
+ },
+ "browser": "155.0",
+ "manifest": "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469",
+ "errors": [],
+ "consoleErrors": [],
+ "failedRequests": []
+ },
+ {
+ "title": "published core / published HLS: real decoding, manual quality, Auto and audio",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "published core / published HLS: failed manifest and actual SDK teardown",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "published core / published HLS: source switch drops SDK audio topology",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "published core / candidate HLS: real decoding, manual quality, Auto and audio",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "published core / candidate HLS: failed manifest and actual SDK teardown",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "published core / candidate HLS: source switch drops SDK audio topology",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "published core / published HLS: real Auto playback loses Auto label on update",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "published core / candidate HLS: SDK events synchronize Auto and audio without replacing menus",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / published HLS: real decoding, manual quality, Auto and audio",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / published HLS: failed manifest and actual SDK teardown",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / published HLS: source switch drops SDK audio topology",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / candidate HLS: real decoding, manual quality, Auto and audio",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / candidate HLS: failed manifest and actual SDK teardown",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / candidate HLS: source switch drops SDK audio topology",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / published HLS: real Auto playback loses Auto label on update",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ },
+ {
+ "title": "candidate core / candidate HLS: SDK events synchronize Auto and audio without replacing menus",
+ "project": "webkit",
+ "status": "skipped",
+ "retry": 0,
+ "duration": 0,
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Bundled Windows WebKit exposes neither MediaSource nor ManagedMediaSource; this is an unverified playback environment, not compatibility approval",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\hls-control.spec.js",
+ "line": 119,
+ "column": 8
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "shared": {
+ "fbf81cdc68a4694c6040ca64954a8249ec6662069499cd0f2b58e40015f3902c": {
+ "sdk": {
+ "name": "hls.js",
+ "version": "1.5.17",
+ "tarball": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.17.tgz",
+ "integrity": "sha512-wA66nnYFvQa1o4DO/BFgLNRKnBTVXpNeldGRBJ2Y0SvFtdwvFKCbqa9zhHoZLoxHhZ+jYsj3aIBkWQQCPNOhMw==",
+ "sha256": "8f9e60001992f59dbfb1dbc397e145743c41bb6dde233ec8a7974013c8a031fe",
+ "files": {
+ "package/package.json": "f2172b9942a23ffb46edd4af815cf1ea161db088e7f63dc518d30e55c95d6ae9",
+ "package/LICENSE": "ca8773cf798c7ed997d4dd7c8e23c348699f8d5b7462636694cc14de6cda12db",
+ "package/dist/hls.min.js": "484054e8cd03d3f6d1781fb7f402bdc318d8a4c527f933a95c624e27cc9a9470",
+ "package/src/hls.ts": "b3d3f797669e89bc8d05d2de2aae243ae110811d6738948f173d789cc1ac1f2d",
+ "package/src/controller/audio-track-controller.ts": "ff34f3cb29b66aa5fcba87758bb20d129b052a053808c150ce779e0ceb1e4626"
+ }
+ },
+ "plugin": {
+ "name": "artplayer-plugin-hls-control",
+ "version": "1.1.0",
+ "metadataUrl": "https://registry.npmjs.org/artplayer-plugin-hls-control/1.1.0",
+ "tarball": "https://registry.npmjs.org/artplayer-plugin-hls-control/-/artplayer-plugin-hls-control-1.1.0.tgz",
+ "integrity": "sha512-lWN5oUOWDorWFHdzKcmy9Z8eHcf5f5EcUGDrxBsZBX7aEL5dGRlHPQ5+wqQEizr38D9PYPfTI/nCyun2yFopEw==",
+ "sha256": "598c354d78d3cb77965136fa61dfc0f94cdea6906a91a30781c1acef2b0c3e8b",
+ "archiveBytes": 3192,
+ "registryGitHead": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
+ "files": {
+ "package/README.md": "42bd00dca467b581f6b3e8399ad1a416d68edcc52d42e6cb51b66f1749694556",
+ "package/dist/artplayer-plugin-hls-control.js": "7a6ce18a0b16f61f6149e0ea77efbb6e0503f997d39afe84271b59e366759e6d",
+ "package/dist/artplayer-plugin-hls-control.legacy.js": "893a3de27f9ed02b4b61f03732aec03bfcaf50d02c9beb15cc8a5767ef9121a0",
+ "package/dist/artplayer-plugin-hls-control.mjs": "85c4de4940e031d835b4de3ccf68af449d10aec4f630c89ecaf62f5503348931",
+ "package/package.json": "710ec2a27dc71f7254e7a1d2eb7cff3f56a6e23811047ac97356f1b18c8ac8b5",
+ "package/types/artplayer-plugin-hls-control.d.ts": "7dd8fdf556932e9cf6140b7513c05155cad6d486e2484bf06c6d48f6e823832a"
+ },
+ "manifest": {
+ "name": "artplayer-plugin-hls-control",
+ "version": "1.1.0",
+ "description": "Hls control plugin for ArtPlayer",
+ "author": "Harvey Zhao ",
+ "license": "MIT",
+ "homepage": "https://artplayer.org",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/zhw2590582/ArtPlayer.git"
+ },
+ "bugs": {
+ "url": "https://github.com/zhw2590582/ArtPlayer/issues"
+ },
+ "keywords": [
+ "html5",
+ "video",
+ "player"
+ ],
+ "exports": {
+ ".": {
+ "types": "./types/artplayer-plugin-hls-control.d.ts",
+ "import": "./dist/artplayer-plugin-hls-control.mjs",
+ "require": "./dist/artplayer-plugin-hls-control.js"
+ },
+ "./legacy": {
+ "types": "./types/artplayer-plugin-hls-control.d.ts",
+ "import": "./dist/artplayer-plugin-hls-control.legacy.js",
+ "require": "./dist/artplayer-plugin-hls-control.legacy.js"
+ }
+ },
+ "main": "./dist/artplayer-plugin-hls-control.js",
+ "module": "./dist/artplayer-plugin-hls-control.mjs",
+ "types": "./types/artplayer-plugin-hls-control.d.ts",
+ "legacy": "./dist/artplayer-plugin-hls-control.legacy.js",
+ "browserslist": "last 1 Chrome version"
+ }
+ },
+ "sourceSHA256": "e6ff00f5d992e80c75b4799d768484f3e5fd80191ebca7db8021fb44551f49f8",
+ "candidate": "D:\\github\\ArtPlayer\\packages\\artplayer-plugin-hls-control\\dist\\artplayer-plugin-hls-control.js",
+ "media": {
+ "kind": "generated-hls-media",
+ "version": "ffmpeg version 2025-11-12-git-6cdd2cbe32-full_build-www.gyan.dev Copyright (c) 2000-2025 the FFmpeg developers",
+ "duration": 12,
+ "commands": [
+ [
+ "-hide_banner",
+ "-loglevel",
+ "error",
+ "-n",
+ "-f",
+ "lavfi",
+ "-i",
+ "testsrc2=size=160x90:rate=24",
+ "-t",
+ "12",
+ "-an",
+ "-c:v",
+ "libx264",
+ "-pix_fmt",
+ "yuv420p",
+ "-profile:v",
+ "baseline",
+ "-level:v",
+ "3.0",
+ "-g",
+ "48",
+ "-keyint_min",
+ "48",
+ "-sc_threshold",
+ "0",
+ "-crf",
+ "28",
+ "-f",
+ "hls",
+ "-hls_time",
+ "2",
+ "-hls_playlist_type",
+ "vod",
+ "-hls_segment_filename",
+ "low-%02d.mpegts",
+ "low.m3u8"
+ ],
+ [
+ "-hide_banner",
+ "-loglevel",
+ "error",
+ "-n",
+ "-f",
+ "lavfi",
+ "-i",
+ "testsrc2=size=320x180:rate=24",
+ "-t",
+ "12",
+ "-an",
+ "-c:v",
+ "libx264",
+ "-pix_fmt",
+ "yuv420p",
+ "-profile:v",
+ "baseline",
+ "-level:v",
+ "3.0",
+ "-g",
+ "48",
+ "-keyint_min",
+ "48",
+ "-sc_threshold",
+ "0",
+ "-crf",
+ "28",
+ "-f",
+ "hls",
+ "-hls_time",
+ "2",
+ "-hls_playlist_type",
+ "vod",
+ "-hls_segment_filename",
+ "high-%02d.mpegts",
+ "high.m3u8"
+ ],
+ [
+ "-hide_banner",
+ "-loglevel",
+ "error",
+ "-n",
+ "-f",
+ "lavfi",
+ "-i",
+ "sine=frequency=440:sample_rate=48000",
+ "-t",
+ "12",
+ "-vn",
+ "-c:a",
+ "aac",
+ "-b:a",
+ "48k",
+ "-ac",
+ "2",
+ "-f",
+ "hls",
+ "-hls_time",
+ "2",
+ "-hls_playlist_type",
+ "vod",
+ "-hls_segment_filename",
+ "english-%02d.mpegts",
+ "english.m3u8"
+ ],
+ [
+ "-hide_banner",
+ "-loglevel",
+ "error",
+ "-n",
+ "-f",
+ "lavfi",
+ "-i",
+ "sine=frequency=880:sample_rate=48000",
+ "-t",
+ "12",
+ "-vn",
+ "-c:a",
+ "aac",
+ "-b:a",
+ "48k",
+ "-ac",
+ "2",
+ "-f",
+ "hls",
+ "-hls_time",
+ "2",
+ "-hls_playlist_type",
+ "vod",
+ "-hls_segment_filename",
+ "french-%02d.mpegts",
+ "french.m3u8"
+ ]
+ ],
+ "files": {
+ "english-00.mpegts": {
+ "sha256": "cc599f7c270385e85c28f710a34c5bd90b09d4d27adb3b1e6af3de65584b8207",
+ "bytes": 13912
+ },
+ "english-01.mpegts": {
+ "sha256": "01b17c28defc8a8ebf4978c5236eb759d80521a1cb840898095c35dac420cec5",
+ "bytes": 14100
+ },
+ "english-02.mpegts": {
+ "sha256": "90751b8b5abedd6b2f2e4eb83ccd609098ec7f2901163aaebf977cb9a11c46eb",
+ "bytes": 14100
+ },
+ "english-03.mpegts": {
+ "sha256": "9b1d86b7b261df66dbdace4ef1e846796cd710ffb72b22fc69532c1fe7d1c55e",
+ "bytes": 13912
+ },
+ "english-04.mpegts": {
+ "sha256": "dbfddd4f997942dd0b912f4c8bdbc5c11ee2a3fc51ac67df48d3aed2756715b9",
+ "bytes": 14100
+ },
+ "english-05.mpegts": {
+ "sha256": "9b53790d4f50f172852b7d4ff51bdd4bedd39e4c7a1efbfe734edf7b9a68a533",
+ "bytes": 14100
+ },
+ "english-06.mpegts": {
+ "sha256": "50baaa5e9fe7ca49b22c4d0817c44103b3edc763aee7f1b17d22f796d4189d4a",
+ "bytes": 752
+ },
+ "english.m3u8": {
+ "sha256": "5aae9d117d458f65f7febae68555ea4e48b2055b4cfb123fb919e385a1edda21",
+ "bytes": 365
+ },
+ "french-00.mpegts": {
+ "sha256": "7278acd094bc7caeeeea424a2c11ac4285fc45a8becb6c8ec55068f295ae35cd",
+ "bytes": 13912
+ },
+ "french-01.mpegts": {
+ "sha256": "e9464d109e36a53d7f151442b544d47ce79324bbaa805e0a98f171621c9c7a13",
+ "bytes": 14100
+ },
+ "french-02.mpegts": {
+ "sha256": "3ac48cab6aac942950b59f79ac6bd950dcae425f14821b83b1b5bb72d6815eb0",
+ "bytes": 14100
+ },
+ "french-03.mpegts": {
+ "sha256": "906f2d8b1f29bb7d57240b145563bab975d8f7dfff78cebd1505b6d2a3feb43b",
+ "bytes": 13912
+ },
+ "french-04.mpegts": {
+ "sha256": "cbf82876118d41af254bdef51f6cd08d842e8c6edf53250667fad2b8088e3601",
+ "bytes": 14100
+ },
+ "french-05.mpegts": {
+ "sha256": "a91e564ec54d845837217c45b8e25b08fe208f2b5f97562f513baabd515427d9",
+ "bytes": 14100
+ },
+ "french-06.mpegts": {
+ "sha256": "2c8edc11b06bacf1e62c4db67594ce187aa93e0241012011ebcf587b64ccff46",
+ "bytes": 752
+ },
+ "french.m3u8": {
+ "sha256": "6f7fb3eacb944597fa3105cb6d708eac3a8dc413e1706a11b6fb0e631214634e",
+ "bytes": 358
+ },
+ "high-00.mpegts": {
+ "sha256": "c5a8faac85b0594818255117696dce43fda6b363df40e2f3ffc453bd236cc833",
+ "bytes": 45308
+ },
+ "high-01.mpegts": {
+ "sha256": "5714640c3f17d86c56036453dc17d45f871af63b461318c43527af3482f8661c",
+ "bytes": 52264
+ },
+ "high-02.mpegts": {
+ "sha256": "fea1cbfb199bc867a322c7a646d43594a1d0d1d23fa0bcb6e770f2669b489440",
+ "bytes": 48128
+ },
+ "high-03.mpegts": {
+ "sha256": "4a74a28e7ad6bc626358e32365a0b07df1da82c92753e28032983e22404958b5",
+ "bytes": 54144
+ },
+ "high-04.mpegts": {
+ "sha256": "d04889b3b996d3e79c12bc297f9577690d3ae4f4172cdc0484b584954dab789c",
+ "bytes": 47376
+ },
+ "high-05.mpegts": {
+ "sha256": "36ee0c64b8c3637d0e2282c611e9250650ac493440095fc2efbe9de1c54add15",
+ "bytes": 48128
+ },
+ "high.m3u8": {
+ "sha256": "672c0e8b99242be05b16fc747a00715b2c25579b1ecc1623c0579fa34f0cb11d",
+ "bytes": 311
+ },
+ "low-00.mpegts": {
+ "sha256": "cbd62f5f4641cbe188a15640139bea393b5927fce600ff7513ef9ade9b33fb62",
+ "bytes": 28952
+ },
+ "low-01.mpegts": {
+ "sha256": "f7821fba38c63f5d80daef21cdd3430880bdc9fd5f5adfa939ed6481106c2507",
+ "bytes": 32900
+ },
+ "low-02.mpegts": {
+ "sha256": "3dc93f9f0fd065678b01fc6c568c12350e9ab0b324d1e0fa0e9f77787da7e7d2",
+ "bytes": 30268
+ },
+ "low-03.mpegts": {
+ "sha256": "d94c8913f7fde98161550041287529b90a58d537d6083d4382540c5fce0b430b",
+ "bytes": 32336
+ },
+ "low-04.mpegts": {
+ "sha256": "11596c59b547c2ff23bbba1f70e15f061a3e87bbdc1f4f59daa9b5279a6003af",
+ "bytes": 29704
+ },
+ "low-05.mpegts": {
+ "sha256": "5c6ec25a69fdad73a23cfbc34f0f3c6d8abe44592650c92fa9110a38d6bd9e45",
+ "bytes": 29516
+ },
+ "low.m3u8": {
+ "sha256": "404a8d868ed2af3598269aee89caba0563c2805dac223fe4c00c9a9eda73554f",
+ "bytes": 305
+ },
+ "master.m3u8": {
+ "sha256": "f226bd38f3c76714ada2b5a6866ad1cb39b041023e4a18d971d175f26f917e38",
+ "bytes": 470
+ },
+ "video-only.m3u8": {
+ "sha256": "3d7bb9069f6218191dc65745389bf1ab0ba6d5ed66cbd14e2cf30211913da3fc",
+ "bytes": 108
+ }
+ }
+ },
+ "limitations": [
+ "SDK worker disabled",
+ "local deterministic media",
+ "not an isolated installed package",
+ "no physical device certification"
+ ]
+ },
+ "16d7794cfdad4a0de3193e43390eed42d5db720c697df7694c7f7a0423ecb469": {
+ "kind": "artplayer-browser-test",
+ "resources": {
+ "/candidate/artplayer.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer\\src\\index.ts"
+ },
+ "sha256": "32f3dc22e296dcc569dc1227f0b0cad92bd0dd7e74d747e19759ac5997443e92",
+ "bytes": 376744
+ },
+ "/compiled/artplayer.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer\\src\\index.ts"
+ },
+ "sha256": "32f3dc22e296dcc569dc1227f0b0cad92bd0dd7e74d747e19759ac5997443e92",
+ "bytes": 376744,
+ "aliasOf": "/candidate/artplayer.js"
+ },
+ "/uncompiled/artplayer/index.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer\\src\\index.ts"
+ },
+ "sha256": "32f3dc22e296dcc569dc1227f0b0cad92bd0dd7e74d747e19759ac5997443e92",
+ "bytes": 376744,
+ "aliasOf": "/candidate/artplayer.js"
+ },
+ "/candidate/artplayer-plugin-chapter.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts"
+ },
+ "sha256": "3e8d0b7e7a90b5dbadf4cb108fd770d294f8a8a92ea089c41d0766c98bbc8230",
+ "bytes": 8433
+ },
+ "/compiled/artplayer-plugin-chapter.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts"
+ },
+ "sha256": "3e8d0b7e7a90b5dbadf4cb108fd770d294f8a8a92ea089c41d0766c98bbc8230",
+ "bytes": 8433,
+ "aliasOf": "/candidate/artplayer-plugin-chapter.js"
+ },
+ "/uncompiled/artplayer-plugin-chapter/index.js": {
+ "source": {
+ "kind": "workspace-build",
+ "entry": "packages\\artplayer-plugin-chapter\\src\\index.ts"
+ },
+ "sha256": "3e8d0b7e7a90b5dbadf4cb108fd770d294f8a8a92ea089c41d0766c98bbc8230",
+ "bytes": 8433,
+ "aliasOf": "/candidate/artplayer-plugin-chapter.js"
+ },
+ "/published/artplayer.js": {
+ "source": {
+ "kind": "npm-release",
+ "version": "5.4.0",
+ "integrity": "sha512-2B+plbx8N2yNsjK4nJU3+EOG8TULm1LRZk/QPkWRAMEX2Ee/MSnZG/WJYz8kcoZxZuLKcQ3uXifqLuPxZOH29A==",
+ "member": "package/dist/artplayer.js"
+ },
+ "sha256": "f1db1fa3406aa6d41baa7c554749429a8e79f99d56a794bde23345eb9c07573f",
+ "bytes": 134037
+ },
+ "/published/artplayer-plugin-chapter.js": {
+ "source": {
+ "kind": "npm-release",
+ "version": "1.1.0",
+ "integrity": "sha512-ji66Lr2K2S+OjFMuMby3yq+pefWlOC51Sbpop/DIr2N6w3F0smrF+YZbshN1Dy8/Tx1TYpT7N6WkkZWy5Jix1w==",
+ "member": "package/dist/artplayer-plugin-chapter.js"
+ },
+ "sha256": "c2aaee11bf1db765433b01d9f8dd3978e41fa4d39655db00ebd03fc0be3e35e0",
+ "bytes": 4973
+ },
+ "/test/pattern.mp4": {
+ "source": {
+ "kind": "generated-media",
+ "file": "test/browser/media/pattern.mp4"
+ },
+ "sha256": "0ad1d7ad286aec6e0fa9e27fc1e487e7277df2e603e30040fb942365763d0233",
+ "bytes": 251538
+ },
+ "/test/pattern.webm": {
+ "source": {
+ "kind": "generated-media",
+ "file": "test/browser/media/pattern.webm"
+ },
+ "sha256": "9d3c2b3dec9581a599de2d6732992ef7846790e0d1368e6e131bd339b39e984f",
+ "bytes": 162073
+ },
+ "/test/declaration-cues.vtt": {
+ "source": {
+ "kind": "test-subtitles",
+ "file": "test/browser/media/declaration-cues.vtt"
+ },
+ "sha256": "5d149d4e641ce41cbb83376835c03daf1fb89f3b9f568fa59e064d51e2b57704",
+ "bytes": 54
+ },
+ "/test/thumbnail-grid.svg": {
+ "source": {
+ "kind": "test-thumbnail-grid",
+ "file": "test/browser/media/thumbnail-grid.svg"
+ },
+ "sha256": "bb3f1ec9895d37def77ca4b192d3cd73577a2668558cc99de1588f511d65d3e6",
+ "bytes": 14969
+ },
+ "/test/legacy-safe-area.js": {
+ "source": {
+ "kind": "frozen-own-source",
+ "file": "test/helpers/legacy-safe-area.js",
+ "commit": "ccf77c4e"
+ },
+ "sha256": "ce0368618ef6363b78271b8bcc7973abd0478e797cc464ddd912929e6dfa501a",
+ "bytes": 795
+ },
+ "/assets/sample/video.mp4": {
+ "source": {
+ "kind": "docs-media",
+ "file": "docs/assets/sample/video.mp4"
+ },
+ "sha256": "30a2de667ba267b6bc605edcee1c4bd728c048ed76f500ea0e73d1f1a6f8f980",
+ "bytes": 5818706
+ },
+ "/test/performance.js": {
+ "source": {
+ "kind": "adapted-baseline-fixture",
+ "file": "refactor/fixtures/performance.js"
+ },
+ "sha256": "2318f39bdb5f3a3348d755cb4335907ae4adc944be3a435839c1bba201e81500",
+ "bytes": 8994
+ }
+ }
+ },
+ "225d40ee872edee7f427b7f1d7af2f04776848d01d2843567e179492b4983db3": {
+ "sdk": {
+ "name": "hls.js",
+ "version": "1.5.17",
+ "tarball": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.17.tgz",
+ "integrity": "sha512-wA66nnYFvQa1o4DO/BFgLNRKnBTVXpNeldGRBJ2Y0SvFtdwvFKCbqa9zhHoZLoxHhZ+jYsj3aIBkWQQCPNOhMw==",
+ "sha256": "8f9e60001992f59dbfb1dbc397e145743c41bb6dde233ec8a7974013c8a031fe",
+ "files": {
+ "package/package.json": "f2172b9942a23ffb46edd4af815cf1ea161db088e7f63dc518d30e55c95d6ae9",
+ "package/LICENSE": "ca8773cf798c7ed997d4dd7c8e23c348699f8d5b7462636694cc14de6cda12db",
+ "package/dist/hls.min.js": "484054e8cd03d3f6d1781fb7f402bdc318d8a4c527f933a95c624e27cc9a9470",
+ "package/src/hls.ts": "b3d3f797669e89bc8d05d2de2aae243ae110811d6738948f173d789cc1ac1f2d",
+ "package/src/controller/audio-track-controller.ts": "ff34f3cb29b66aa5fcba87758bb20d129b052a053808c150ce779e0ceb1e4626"
+ }
+ },
+ "plugin": {
+ "name": "artplayer-plugin-hls-control",
+ "version": "1.1.0",
+ "metadataUrl": "https://registry.npmjs.org/artplayer-plugin-hls-control/1.1.0",
+ "tarball": "https://registry.npmjs.org/artplayer-plugin-hls-control/-/artplayer-plugin-hls-control-1.1.0.tgz",
+ "integrity": "sha512-lWN5oUOWDorWFHdzKcmy9Z8eHcf5f5EcUGDrxBsZBX7aEL5dGRlHPQ5+wqQEizr38D9PYPfTI/nCyun2yFopEw==",
+ "sha256": "598c354d78d3cb77965136fa61dfc0f94cdea6906a91a30781c1acef2b0c3e8b",
+ "archiveBytes": 3192,
+ "registryGitHead": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
+ "files": {
+ "package/README.md": "42bd00dca467b581f6b3e8399ad1a416d68edcc52d42e6cb51b66f1749694556",
+ "package/dist/artplayer-plugin-hls-control.js": "7a6ce18a0b16f61f6149e0ea77efbb6e0503f997d39afe84271b59e366759e6d",
+ "package/dist/artplayer-plugin-hls-control.legacy.js": "893a3de27f9ed02b4b61f03732aec03bfcaf50d02c9beb15cc8a5767ef9121a0",
+ "package/dist/artplayer-plugin-hls-control.mjs": "85c4de4940e031d835b4de3ccf68af449d10aec4f630c89ecaf62f5503348931",
+ "package/package.json": "710ec2a27dc71f7254e7a1d2eb7cff3f56a6e23811047ac97356f1b18c8ac8b5",
+ "package/types/artplayer-plugin-hls-control.d.ts": "7dd8fdf556932e9cf6140b7513c05155cad6d486e2484bf06c6d48f6e823832a"
+ },
+ "manifest": {
+ "name": "artplayer-plugin-hls-control",
+ "version": "1.1.0",
+ "description": "Hls control plugin for ArtPlayer",
+ "author": "Harvey Zhao ",
+ "license": "MIT",
+ "homepage": "https://artplayer.org",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/zhw2590582/ArtPlayer.git"
+ },
+ "bugs": {
+ "url": "https://github.com/zhw2590582/ArtPlayer/issues"
+ },
+ "keywords": [
+ "html5",
+ "video",
+ "player"
+ ],
+ "exports": {
+ ".": {
+ "types": "./types/artplayer-plugin-hls-control.d.ts",
+ "import": "./dist/artplayer-plugin-hls-control.mjs",
+ "require": "./dist/artplayer-plugin-hls-control.js"
+ },
+ "./legacy": {
+ "types": "./types/artplayer-plugin-hls-control.d.ts",
+ "import": "./dist/artplayer-plugin-hls-control.legacy.js",
+ "require": "./dist/artplayer-plugin-hls-control.legacy.js"
+ }
+ },
+ "main": "./dist/artplayer-plugin-hls-control.js",
+ "module": "./dist/artplayer-plugin-hls-control.mjs",
+ "types": "./types/artplayer-plugin-hls-control.d.ts",
+ "legacy": "./dist/artplayer-plugin-hls-control.legacy.js",
+ "browserslist": "last 1 Chrome version"
+ }
+ },
+ "sourceSHA256": "acce81bb77ff7f92a9a523c892e398c5db1c4d85610142e858e9ec61575a1c3d",
+ "candidate": "D:\\github\\ArtPlayer\\packages\\artplayer-plugin-hls-control\\dist\\artplayer-plugin-hls-control.legacy.js",
+ "media": {
+ "kind": "generated-hls-media",
+ "version": "ffmpeg version 2025-11-12-git-6cdd2cbe32-full_build-www.gyan.dev Copyright (c) 2000-2025 the FFmpeg developers",
+ "duration": 12,
+ "commands": [
+ [
+ "-hide_banner",
+ "-loglevel",
+ "error",
+ "-n",
+ "-f",
+ "lavfi",
+ "-i",
+ "testsrc2=size=160x90:rate=24",
+ "-t",
+ "12",
+ "-an",
+ "-c:v",
+ "libx264",
+ "-pix_fmt",
+ "yuv420p",
+ "-profile:v",
+ "baseline",
+ "-level:v",
+ "3.0",
+ "-g",
+ "48",
+ "-keyint_min",
+ "48",
+ "-sc_threshold",
+ "0",
+ "-crf",
+ "28",
+ "-f",
+ "hls",
+ "-hls_time",
+ "2",
+ "-hls_playlist_type",
+ "vod",
+ "-hls_segment_filename",
+ "low-%02d.mpegts",
+ "low.m3u8"
+ ],
+ [
+ "-hide_banner",
+ "-loglevel",
+ "error",
+ "-n",
+ "-f",
+ "lavfi",
+ "-i",
+ "testsrc2=size=320x180:rate=24",
+ "-t",
+ "12",
+ "-an",
+ "-c:v",
+ "libx264",
+ "-pix_fmt",
+ "yuv420p",
+ "-profile:v",
+ "baseline",
+ "-level:v",
+ "3.0",
+ "-g",
+ "48",
+ "-keyint_min",
+ "48",
+ "-sc_threshold",
+ "0",
+ "-crf",
+ "28",
+ "-f",
+ "hls",
+ "-hls_time",
+ "2",
+ "-hls_playlist_type",
+ "vod",
+ "-hls_segment_filename",
+ "high-%02d.mpegts",
+ "high.m3u8"
+ ],
+ [
+ "-hide_banner",
+ "-loglevel",
+ "error",
+ "-n",
+ "-f",
+ "lavfi",
+ "-i",
+ "sine=frequency=440:sample_rate=48000",
+ "-t",
+ "12",
+ "-vn",
+ "-c:a",
+ "aac",
+ "-b:a",
+ "48k",
+ "-ac",
+ "2",
+ "-f",
+ "hls",
+ "-hls_time",
+ "2",
+ "-hls_playlist_type",
+ "vod",
+ "-hls_segment_filename",
+ "english-%02d.mpegts",
+ "english.m3u8"
+ ],
+ [
+ "-hide_banner",
+ "-loglevel",
+ "error",
+ "-n",
+ "-f",
+ "lavfi",
+ "-i",
+ "sine=frequency=880:sample_rate=48000",
+ "-t",
+ "12",
+ "-vn",
+ "-c:a",
+ "aac",
+ "-b:a",
+ "48k",
+ "-ac",
+ "2",
+ "-f",
+ "hls",
+ "-hls_time",
+ "2",
+ "-hls_playlist_type",
+ "vod",
+ "-hls_segment_filename",
+ "french-%02d.mpegts",
+ "french.m3u8"
+ ]
+ ],
+ "files": {
+ "english-00.mpegts": {
+ "sha256": "cc599f7c270385e85c28f710a34c5bd90b09d4d27adb3b1e6af3de65584b8207",
+ "bytes": 13912
+ },
+ "english-01.mpegts": {
+ "sha256": "01b17c28defc8a8ebf4978c5236eb759d80521a1cb840898095c35dac420cec5",
+ "bytes": 14100
+ },
+ "english-02.mpegts": {
+ "sha256": "90751b8b5abedd6b2f2e4eb83ccd609098ec7f2901163aaebf977cb9a11c46eb",
+ "bytes": 14100
+ },
+ "english-03.mpegts": {
+ "sha256": "9b1d86b7b261df66dbdace4ef1e846796cd710ffb72b22fc69532c1fe7d1c55e",
+ "bytes": 13912
+ },
+ "english-04.mpegts": {
+ "sha256": "dbfddd4f997942dd0b912f4c8bdbc5c11ee2a3fc51ac67df48d3aed2756715b9",
+ "bytes": 14100
+ },
+ "english-05.mpegts": {
+ "sha256": "9b53790d4f50f172852b7d4ff51bdd4bedd39e4c7a1efbfe734edf7b9a68a533",
+ "bytes": 14100
+ },
+ "english-06.mpegts": {
+ "sha256": "50baaa5e9fe7ca49b22c4d0817c44103b3edc763aee7f1b17d22f796d4189d4a",
+ "bytes": 752
+ },
+ "english.m3u8": {
+ "sha256": "5aae9d117d458f65f7febae68555ea4e48b2055b4cfb123fb919e385a1edda21",
+ "bytes": 365
+ },
+ "french-00.mpegts": {
+ "sha256": "7278acd094bc7caeeeea424a2c11ac4285fc45a8becb6c8ec55068f295ae35cd",
+ "bytes": 13912
+ },
+ "french-01.mpegts": {
+ "sha256": "e9464d109e36a53d7f151442b544d47ce79324bbaa805e0a98f171621c9c7a13",
+ "bytes": 14100
+ },
+ "french-02.mpegts": {
+ "sha256": "3ac48cab6aac942950b59f79ac6bd950dcae425f14821b83b1b5bb72d6815eb0",
+ "bytes": 14100
+ },
+ "french-03.mpegts": {
+ "sha256": "906f2d8b1f29bb7d57240b145563bab975d8f7dfff78cebd1505b6d2a3feb43b",
+ "bytes": 13912
+ },
+ "french-04.mpegts": {
+ "sha256": "cbf82876118d41af254bdef51f6cd08d842e8c6edf53250667fad2b8088e3601",
+ "bytes": 14100
+ },
+ "french-05.mpegts": {
+ "sha256": "a91e564ec54d845837217c45b8e25b08fe208f2b5f97562f513baabd515427d9",
+ "bytes": 14100
+ },
+ "french-06.mpegts": {
+ "sha256": "2c8edc11b06bacf1e62c4db67594ce187aa93e0241012011ebcf587b64ccff46",
+ "bytes": 752
+ },
+ "french.m3u8": {
+ "sha256": "6f7fb3eacb944597fa3105cb6d708eac3a8dc413e1706a11b6fb0e631214634e",
+ "bytes": 358
+ },
+ "high-00.mpegts": {
+ "sha256": "c5a8faac85b0594818255117696dce43fda6b363df40e2f3ffc453bd236cc833",
+ "bytes": 45308
+ },
+ "high-01.mpegts": {
+ "sha256": "5714640c3f17d86c56036453dc17d45f871af63b461318c43527af3482f8661c",
+ "bytes": 52264
+ },
+ "high-02.mpegts": {
+ "sha256": "fea1cbfb199bc867a322c7a646d43594a1d0d1d23fa0bcb6e770f2669b489440",
+ "bytes": 48128
+ },
+ "high-03.mpegts": {
+ "sha256": "4a74a28e7ad6bc626358e32365a0b07df1da82c92753e28032983e22404958b5",
+ "bytes": 54144
+ },
+ "high-04.mpegts": {
+ "sha256": "d04889b3b996d3e79c12bc297f9577690d3ae4f4172cdc0484b584954dab789c",
+ "bytes": 47376
+ },
+ "high-05.mpegts": {
+ "sha256": "36ee0c64b8c3637d0e2282c611e9250650ac493440095fc2efbe9de1c54add15",
+ "bytes": 48128
+ },
+ "high.m3u8": {
+ "sha256": "672c0e8b99242be05b16fc747a00715b2c25579b1ecc1623c0579fa34f0cb11d",
+ "bytes": 311
+ },
+ "low-00.mpegts": {
+ "sha256": "cbd62f5f4641cbe188a15640139bea393b5927fce600ff7513ef9ade9b33fb62",
+ "bytes": 28952
+ },
+ "low-01.mpegts": {
+ "sha256": "f7821fba38c63f5d80daef21cdd3430880bdc9fd5f5adfa939ed6481106c2507",
+ "bytes": 32900
+ },
+ "low-02.mpegts": {
+ "sha256": "3dc93f9f0fd065678b01fc6c568c12350e9ab0b324d1e0fa0e9f77787da7e7d2",
+ "bytes": 30268
+ },
+ "low-03.mpegts": {
+ "sha256": "d94c8913f7fde98161550041287529b90a58d537d6083d4382540c5fce0b430b",
+ "bytes": 32336
+ },
+ "low-04.mpegts": {
+ "sha256": "11596c59b547c2ff23bbba1f70e15f061a3e87bbdc1f4f59daa9b5279a6003af",
+ "bytes": 29704
+ },
+ "low-05.mpegts": {
+ "sha256": "5c6ec25a69fdad73a23cfbc34f0f3c6d8abe44592650c92fa9110a38d6bd9e45",
+ "bytes": 29516
+ },
+ "low.m3u8": {
+ "sha256": "404a8d868ed2af3598269aee89caba0563c2805dac223fe4c00c9a9eda73554f",
+ "bytes": 305
+ },
+ "master.m3u8": {
+ "sha256": "f226bd38f3c76714ada2b5a6866ad1cb39b041023e4a18d971d175f26f917e38",
+ "bytes": 470
+ },
+ "video-only.m3u8": {
+ "sha256": "3d7bb9069f6218191dc65745389bf1ab0ba6d5ed66cbd14e2cf30211913da3fc",
+ "bytes": 108
+ }
+ }
+ },
+ "limitations": [
+ "SDK worker disabled",
+ "local deterministic media",
+ "not an isolated installed package",
+ "no physical device certification"
+ ]
+ }
+ },
+ "scope": "Working-tree build artifacts with source/published core, not isolated npm installation. Windows WebKit playback unavailable; 16 skipped per format. Full SDK/device range remains open."
+}
diff --git a/refactor/changes/2026-09-12-PKG-HLS-03-modules.md b/refactor/changes/2026-09-12-PKG-HLS-03-modules.md
new file mode 100644
index 000000000..275da219f
--- /dev/null
+++ b/refactor/changes/2026-09-12-PKG-HLS-03-modules.md
@@ -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,迁移已拆分模块与兼容公开类型,同样独立提交。
diff --git a/refactor/hls-validation.md b/refactor/hls-validation.md
index 876b0a071..fe8cf513f 100644
--- a/refactor/hls-validation.md
+++ b/refactor/hls-validation.md
@@ -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)。
diff --git a/refactor/plan.md b/refactor/plan.md
index bcae94f22..a2105d223 100644
--- a/refactor/plan.md
+++ b/refactor/plan.md
@@ -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
核对包契约与历史用法 | BASE-05 | quality/audio 配置、getName、update、控件名及 Hls 实例绑定 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-HLS-02 | artplayer-plugin-hls-control
建立特有行为与错误测试 | PKG-HLS-01, ENG-03, ENG-05 | Auto/手动实际 level、音轨、去重/过滤和拓扑变化 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
-| PKG-HLS-03 | artplayer-plugin-hls-control
整理内部职责与资源 | PKG-HLS-02, CORE-11, CORE-14 | 映射计算与 selector 更新/删除、Hls 事件订阅分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
+| PKG-HLS-03 | artplayer-plugin-hls-control
整理内部职责与资源 | PKG-HLS-02, CORE-11, CORE-14 | 映射计算与 selector 更新/删除、Hls 事件订阅分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-HLS-04 | artplayer-plugin-hls-control
迁移自有源码和公开类型 | PKG-HLS-03, ENG-04, ENG-06, CORE-07 | HLS 能力适配类型、回调上下文和旧 getName 参数 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| PKG-HLS-05 | artplayer-plugin-hls-control
验证新旧核心和组合 | PKG-HLS-04, CORE-22 | 本地多码率 HLS、换成无轨道来源、旧核心和最终核心 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-HLS-06 | artplayer-plugin-hls-control
验证分发并同步文档 | 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)
diff --git a/refactor/progress.md b/refactor/progress.md
index 064d0a146..924015851 100644
--- a/refactor/progress.md
+++ b/refactor/progress.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、切源、失败恢复和销毁。
diff --git a/refactor/risk-table.md b/refactor/risk-table.md
index 6f1ad17c8..1a9f4ea34 100644
--- a/refactor/risk-table.md
+++ b/refactor/risk-table.md
@@ -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 |
diff --git a/refactor/risks.json b/refactor/risks.json
index a05b378ef..b84b9c539 100644
--- a/refactor/risks.json
+++ b/refactor/risks.json
@@ -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",
diff --git a/refactor/tasks.json b/refactor/tasks.json
index 577275be5..6661a9051 100644
--- a/refactor/tasks.json
+++ b/refactor/tasks.json
@@ -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",
diff --git a/refactor/third-party.json b/refactor/third-party.json
index ca40f1e25..3a78a426b 100644
--- a/refactor/third-party.json
+++ b/refactor/third-party.json
@@ -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"
},
{
diff --git a/test/browser/hls-control.spec.js b/test/browser/hls-control.spec.js
index 0382df4d3..768755cd9 100644
--- a/test/browser/hls-control.spec.js
+++ b/test/browser/hls-control.spec.js
@@ -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())
+ })
}
})
diff --git a/test/helpers/hls-control.js b/test/helpers/hls-control.js
index 43a1f46e0..6c00a91e4 100644
--- a/test/helpers/hls-control.js
+++ b/test/helpers/hls-control.js
@@ -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),
+ }
+}
diff --git a/test/hls-control.test.js b/test/hls-control.test.js
index cc301e9ac..a4fdd3d96 100644
--- a/test/hls-control.test.js
+++ b/test/hls-control.test.js
@@ -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)
+})