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) +})