mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 10:56:15 -08:00
refactor(mediabunny): [PKG-MB-07] own HLS selection intent and menu lifecycle
This commit is contained in:
1 parent
ccee9157a7
commit
8a2b56e829
27 files changed
+4217
-680
No files matched your search
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i,r,a,s,n,o,c,l,u,h,d,f,m,p,g,k,y,b,w,T,S,v,P,I,x,C,B,E,_,R,F,A=e=>{throw TypeError(e)},M=(e,t,i)=>t.has(e)||A("Cannot "+i),D=(e,t,i)=>(M(e,t,"read from private field"),i?i.call(e):t.get(e)),N=(e,t,i)=>t.has(e)?A("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,i),O=(e,t,i,r)=>(M(e,t,"write to private field"),r?r.call(e,i):t.set(e,i),i),z=(e,t,i)=>(M(e,t,"access private method"),i),L=(e,t,i,r)=>({set _(r){O(e,t,r,i)},get _(){return D(e,t,r)}});function U(e,t){const i=new Map;return e.filter(e=>{const r=e[t];return void 0===r||!i.has(r)&&i.set(r,1)})}class q{constructor(){N(this,e,!1),this.listeners=new Map}addEventListener(t,i){D(this,e)||(this.listeners.has(t)||this.listeners.set(t,[]),this.listeners.get(t)?.push(i))}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(t,i){if(D(this,e))return;const r=Object.assign(new Event(t),{detail:i});this.listeners.get(t)?.forEach(t=>{D(this,e)||t(r)})}destroy(){O(this,e,!0),this.listeners.clear()}}function W(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const H=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},V=e=>e&&e[e.length-1],j=e=>{let t=0;for(;0===e.readBits(1)&&t<32;)t++;if(t>=32)throw new Error("Invalid exponential-Golomb code.");return(1<<t)-1+e.readBits(t)},K=(e,t)=>{const i=t+1,r=Math.floor(Math.log2(i));e.writeBits(r,0),e.writeBits(1,1),e.writeBits(r,i-2**r)},X=e=>{const t=j(e);return 0===(1&t)?-(t>>1):t+1>>1},$=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),G=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),Q=new TextDecoder,Y=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),J={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},Z=Y(J),ee={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},te=Y(ee),ie={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},re=Y(ie),ae=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,se={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},ne=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class oe{constructor(){this.currentPromise=Promise.resolve(),this.pending=0}async acquire(){let e;const t=new Promise(t=>{let i=!1;e=()=>{i||(t(),this.pending--,i=!0)}}),i=this.currentPromise;return this.currentPromise=t,this.pending++,await i,e}}const ce=/^[0-9a-fA-F]+$/,le=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),ue=e=>(e=(e=(e=(e=(e=e>>1&1431655765|(1431655765&e)<<1)>>2&858993459|(858993459&e)<<2)>>4&252645135|(252645135&e)<<4)>>8&16711935|(16711935&e)<<8)>>16&65535|(65535&e)<<16)>>>0,he=(e,t,i)=>{let r=0,a=e.length-1,s=-1;for(;r<=a;){const n=r+a>>1,o=i(e[n]);o===t?(s=n,a=n-1):o<t?r=n+1:a=n-1}return s},de=(e,t,i)=>{let r=0,a=e.length-1,s=-1;for(;r<=a;){const n=r+(a-r+1)/2|0;i(e[n])<=t?(s=n,r=n+1):a=n-1}return s},fe=(e,t,i)=>{const r=de(e,i(t),i);e.splice(r+1,0,t)},me=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},pe=(e,t)=>{const i=e.indexOf(t);-1!==i&&e.splice(i,1)},ge=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},ke=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},ye=e=>{throw new Error(`Unexpected value: ${e}`)},be=(e,t,i)=>{const r=e.getUint8(t),a=e.getUint8(t+1),s=e.getUint8(t+2);return i?r|a<<8|s<<16:r<<16|a<<8|s},we=(e,t,i,r)=>{i>>>=0,i&=16777215,e.setUint8(t,i>>>16&255),e.setUint8(t+1,i>>>8&255),e.setUint8(t+2,255&i)},Te=(e,t)=>({async next(){const i=await e.next();return i.done?{value:void 0,done:!0}:{value:t(i.value),done:!1}},return:()=>e.return(),throw:t=>e.throw(t),[Symbol.asyncIterator](){return this}}),Se=(e,t,i)=>Math.max(t,Math.min(i,e)),ve="und",Pe=e=>{const t=Math.round(e);return Math.abs(e/t-1)<10*Number.EPSILON?t:e},Ie=(e,t)=>Math.round(e/t)*t,xe=(e,t)=>Math.round(e*t)/t,Ce=(e,t)=>Math.floor(e/t)*t,Be=e=>{let t=0;for(;0!==e;)e&=e-1,t++;return t},Ee=/^[a-z]{3}$/,_e=e=>Ee.test(e),Re=1e6*(1+Number.EPSILON),Fe=(e,t)=>{const i={...e,...t};if(e.headers||t.headers){const r=e.headers?Ae(e.headers):{},a=t.headers?Ae(t.headers):{},s={...r};Object.entries(a).forEach(([e,t])=>{const i=Object.keys(s).find(t=>t.toLowerCase()===e.toLowerCase());i&&delete s[i],s[e]=t}),i.headers=s}return i},Ae=e=>{if(e instanceof Headers){const t={};return e.forEach((e,i)=>{t[i]=e}),t}if(Array.isArray(e)){const t={};return e.forEach(([e,i])=>{t[e]=i}),t}return e},Me=async(e,t,i,r,a)=>{lLine truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i,r,a,s,n,o,c,l,u,h,d,f,m,p,g,k,y,b,w,T,S,v,P,I,x,C,B,E,_,R,F,A=e=>{throw TypeError(e)},M=(e,t,i)=>t.has(e)||A("Cannot "+i),D=(e,t,i)=>(M(e,t,"read from private field"),i?i.call(e):t.get(e)),N=(e,t,i)=>t.has(e)?A("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,i),O=(e,t,i,r)=>(M(e,t,"write to private field"),r?r.call(e,i):t.set(e,i),i),L=(e,t,i)=>(M(e,t,"access private method"),i),z=(e,t,i,r)=>({set _(r){O(e,t,r,i)},get _(){return D(e,t,r)}});function U(e){const t=[];for(const r of e)try{r()}catch(i){t.push(i)}if(t.length)throw t[0]}function q(e,t,i){const r={control:!1,setting:!1};let a=null;function s(i){r[i]&&(r[i]=!1,"control"===i?e.controls.cache.has(t)&&e.controls.remove(t):e.setting.find(t)&&e.setting.remove(t))}function n(){a=null,U([()=>s("control"),()=>s("setting")])}return{update:function(o,c,l,u){if(!o)return void n();a=o;const h=()=>a===o&&l(),d=async e=>h()&&o.selector.some(t=>t.value===e.value)?u(e,o.title):e.html;c.control?(r.control=!0,e.controls.update({name:t,position:"right",html:o.html,style:{padding:"0 10px"},selector:o.selector,onSelect:d})):s("control"),h()&&(c.setting?(r.setting=!0,e.setting.update({name:t,tooltip:o.html,html:o.title,icon:i,width:200,selector:o.selector,onSelect:d})):s("setting"))},clear:n}}function W(e){const t=new Map,i=[];for(const r of e){const e=t.get(r.html);void 0!==r.html&&void 0!==e?r.default&&(i[e]=r):(t.set(r.html,i.length),i.push(r))}return i}function H({art:e,shim:t,option:i}){if(!i.m3u8)return;let r=!1,a=0,s=0;const n=q(e,"mediabunny-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),o=q(e,"mediabunny-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),c=e=>!r&&!t.engine.destroyed&&t.engine.loadSeq===e;async function l(){if(r||t.engine.destroyed)return;const e=++a,s=t.engine.loadSeq,l=()=>e===a&&c(s);try{const e=await t.getM3u8State();if(!l())return;const r=i.m3u8?.quality||{},a=function(e,t,i){if(!t.control&&!t.setting||!e?.levels.length)return null;const r=t.auto||"Auto",a=t.getName||(e=>e.name||`${e.height}P`),s=e.currentLevel?a(e.currentLevel):r,n=[];for(const l of e.levels){if(!i())return null;n.push({html:a(l),value:l.id,default:e.currentLevel?.id===l.id})}const o=new Map(e.levels.map(e=>[e.id,e.height])),c=W(n).sort((e,t)=>("auto"===t.value?0:o.get(t.value)||0)-("auto"===e.value?0:o.get(e.value)||0));return c.push({html:r,value:"auto",default:!e.currentLevel}),{title:t.title||"Quality",html:s,selector:c}}(e,r,l);if(!l())return;if(n.update(a,r,()=>c(s),(e,t)=>u("quality",e,t)),!l())return;const h=i.m3u8?.audio||{},d=function(e,t,i){if(!t.control&&!t.setting||!e||e.audios.length<2)return null;const r=t.auto||"Auto",a=t.getName||(e=>e.name||e.lang||e.language),s=e.currentAudio?a(e.currentAudio):r,n=[];for(const c of e.audios){if(!i())return null;n.push({html:a(c),value:c.id,default:e.currentAudio?.id===c.id})}const o=W(n);return o.push({html:r,value:"auto",default:!e.currentAudio}),{title:t.title||"Audio",html:s,selector:o}}(e,h,l);l()&&o.update(d,h,()=>c(s),(e,t)=>u("audio",e,t))}catch(h){if(l())throw h}}async function u(i,r,n){const o=++s,u=t.engine.loadSeq;a++;const d=()=>o===s&&c(u);try{"quality"===i?await t.switchM3u8Quality(r.value):await t.switchM3u8Audio(r.value),d()&&(await l(),d()&&(e.notice.show=`${n}: ${r.html}`))}catch(f){if(d()&&(await h(),d()))throw f}return r.html}function h(){return l().catch(e=>console.warn("MediaBunny HLS menu update:",e))}function d(){a++,s++;try{U([n.clear,o.clear])}catch(e){console.warn("MediaBunny HLS menu cleanup:",e)}}return e.on("video:loadedmetadata",h),e.on("restart",h),e.on("video:loadstart",d),e.on("video:error",d),e.on("destroy",function t(){if(!r){r=!0,d();try{U([()=>e.off("video:loadedmetadata",h),()=>e.off("restart",h),()=>e.off("video:loadstart",d),()=>e.off("video:error",d),()=>e.off("destLine truncated
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i,r,n,s,a,o,l,c,u,d,h,f,m,p,g,k,y,b,T,v,w,S,P,I,x,C,B,E,_,R,F,A=Object.defineProperty,M=Object.defineProperties,D=Object.getOwnPropertyDescriptors,N=Object.getOwnPropertySymbols,O=Object.prototype.hasOwnProperty,z=Object.prototype.propertyIsEnumerable,L=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),U=e=>{throw TypeError(e)},q=Math.pow,W=(e,t,i)=>t in e?A(e,t,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[t]=i,H=(e,t)=>{for(var i in t||(t={}))O.call(t,i)&&W(e,i,t[i]);if(N)for(var i of N(t))z.call(t,i)&&W(e,i,t[i]);return e},V=(e,t)=>M(e,D(t)),j=(e,t,i)=>t.has(e)||U("Cannot "+i),K=(e,t,i)=>(j(e,t,"read from private field"),i?i.call(e):t.get(e)),X=(e,t,i)=>t.has(e)?U("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,i),$=(e,t,i,r)=>(j(e,t,"write to private field"),r?r.call(e,i):t.set(e,i),i),G=(e,t,i)=>(j(e,t,"access private method"),i),Q=(e,t,i,r)=>({set _(r){$(e,t,r,i)},get _(){return K(e,t,r)}}),Y=(e,t,i)=>new Promise((r,n)=>{var s=e=>{try{o(i.next(e))}catch(t){n(t)}},a=e=>{try{o(i.throw(e))}catch(t){n(t)}},o=e=>e.done?r(e.value):Promise.resolve(e.value).then(s,a);o((i=i.apply(e,t)).next())}),J=function(e,t){this[0]=e,this[1]=t},Z=(e,t,i)=>{var r,n=(e,t,r,s)=>{try{var a=i[e](t),o=(t=a.value)instanceof J,l=a.done;Promise.resolve(o?t[0]:t).then(i=>o?n("return"===e?e:"next",t[1]?{done:i.done,value:i.value}:i,r,s):r({value:i,done:l})).catch(e=>n("throw",e,r,s))}catch(c){s(c)}},s=(e,t,i,s)=>a[e]=a=>(t=new Promise((t,i,s)=>(s=()=>n(e,a,t,i),r?r.then(s):s())),s=()=>r===i&&(r=0),r=i=t.then(s,s),t),a={};return i=i.apply(e,t),a[L("asyncIterator")]=()=>a,s("next"),s("throw"),s("return"),a},ee=e=>{var t,i=e[L("asyncIterator")],r=!1,n={};return null==i?(i=e[L("iterator")](),t=e=>n[e]=t=>i[e](t)):(i=i.call(e),t=e=>n[e]=t=>{if(r){if(r=!1,"throw"===e)throw t;return t}return r=!0,{done:!1,value:new J(new Promise(r=>{var n=i[e](t);n instanceof Object||U("Object expected"),r(n)}),1)}}),n[L("iterator")]=()=>n,t("next"),"throw"in i?t("throw"):n.throw=e=>{throw e},"return"in i&&t("return"),n},te=(e,t,i)=>(t=e[L("asyncIterator")])?t.call(e):(e=e[L("iterator")](),t={},(i=(i,r)=>(r=e[i])&&(t[i]=t=>new Promise((i,n,s)=>(t=r.call(e,t),s=t.done,Promise.resolve(t.value).then(e=>i({value:e,done:s}),n)))))("next"),i("return"),t);function ie(e,t){const i=new Map;return e.filter(e=>{const r=e[t];return void 0===r||!i.has(r)&&i.set(r,1)})}class re{constructor(){X(this,e,!1),this.listeners=new Map}addEventListener(t,i){var r;K(this,e)||(this.listeners.has(t)||this.listeners.set(t,[]),null==(r=this.listeners.get(t))||r.push(i))}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(t,i){var r;if(K(this,e))return;const n=Object.assign(new Event(t),{detail:i});null==(r=this.listeners.get(t))||r.forEach(t=>{K(this,e)||t(n)})}destroy(){$(this,e,!0),this.listeners.clear()}}function ne(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const se=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},ae=e=>e&&e[e.length-1],oe=e=>{let t=0;for(;0===e.readBits(1)&&t<32;)t++;if(t>=32)throw new Error("Invalid exponential-Golomb code.");return(1<<t)-1+e.readBits(t)},le=(e,t)=>{const i=t+1,r=Math.floor(Math.log2(i));e.writeBits(r,0),e.writeBits(1,1),e.writeBits(r,i-q(2,r))},ce=e=>{const t=oe(e);return 0===(1&t)?-(t>>1):t+1>>1},ue=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),de=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),he=new TextDecoder,fe=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),me={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},pe=fe(me),ge={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},ke=fe(ge),ye={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},be=fe(ye),Te=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,ve={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},we=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class Se{constructor(){this.currentPromise=Promise.resolve(),this.pending=0}acquire(){return Y(this,null,function*(){let e;const t=new Promise(t=>{let i=!1;e=()=>{i||(t(),this.pending--,i=!0)}}),i=this.currentPromise;return this.currentPromise=t,this.pending++,yield i,e})}}const Pe=/^[0-9a-fA-F]+$/,Ie=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),xe=e=>(e=(e=(e=(e=(e=e>>1&1431655765|(1431655765&e)<<1)>>2&858993459|(858993459&e)<<Line truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i,r,n,s,a,o,l,c,u,d,h,f,m,p,g,k,y,b,T,v,w,S,P,I,x,C,B,E,_,R,F,A=Object.defineProperty,M=Object.defineProperties,D=Object.getOwnPropertyDescriptors,N=Object.getOwnPropertySymbols,O=Object.prototype.hasOwnProperty,L=Object.prototype.propertyIsEnumerable,z=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),U=e=>{throw TypeError(e)},q=Math.pow,W=(e,t,i)=>t in e?A(e,t,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[t]=i,H=(e,t)=>{for(var i in t||(t={}))O.call(t,i)&&W(e,i,t[i]);if(N)for(var i of N(t))L.call(t,i)&&W(e,i,t[i]);return e},V=(e,t)=>M(e,D(t)),j=(e,t,i)=>t.has(e)||U("Cannot "+i),K=(e,t,i)=>(j(e,t,"read from private field"),i?i.call(e):t.get(e)),X=(e,t,i)=>t.has(e)?U("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,i),$=(e,t,i,r)=>(j(e,t,"write to private field"),r?r.call(e,i):t.set(e,i),i),G=(e,t,i)=>(j(e,t,"access private method"),i),Q=(e,t,i,r)=>({set _(r){$(e,t,r,i)},get _(){return K(e,t,r)}}),Y=(e,t,i)=>new Promise((r,n)=>{var s=e=>{try{o(i.next(e))}catch(t){n(t)}},a=e=>{try{o(i.throw(e))}catch(t){n(t)}},o=e=>e.done?r(e.value):Promise.resolve(e.value).then(s,a);o((i=i.apply(e,t)).next())}),J=function(e,t){this[0]=e,this[1]=t},Z=(e,t,i)=>{var r,n=(e,t,r,s)=>{try{var a=i[e](t),o=(t=a.value)instanceof J,l=a.done;Promise.resolve(o?t[0]:t).then(i=>o?n("return"===e?e:"next",t[1]?{done:i.done,value:i.value}:i,r,s):r({value:i,done:l})).catch(e=>n("throw",e,r,s))}catch(c){s(c)}},s=(e,t,i,s)=>a[e]=a=>(t=new Promise((t,i,s)=>(s=()=>n(e,a,t,i),r?r.then(s):s())),s=()=>r===i&&(r=0),r=i=t.then(s,s),t),a={};return i=i.apply(e,t),a[z("asyncIterator")]=()=>a,s("next"),s("throw"),s("return"),a},ee=e=>{var t,i=e[z("asyncIterator")],r=!1,n={};return null==i?(i=e[z("iterator")](),t=e=>n[e]=t=>i[e](t)):(i=i.call(e),t=e=>n[e]=t=>{if(r){if(r=!1,"throw"===e)throw t;return t}return r=!0,{done:!1,value:new J(new Promise(r=>{var n=i[e](t);n instanceof Object||U("Object expected"),r(n)}),1)}}),n[z("iterator")]=()=>n,t("next"),"throw"in i?t("throw"):n.throw=e=>{throw e},"return"in i&&t("return"),n},te=(e,t,i)=>(t=e[z("asyncIterator")])?t.call(e):(e=e[z("iterator")](),t={},(i=(i,r)=>(r=e[i])&&(t[i]=t=>new Promise((i,n,s)=>(t=r.call(e,t),s=t.done,Promise.resolve(t.value).then(e=>i({value:e,done:s}),n)))))("next"),i("return"),t);function ie(e){const t=[];for(const r of e)try{r()}catch(i){t.push(i)}if(t.length)throw t[0]}function re(e,t,i){const r={control:!1,setting:!1};let n=null;function s(i){r[i]&&(r[i]=!1,"control"===i?e.controls.cache.has(t)&&e.controls.remove(t):e.setting.find(t)&&e.setting.remove(t))}function a(){n=null,ie([()=>s("control"),()=>s("setting")])}return{update:function(o,l,c,u){if(!o)return void a();n=o;const d=()=>n===o&&c(),h=e=>Y(null,null,function*(){return d()&&o.selector.some(t=>t.value===e.value)?u(e,o.title):e.html});l.control?(r.control=!0,e.controls.update({name:t,position:"right",html:o.html,style:{padding:"0 10px"},selector:o.selector,onSelect:h})):s("control"),d()&&(l.setting?(r.setting=!0,e.setting.update({name:t,tooltip:o.html,html:o.title,icon:i,width:200,selector:o.selector,onSelect:h})):s("setting"))},clear:a}}function ne(e){const t=new Map,i=[];for(const r of e){const e=t.get(r.html);void 0!==r.html&&void 0!==e?r.default&&(i[e]=r):(t.set(r.html,i.length),i.push(r))}return i}function se({art:e,shim:t,option:i}){if(!i.m3u8)return;let r=!1,n=0,s=0;const a=re(e,"mediabunny-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),o=re(e,"mediabunny-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),l=e=>!r&&!t.engine.destroyed&&t.engine.loadSeq===e;function c(){return Y(this,null,function*(){var e,s;if(r||t.engine.destroyed)return;const c=++n,d=t.engine.loadSeq,h=()=>c===n&&l(d);try{const r=yield t.getM3u8State();if(!h())return;const n=(null==(e=i.m3u8)?void 0:e.quality)||{},c=function(e,t,i){vaLine truncated
|
||||
@@ -23,164 +23,206 @@ var __privateWrapper = (obj, member, setter, getter) => ({
|
||||
var _closed, _closed2, _generation, _contexts, _nodes, _pump, _playTask, _playing, _loading, _onError, _AudioEngine_instances, load_fn, startPump_fn, play_fn, _generation2, _iteration, _loading2, _createSink, _usedSink, _destroyed, _preparing, _poster, _renderer, _onError2, _VideoEngine_instances, current_fn, context_fn, report_fn, release_fn, draw_fn, load_fn2, reset_fn, _playback;
|
||||
const $audio = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>';
|
||||
const $quality = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
|
||||
function uniqBy(array, property) {
|
||||
const seen = /* @__PURE__ */ new Map();
|
||||
return array.filter((item) => {
|
||||
const key = item[property];
|
||||
if (key === void 0) {
|
||||
return true;
|
||||
function releaseAll(actions) {
|
||||
const failures = [];
|
||||
for (const action of actions) {
|
||||
try {
|
||||
action();
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
return !seen.has(key) && seen.set(key, 1);
|
||||
});
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0];
|
||||
}
|
||||
function createMenu(art, name, icon) {
|
||||
const owned = { control: false, setting: false };
|
||||
let model = null;
|
||||
function remove(surface) {
|
||||
if (!owned[surface])
|
||||
return;
|
||||
owned[surface] = false;
|
||||
if (surface === "control") {
|
||||
if (art.controls.cache.has(name))
|
||||
art.controls.remove(name);
|
||||
} else if (art.setting.find(name)) {
|
||||
art.setting.remove(name);
|
||||
}
|
||||
}
|
||||
function clear() {
|
||||
model = null;
|
||||
releaseAll([() => remove("control"), () => remove("setting")]);
|
||||
}
|
||||
function update(next, config, active, select2) {
|
||||
if (!next) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
model = next;
|
||||
const valid = () => model === next && active();
|
||||
const onSelect = async (item) => {
|
||||
if (!valid() || !next.selector.some((entry) => entry.value === item.value))
|
||||
return item.html;
|
||||
return select2(item, next.title);
|
||||
};
|
||||
if (config.control) {
|
||||
owned.control = true;
|
||||
art.controls.update({ name, position: "right", html: next.html, style: { padding: "0 10px" }, selector: next.selector, onSelect });
|
||||
} else {
|
||||
remove("control");
|
||||
}
|
||||
if (!valid())
|
||||
return;
|
||||
if (config.setting) {
|
||||
owned.setting = true;
|
||||
art.setting.update({ name, tooltip: next.html, html: next.title, icon, width: 200, selector: next.selector, onSelect });
|
||||
} else {
|
||||
remove("setting");
|
||||
}
|
||||
}
|
||||
return { update, clear };
|
||||
}
|
||||
function unique(items) {
|
||||
const positions = /* @__PURE__ */ new Map();
|
||||
const output = [];
|
||||
for (const item of items) {
|
||||
const position = positions.get(item.html);
|
||||
if (item.html !== void 0 && position !== void 0) {
|
||||
if (item.default)
|
||||
output[position] = item;
|
||||
} else {
|
||||
positions.set(item.html, output.length);
|
||||
output.push(item);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
function qualityModel(state2, config, active) {
|
||||
if (!config.control && !config.setting || !state2?.levels.length)
|
||||
return null;
|
||||
const auto = config.auto || "Auto";
|
||||
const getName = config.getName || ((level) => level.name || `${level.height}P`);
|
||||
const html = state2.currentLevel ? getName(state2.currentLevel) : auto;
|
||||
const items = [];
|
||||
for (const level of state2.levels) {
|
||||
if (!active())
|
||||
return null;
|
||||
items.push({ html: getName(level), value: level.id, default: state2.currentLevel?.id === level.id });
|
||||
}
|
||||
const heights = new Map(state2.levels.map((level) => [level.id, level.height]));
|
||||
const selector = unique(items).sort((left, right) => (right.value === "auto" ? 0 : heights.get(right.value) || 0) - (left.value === "auto" ? 0 : heights.get(left.value) || 0));
|
||||
selector.push({ html: auto, value: "auto", default: !state2.currentLevel });
|
||||
return { title: config.title || "Quality", html, selector };
|
||||
}
|
||||
function audioModel(state2, config, active) {
|
||||
if (!config.control && !config.setting || !state2 || state2.audios.length < 2)
|
||||
return null;
|
||||
const auto = config.auto || "Auto";
|
||||
const getName = config.getName || ((track) => track.name || track.lang || track.language);
|
||||
const html = state2.currentAudio ? getName(state2.currentAudio) : auto;
|
||||
const items = [];
|
||||
for (const track of state2.audios) {
|
||||
if (!active())
|
||||
return null;
|
||||
items.push({ html: getName(track), value: track.id, default: state2.currentAudio?.id === track.id });
|
||||
}
|
||||
const selector = unique(items);
|
||||
selector.push({ html: auto, value: "auto", default: !state2.currentAudio });
|
||||
return { title: config.title || "Audio", html, selector };
|
||||
}
|
||||
function setupM3u8Controls({ art, shim, option }) {
|
||||
if (!option.m3u8)
|
||||
return;
|
||||
function removeControl(name) {
|
||||
if (art.controls.cache.has(name)) {
|
||||
art.controls.remove(name);
|
||||
}
|
||||
}
|
||||
function removeSetting(name) {
|
||||
if (art.setting.find(name)) {
|
||||
art.setting.remove(name);
|
||||
}
|
||||
}
|
||||
function clearQuality() {
|
||||
removeControl("mediabunny-quality");
|
||||
removeSetting("mediabunny-quality");
|
||||
}
|
||||
function clearAudio() {
|
||||
removeControl("mediabunny-audio");
|
||||
removeSetting("mediabunny-audio");
|
||||
}
|
||||
function canRender(config = {}) {
|
||||
return config.control || config.setting;
|
||||
}
|
||||
async function updateQuality(state2) {
|
||||
const config = option.m3u8?.quality || {};
|
||||
if (!canRender(config) || !state2?.levels.length) {
|
||||
clearQuality();
|
||||
return;
|
||||
}
|
||||
const auto = config.auto || "Auto";
|
||||
const title = config.title || "Quality";
|
||||
const getName = config.getName || ((level) => level.name || `${level.height}P`);
|
||||
const defaultHtml = state2.currentLevel ? getName(state2.currentLevel) : auto;
|
||||
const selector = uniqBy(
|
||||
state2.levels.map((item) => {
|
||||
return {
|
||||
html: getName(item),
|
||||
value: item.id,
|
||||
default: state2.currentLevel?.id === item.id
|
||||
};
|
||||
}),
|
||||
"html"
|
||||
).sort((a, b) => {
|
||||
const left = state2.levels.find((item) => item.id === a.value);
|
||||
const right = state2.levels.find((item) => item.id === b.value);
|
||||
return (right?.height || 0) - (left?.height || 0);
|
||||
});
|
||||
selector.push({
|
||||
html: auto,
|
||||
value: "auto",
|
||||
default: !state2.currentLevel
|
||||
});
|
||||
const onSelect = async (item) => {
|
||||
await shim.switchM3u8Quality(item.value);
|
||||
art.notice.show = `${title}: ${item.html}`;
|
||||
await update();
|
||||
return item.html;
|
||||
};
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: "mediabunny-quality",
|
||||
position: "right",
|
||||
html: defaultHtml,
|
||||
style: { padding: "0 10px" },
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: "mediabunny-quality",
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $quality,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
}
|
||||
async function updateAudio(state2) {
|
||||
const config = option.m3u8?.audio || {};
|
||||
if (!canRender(config) || !state2?.audios.length || state2.audios.length < 2) {
|
||||
clearAudio();
|
||||
return;
|
||||
}
|
||||
const auto = config.auto || "Auto";
|
||||
const title = config.title || "Audio";
|
||||
const getName = config.getName || ((track) => track.name || track.lang || track.language);
|
||||
const defaultHtml = state2.currentAudio ? getName(state2.currentAudio) : auto;
|
||||
const selector = uniqBy(
|
||||
state2.audios.map((item) => {
|
||||
return {
|
||||
html: getName(item),
|
||||
value: item.id,
|
||||
default: state2.currentAudio?.id === item.id
|
||||
};
|
||||
}),
|
||||
"html"
|
||||
);
|
||||
selector.push({
|
||||
html: auto,
|
||||
value: "auto",
|
||||
default: !state2.currentAudio
|
||||
});
|
||||
const onSelect = async (item) => {
|
||||
await shim.switchM3u8Audio(item.value);
|
||||
art.notice.show = `${title}: ${item.html}`;
|
||||
await update();
|
||||
return item.html;
|
||||
};
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: "mediabunny-audio",
|
||||
position: "right",
|
||||
html: defaultHtml,
|
||||
style: { padding: "0 10px" },
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: "mediabunny-audio",
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $audio,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
}
|
||||
let closed = false;
|
||||
let revision = 0;
|
||||
let selection = 0;
|
||||
const quality2 = createMenu(art, "mediabunny-quality", $quality);
|
||||
const audio2 = createMenu(art, "mediabunny-audio", $audio);
|
||||
const current = (source) => !closed && !shim.engine.destroyed && shim.engine.loadSeq === source;
|
||||
async function update() {
|
||||
const state2 = await shim.getM3u8State();
|
||||
if (!state2) {
|
||||
clearQuality();
|
||||
clearAudio();
|
||||
if (closed || shim.engine.destroyed)
|
||||
return;
|
||||
const version = ++revision;
|
||||
const source = shim.engine.loadSeq;
|
||||
const active = () => version === revision && current(source);
|
||||
try {
|
||||
const state2 = await shim.getM3u8State();
|
||||
if (!active())
|
||||
return;
|
||||
const qualityConfig = option.m3u8?.quality || {};
|
||||
const qualityView = qualityModel(state2, qualityConfig, active);
|
||||
if (!active())
|
||||
return;
|
||||
quality2.update(qualityView, qualityConfig, () => current(source), (item, title) => select2("quality", item, title));
|
||||
if (!active())
|
||||
return;
|
||||
const audioConfig = option.m3u8?.audio || {};
|
||||
const audioView = audioModel(state2, audioConfig, active);
|
||||
if (active())
|
||||
audio2.update(audioView, audioConfig, () => current(source), (item, title) => select2("audio", item, title));
|
||||
} catch (error) {
|
||||
if (active())
|
||||
throw error;
|
||||
}
|
||||
await Promise.all([
|
||||
updateQuality(state2),
|
||||
updateAudio(state2)
|
||||
]);
|
||||
}
|
||||
art.on("video:loadedmetadata", update);
|
||||
art.on("restart", update);
|
||||
async function select2(kind, item, title) {
|
||||
const intent = ++selection;
|
||||
const source = shim.engine.loadSeq;
|
||||
revision++;
|
||||
const active = () => intent === selection && current(source);
|
||||
try {
|
||||
if (kind === "quality")
|
||||
await shim.switchM3u8Quality(item.value);
|
||||
else
|
||||
await shim.switchM3u8Audio(item.value);
|
||||
if (active()) {
|
||||
await update();
|
||||
if (active())
|
||||
art.notice.show = `${title}: ${item.html}`;
|
||||
}
|
||||
} catch (error) {
|
||||
if (active()) {
|
||||
await refresh();
|
||||
if (active())
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return item.html;
|
||||
}
|
||||
function refresh() {
|
||||
return update().catch((error) => console.warn("MediaBunny HLS menu update:", error));
|
||||
}
|
||||
function invalidate() {
|
||||
revision++;
|
||||
selection++;
|
||||
try {
|
||||
releaseAll([quality2.clear, audio2.clear]);
|
||||
} catch (error) {
|
||||
console.warn("MediaBunny HLS menu cleanup:", error);
|
||||
}
|
||||
}
|
||||
function destroy() {
|
||||
if (closed)
|
||||
return;
|
||||
closed = true;
|
||||
invalidate();
|
||||
try {
|
||||
releaseAll([
|
||||
() => art.off("video:loadedmetadata", refresh),
|
||||
() => art.off("restart", refresh),
|
||||
() => art.off("video:loadstart", invalidate),
|
||||
() => art.off("video:error", invalidate),
|
||||
() => art.off("destroy", destroy)
|
||||
]);
|
||||
} catch (error) {
|
||||
console.warn("MediaBunny HLS listener cleanup:", error);
|
||||
}
|
||||
}
|
||||
art.on("video:loadedmetadata", refresh);
|
||||
art.on("restart", refresh);
|
||||
art.on("video:loadstart", invalidate);
|
||||
art.on("video:error", invalidate);
|
||||
art.on("destroy", destroy);
|
||||
return { update };
|
||||
}
|
||||
class EventTarget {
|
||||
@@ -24301,15 +24343,15 @@ function publish(events, current, names) {
|
||||
}
|
||||
function metadataBarrier(ready, current) {
|
||||
let video = false;
|
||||
let audio = false;
|
||||
let audio2 = false;
|
||||
let published = false;
|
||||
function complete(part) {
|
||||
if (!current() || published)
|
||||
return;
|
||||
if (part === "video")
|
||||
video = true;
|
||||
else audio = true;
|
||||
if (video && audio) {
|
||||
else audio2 = true;
|
||||
if (video && audio2) {
|
||||
published = true;
|
||||
ready();
|
||||
}
|
||||
@@ -24651,61 +24693,78 @@ play_fn = async function(task, generation) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
async function selectQuality(host, value) {
|
||||
const { input, media, loadSeq } = host;
|
||||
if (!media?.isHls || !input || host.destroyed)
|
||||
const requests = /* @__PURE__ */ new WeakMap();
|
||||
async function select(host, value, resolve) {
|
||||
const { input, loadSeq } = host;
|
||||
if (!host.media?.isHls || !input || host.destroyed)
|
||||
return;
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && host.media === media;
|
||||
let selection;
|
||||
try {
|
||||
const tracks = await input.getVideoTracks();
|
||||
if (!current())
|
||||
const token = {};
|
||||
requests.set(host, token);
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && requests.get(host) === token;
|
||||
while (current()) {
|
||||
const media = host.media;
|
||||
if (!media?.isHls)
|
||||
return;
|
||||
const videoTrack = value === "auto" ? await input.getPrimaryVideoTrack() : tracks.find((track) => track.id === value) ?? media.videoTrack;
|
||||
if (!current() || !videoTrack)
|
||||
return;
|
||||
let audioTrack = media.audioTrack;
|
||||
let audioMode = media.audioMode;
|
||||
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
||||
audioTrack = await videoTrack.getPrimaryPairableAudioTrack();
|
||||
audioMode = "auto";
|
||||
let selection;
|
||||
try {
|
||||
selection = await resolve(input, media, value, current);
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
if (host.media === media)
|
||||
throw error;
|
||||
continue;
|
||||
}
|
||||
selection = { videoTrack, audioTrack, videoMode: value === "auto" ? "auto" : "manual", audioMode };
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
throw error;
|
||||
if (host.media !== media)
|
||||
continue;
|
||||
if (selection) {
|
||||
try {
|
||||
await host.replaceTracks(selection);
|
||||
} catch (error) {
|
||||
if (current())
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (current())
|
||||
await host.replaceTracks(selection);
|
||||
}
|
||||
async function selectAudio(host, value) {
|
||||
const { input, media, loadSeq } = host;
|
||||
if (!media?.isHls || !input || host.destroyed)
|
||||
return;
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && host.media === media;
|
||||
let selection;
|
||||
try {
|
||||
const tracks = await input.getAudioTracks();
|
||||
if (!current())
|
||||
return;
|
||||
const audioTrack = value === "auto" ? media.videoTrack ? await media.videoTrack.getPrimaryPairableAudioTrack() : await input.getPrimaryAudioTrack() : tracks.find((track) => track.id === value) ?? media.audioTrack;
|
||||
if (!current() || !audioTrack)
|
||||
return;
|
||||
let videoTrack = media.videoTrack;
|
||||
let videoMode = media.videoMode;
|
||||
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
||||
videoTrack = await audioTrack.getPrimaryPairableVideoTrack();
|
||||
videoMode = "auto";
|
||||
}
|
||||
selection = { videoTrack, audioTrack, videoMode, audioMode: value === "auto" ? "auto" : "manual" };
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
throw error;
|
||||
async function quality(input, media, value, current) {
|
||||
const tracks = await input.getVideoTracks();
|
||||
if (!current())
|
||||
return null;
|
||||
const videoTrack = value === "auto" ? await input.getPrimaryVideoTrack() : tracks.find((track) => track.id === value) ?? media.videoTrack;
|
||||
if (!current() || !videoTrack)
|
||||
return null;
|
||||
let audioTrack = media.audioTrack;
|
||||
let audioMode = media.audioMode;
|
||||
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
||||
audioTrack = await videoTrack.getPrimaryPairableAudioTrack();
|
||||
audioMode = "auto";
|
||||
}
|
||||
if (current())
|
||||
await host.replaceTracks(selection);
|
||||
return { videoTrack, audioTrack, videoMode: value === "auto" ? "auto" : "manual", audioMode };
|
||||
}
|
||||
async function audio(input, media, value, current) {
|
||||
const tracks = await input.getAudioTracks();
|
||||
if (!current())
|
||||
return null;
|
||||
const audioTrack = value === "auto" ? media.videoTrack ? await media.videoTrack.getPrimaryPairableAudioTrack() : await input.getPrimaryAudioTrack() : tracks.find((track) => track.id === value) ?? media.audioTrack;
|
||||
if (!current() || !audioTrack)
|
||||
return null;
|
||||
let videoTrack = media.videoTrack;
|
||||
let videoMode = media.videoMode;
|
||||
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
||||
videoTrack = await audioTrack.getPrimaryPairableVideoTrack();
|
||||
videoMode = "auto";
|
||||
}
|
||||
return { videoTrack, audioTrack, videoMode, audioMode: value === "auto" ? "auto" : "manual" };
|
||||
}
|
||||
function selectQuality(host, value) {
|
||||
return select(host, value, quality);
|
||||
}
|
||||
function selectAudio(host, value) {
|
||||
return select(host, value, audio);
|
||||
}
|
||||
async function getTrackBitrate(track) {
|
||||
return await track.getAverageBitrate() ?? await track.getBitrate() ?? 0;
|
||||
@@ -24985,9 +25044,9 @@ class Playback {
|
||||
}
|
||||
seek(time, resume) {
|
||||
return this.position(async (current) => {
|
||||
const audio = this.host.audio.seek(time);
|
||||
const audio2 = this.host.audio.seek(time);
|
||||
const video = current() ? this.host.video.seek(time) : Promise.resolve();
|
||||
await Promise.all([audio, video]);
|
||||
await Promise.all([audio2, video]);
|
||||
}, ["seeked"], false, resume);
|
||||
}
|
||||
replace(media, time) {
|
||||
@@ -25438,8 +25497,8 @@ class VideoEngine {
|
||||
render() {
|
||||
__privateGet(this, _renderer).render();
|
||||
}
|
||||
start(audio) {
|
||||
__privateGet(this, _renderer).start(audio);
|
||||
start(audio2) {
|
||||
__privateGet(this, _renderer).start(audio2);
|
||||
}
|
||||
stop() {
|
||||
__privateGet(this, _renderer).stop();
|
||||
@@ -25692,8 +25751,8 @@ class MediaBunnyEngine {
|
||||
}, current);
|
||||
try {
|
||||
const video = this.video.load(media, metadata.video);
|
||||
const audio = current() ? this.audio.load(media, metadata.audio) : Promise.resolve();
|
||||
await Promise.all([video, audio]);
|
||||
const audio2 = current() ? this.audio.load(media, metadata.audio) : Promise.resolve();
|
||||
await Promise.all([video, audio2]);
|
||||
if (!current())
|
||||
return;
|
||||
this.readyState = 4;
|
||||
@@ -25729,13 +25788,14 @@ class MediaBunnyEngine {
|
||||
}
|
||||
async getHlsState() {
|
||||
const media = this.media;
|
||||
const source = this.loadSeq;
|
||||
if (this.destroyed)
|
||||
return null;
|
||||
try {
|
||||
const state2 = await getHlsState(media);
|
||||
return !this.destroyed && this.media === media ? state2 : null;
|
||||
return !this.destroyed && this.loadSeq === source && this.media === media ? state2 : null;
|
||||
} catch (error) {
|
||||
if (this.destroyed || this.media !== media)
|
||||
if (this.destroyed || this.loadSeq !== source || this.media !== media)
|
||||
return null;
|
||||
throw error;
|
||||
}
|
||||
|
||||
+2
-2
@@ -51,7 +51,7 @@
|
||||
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
"test:unit": "node --test test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
|
||||
"test:unit": "node --test test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
|
||||
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
|
||||
"test": "yarn test:node && yarn test:baseline",
|
||||
"test:browser": "playwright test",
|
||||
@@ -76,7 +76,7 @@
|
||||
"test:canvas-types-package": "node refactor/scripts/canvas-package-types.mjs",
|
||||
"test:dpip": "node --test test/dpip.test.js test/dpip-lifecycle.test.js",
|
||||
"test:dpip-types-package": "node refactor/scripts/dpip-package-types.mjs",
|
||||
"test:mediabunny": "node --test test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js"
|
||||
"test:mediabunny": "node --test test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -18,7 +18,7 @@ declarations, the factory signature, canvas forwarding, or package entrypoints.
|
||||
| `MediaBunnyEngine.ts` | Load/readiness coordinator, source identity, errors and terminal teardown |
|
||||
| `playback.ts` | Playback intent and cancellable play/seek/replacement operations |
|
||||
| `readiness.ts` | One metadata publication after both participants, guarded event sequences |
|
||||
| `hls-selection.ts` | Source-bound track queries, pairing and active replacement errors |
|
||||
| `hls-selection.ts` | Latest selection intent, stable-media pairing queries and active replacement errors |
|
||||
| `VideoEngine.ts` | Video capability/setup, source and seek ownership, first frames and terminal cleanup |
|
||||
| `video-frames.ts` | Shared pending iterator reads, idempotent iterator release and existing frame timing policy |
|
||||
| `video-renderer.ts` | Owned RAF, stale frame rejection, clock-driven draw/stall/end events |
|
||||
@@ -33,12 +33,15 @@ declarations, the factory signature, canvas forwarding, or package entrypoints.
|
||||
| `shim-values.ts` | Existing volume coercion and synthetic TimeRanges values |
|
||||
| `shim-frames.ts` | Synthetic frame callbacks, per-instance RAF ownership and terminal cleanup |
|
||||
| `engine-types.ts` / `engine-ports.ts` | Internal shim, coordinator and decoder interfaces |
|
||||
| `m3u8.js` | Current ArtPlayer control/setting integration |
|
||||
| `m3u8.ts` | HLS refresh/selection revisions, source guards, events and terminal cleanup |
|
||||
| `m3u8-model.ts` | Display names, selected IDs, label deduplication and sorted selector models |
|
||||
| `m3u8-menu.ts` | Owned control/setting surfaces and stale callback guards |
|
||||
| `m3u8-types.ts` | Narrow internal UI/shim contracts without changing public factory declarations |
|
||||
|
||||
The twenty-six production TypeScript modules use strict checking with `skipLibCheck: false`
|
||||
The thirty production TypeScript modules use strict checking with `skipLibCheck: false`
|
||||
and no ambient Node types. The coordinator and both decoder engines are checked
|
||||
implementations; there are no remaining adjacent JavaScript declaration bridges.
|
||||
The public factory and HLS integration remain JavaScript, scheduled in PKG-MB-07/08.
|
||||
Only the public factory remains JavaScript, scheduled in PKG-MB-08.
|
||||
Do not describe the whole package as migrated yet.
|
||||
|
||||
The shim keeps its existing own-property order and direct prototype surface because
|
||||
@@ -100,8 +103,9 @@ event the coordinator rechecks ownership, since a listener may synchronously pau
|
||||
load another source or destroy. Active play errors reject their Promise and restore paused
|
||||
state; active seek errors additionally publish one media error. The synchronous currentTime
|
||||
setter observes its otherwise unreturnable rejection; direct engine.seek still rejects.
|
||||
Active replacement errors remain visible even after the media object changes. Stale HLS query
|
||||
errors are ignored only when their captured source no longer owns the result.
|
||||
Active replacement errors remain visible even after the media object changes. HLS queries
|
||||
and replacement errors also carry a selection intent; obsolete intents cannot replace newer
|
||||
results or surface an old failure. A still-current query retries if its media snapshot changed.
|
||||
|
||||
Metadata publishes once after both audio/video participants report; repeated or obsolete
|
||||
callbacks cannot ready the current source. A parsed container with neither track fails with
|
||||
@@ -180,6 +184,42 @@ and repeated destroy is inert. Real-browser teardown assertions poll the native
|
||||
Tests deliberately delay native resume completion or decoded buffer delivery, without
|
||||
substituting fake audio decoding for native acceptance.
|
||||
|
||||
## HLS selection and UI ownership
|
||||
|
||||
Quality and audio requests share a per-engine intent token. A newer request supersedes an
|
||||
unresolved older query, including cross-kind requests, before it can call replaceTracks.
|
||||
Queries capture source generation and Input identity. If a prior replacement installs new
|
||||
media while the latest query is awaiting track metadata or pairing, recompute against the
|
||||
current media instead of dropping the latest intent or restoring an old counterpart.
|
||||
The resolver rechecks cancellation between SDK calls; active failures still reject the caller.
|
||||
An already-started replacement remains governed by Playback until the next replacement
|
||||
begins. This does not claim that issuing a selection synchronously cancels SDK decoding.
|
||||
Pairing preserves the current compatible counterpart and its mode; fallback chooses the
|
||||
SDK primary pairable track and resets only the counterpart mode to auto. Invalid IDs retain
|
||||
the historical current-track fallback. Media state getters suppress stale source results.
|
||||
|
||||
The UI has separate refresh revisions, selection intent and source identity. Old reads
|
||||
cannot overwrite newer models; callbacks from replaced menus or a previous source are inert.
|
||||
The same visible menu accepts a newer click while a prior selection is pending. Its own
|
||||
loadedmetadata refresh does not cancel that valid selection's completion. Active failures
|
||||
refresh actual track highlights before rejecting; stale failures, including a failure
|
||||
superseded during recovery, cannot disturb the current intent. Background event-driven
|
||||
refreshes observe failures through a warning instead of unhandled Promise rejections.
|
||||
|
||||
Models retain the existing menu names, title/Auto/getName defaults, sort order and notice
|
||||
format. Auto remains an action, while the actual selected track ID controls the highlighted
|
||||
item and displayed track label. Label deduplication is retained; if labels collide, keep the
|
||||
selected ID as that label's representative so it is not hidden by the first item. Height
|
||||
lookups use a map rather than repeated array searches during sorting. User getName callbacks
|
||||
are guarded against synchronous source/destroy reentry between calls and before UI writes.
|
||||
|
||||
Each menu owns control and setting independently. Disabling either removes that surface;
|
||||
video-only/single-audio topology removes audio menus, non-HLS clears both, and loadstart/error/
|
||||
destroy invalidate pending work and clear owned UI. Teardown attempts every surface and
|
||||
listener even after one failure, and cannot prevent the entry's shim destroy callback.
|
||||
Control/setting callbacks retain their Promise and selected-label result; no new public
|
||||
methods or declarations are exposed by the internal modules.
|
||||
|
||||
## Types and dependencies
|
||||
|
||||
The locked runtime SDK remains MediaBunny 1.56.1. Its pinned WebCodecs declaration
|
||||
@@ -201,6 +241,7 @@ yarn test:browser test/browser/mediabunny-inputs.spec.js test/browser/mediabunny
|
||||
yarn test:browser test/browser/mediabunny-shim.spec.js
|
||||
yarn test:browser test/browser/mediabunny-video.spec.js
|
||||
yarn test:browser test/browser/mediabunny-audio.spec.js
|
||||
yarn test:browser test/browser/mediabunny-hls.spec.js
|
||||
```
|
||||
|
||||
Node tests use actual SDK parsing and controlled lifecycle interleavings. Browser tests
|
||||
@@ -217,8 +258,8 @@ inputs, not a rebuilt dist. Package-specific evidence and remaining tasks are in
|
||||
PKG-MB-04 covers duplicate/trackless readiness, reentrant event sequences and
|
||||
pending play/seek coordination. PKG-MB-05/06 cover late frame and audio callbacks,
|
||||
including operations already inside decoders at cancellation. These bounded cases do not
|
||||
replace long-run synchronization and combination validation. PKG-MB-07 owns HLS UI/selection
|
||||
races. PKG-MB-09 owns long-run AV sync, full core/proxy/plugin combinations and device
|
||||
replace long-run synchronization and combination validation. PKG-MB-07 covers HLS UI/selection
|
||||
races using the actual new/old cores and SDK with controlled query ordering. PKG-MB-09 owns long-run AV sync, full core/proxy/plugin combinations and device
|
||||
validation. PKG-MB-10 owns the demo, installed package and MPL/source notice gate.
|
||||
Windows WebKit without WebCodecs/Web Audio is recorded as a capability failure,
|
||||
not successful playback or a claim about all Safari installations.
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i,r,a,s,n,o,c,l,u,h,d,f,m,p,g,k,y,b,w,T,S,v,P,I,x,C,B,E,_,R,F,A=e=>{throw TypeError(e)},M=(e,t,i)=>t.has(e)||A("Cannot "+i),D=(e,t,i)=>(M(e,t,"read from private field"),i?i.call(e):t.get(e)),N=(e,t,i)=>t.has(e)?A("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,i),O=(e,t,i,r)=>(M(e,t,"write to private field"),r?r.call(e,i):t.set(e,i),i),z=(e,t,i)=>(M(e,t,"access private method"),i),L=(e,t,i,r)=>({set _(r){O(e,t,r,i)},get _(){return D(e,t,r)}});function U(e,t){const i=new Map;return e.filter(e=>{const r=e[t];return void 0===r||!i.has(r)&&i.set(r,1)})}class q{constructor(){N(this,e,!1),this.listeners=new Map}addEventListener(t,i){D(this,e)||(this.listeners.has(t)||this.listeners.set(t,[]),this.listeners.get(t)?.push(i))}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(t,i){if(D(this,e))return;const r=Object.assign(new Event(t),{detail:i});this.listeners.get(t)?.forEach(t=>{D(this,e)||t(r)})}destroy(){O(this,e,!0),this.listeners.clear()}}function W(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const H=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},V=e=>e&&e[e.length-1],j=e=>{let t=0;for(;0===e.readBits(1)&&t<32;)t++;if(t>=32)throw new Error("Invalid exponential-Golomb code.");return(1<<t)-1+e.readBits(t)},K=(e,t)=>{const i=t+1,r=Math.floor(Math.log2(i));e.writeBits(r,0),e.writeBits(1,1),e.writeBits(r,i-2**r)},X=e=>{const t=j(e);return 0===(1&t)?-(t>>1):t+1>>1},$=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),G=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),Q=new TextDecoder,Y=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),J={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},Z=Y(J),ee={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},te=Y(ee),ie={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},re=Y(ie),ae=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,se={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},ne=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class oe{constructor(){this.currentPromise=Promise.resolve(),this.pending=0}async acquire(){let e;const t=new Promise(t=>{let i=!1;e=()=>{i||(t(),this.pending--,i=!0)}}),i=this.currentPromise;return this.currentPromise=t,this.pending++,await i,e}}const ce=/^[0-9a-fA-F]+$/,le=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),ue=e=>(e=(e=(e=(e=(e=e>>1&1431655765|(1431655765&e)<<1)>>2&858993459|(858993459&e)<<2)>>4&252645135|(252645135&e)<<4)>>8&16711935|(16711935&e)<<8)>>16&65535|(65535&e)<<16)>>>0,he=(e,t,i)=>{let r=0,a=e.length-1,s=-1;for(;r<=a;){const n=r+a>>1,o=i(e[n]);o===t?(s=n,a=n-1):o<t?r=n+1:a=n-1}return s},de=(e,t,i)=>{let r=0,a=e.length-1,s=-1;for(;r<=a;){const n=r+(a-r+1)/2|0;i(e[n])<=t?(s=n,r=n+1):a=n-1}return s},fe=(e,t,i)=>{const r=de(e,i(t),i);e.splice(r+1,0,t)},me=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},pe=(e,t)=>{const i=e.indexOf(t);-1!==i&&e.splice(i,1)},ge=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},ke=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},ye=e=>{throw new Error(`Unexpected value: ${e}`)},be=(e,t,i)=>{const r=e.getUint8(t),a=e.getUint8(t+1),s=e.getUint8(t+2);return i?r|a<<8|s<<16:r<<16|a<<8|s},we=(e,t,i,r)=>{i>>>=0,i&=16777215,e.setUint8(t,i>>>16&255),e.setUint8(t+1,i>>>8&255),e.setUint8(t+2,255&i)},Te=(e,t)=>({async next(){const i=await e.next();return i.done?{value:void 0,done:!0}:{value:t(i.value),done:!1}},return:()=>e.return(),throw:t=>e.throw(t),[Symbol.asyncIterator](){return this}}),Se=(e,t,i)=>Math.max(t,Math.min(i,e)),ve="und",Pe=e=>{const t=Math.round(e);return Math.abs(e/t-1)<10*Number.EPSILON?t:e},Ie=(e,t)=>Math.round(e/t)*t,xe=(e,t)=>Math.round(e*t)/t,Ce=(e,t)=>Math.floor(e/t)*t,Be=e=>{let t=0;for(;0!==e;)e&=e-1,t++;return t},Ee=/^[a-z]{3}$/,_e=e=>Ee.test(e),Re=1e6*(1+Number.EPSILON),Fe=(e,t)=>{const i={...e,...t};if(e.headers||t.headers){const r=e.headers?Ae(e.headers):{},a=t.headers?Ae(t.headers):{},s={...r};Object.entries(a).forEach(([e,t])=>{const i=Object.keys(s).find(t=>t.toLowerCase()===e.toLowerCase());i&&delete s[i],s[e]=t}),i.headers=s}return i},Ae=e=>{if(e instanceof Headers){const t={};return e.forEach((e,i)=>{t[i]=e}),t}if(Array.isArray(e)){const t={};return e.forEach(([e,i])=>{t[e]=i}),t}return e},Me=async(e,t,i,r,a)=>{lLine truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i,r,a,s,n,o,c,l,u,h,d,f,m,p,g,k,y,b,w,T,S,v,P,I,x,C,B,E,_,R,F,A=e=>{throw TypeError(e)},M=(e,t,i)=>t.has(e)||A("Cannot "+i),D=(e,t,i)=>(M(e,t,"read from private field"),i?i.call(e):t.get(e)),N=(e,t,i)=>t.has(e)?A("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,i),O=(e,t,i,r)=>(M(e,t,"write to private field"),r?r.call(e,i):t.set(e,i),i),L=(e,t,i)=>(M(e,t,"access private method"),i),z=(e,t,i,r)=>({set _(r){O(e,t,r,i)},get _(){return D(e,t,r)}});function U(e){const t=[];for(const r of e)try{r()}catch(i){t.push(i)}if(t.length)throw t[0]}function q(e,t,i){const r={control:!1,setting:!1};let a=null;function s(i){r[i]&&(r[i]=!1,"control"===i?e.controls.cache.has(t)&&e.controls.remove(t):e.setting.find(t)&&e.setting.remove(t))}function n(){a=null,U([()=>s("control"),()=>s("setting")])}return{update:function(o,c,l,u){if(!o)return void n();a=o;const h=()=>a===o&&l(),d=async e=>h()&&o.selector.some(t=>t.value===e.value)?u(e,o.title):e.html;c.control?(r.control=!0,e.controls.update({name:t,position:"right",html:o.html,style:{padding:"0 10px"},selector:o.selector,onSelect:d})):s("control"),h()&&(c.setting?(r.setting=!0,e.setting.update({name:t,tooltip:o.html,html:o.title,icon:i,width:200,selector:o.selector,onSelect:d})):s("setting"))},clear:n}}function W(e){const t=new Map,i=[];for(const r of e){const e=t.get(r.html);void 0!==r.html&&void 0!==e?r.default&&(i[e]=r):(t.set(r.html,i.length),i.push(r))}return i}function H({art:e,shim:t,option:i}){if(!i.m3u8)return;let r=!1,a=0,s=0;const n=q(e,"mediabunny-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),o=q(e,"mediabunny-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),c=e=>!r&&!t.engine.destroyed&&t.engine.loadSeq===e;async function l(){if(r||t.engine.destroyed)return;const e=++a,s=t.engine.loadSeq,l=()=>e===a&&c(s);try{const e=await t.getM3u8State();if(!l())return;const r=i.m3u8?.quality||{},a=function(e,t,i){if(!t.control&&!t.setting||!e?.levels.length)return null;const r=t.auto||"Auto",a=t.getName||(e=>e.name||`${e.height}P`),s=e.currentLevel?a(e.currentLevel):r,n=[];for(const l of e.levels){if(!i())return null;n.push({html:a(l),value:l.id,default:e.currentLevel?.id===l.id})}const o=new Map(e.levels.map(e=>[e.id,e.height])),c=W(n).sort((e,t)=>("auto"===t.value?0:o.get(t.value)||0)-("auto"===e.value?0:o.get(e.value)||0));return c.push({html:r,value:"auto",default:!e.currentLevel}),{title:t.title||"Quality",html:s,selector:c}}(e,r,l);if(!l())return;if(n.update(a,r,()=>c(s),(e,t)=>u("quality",e,t)),!l())return;const h=i.m3u8?.audio||{},d=function(e,t,i){if(!t.control&&!t.setting||!e||e.audios.length<2)return null;const r=t.auto||"Auto",a=t.getName||(e=>e.name||e.lang||e.language),s=e.currentAudio?a(e.currentAudio):r,n=[];for(const c of e.audios){if(!i())return null;n.push({html:a(c),value:c.id,default:e.currentAudio?.id===c.id})}const o=W(n);return o.push({html:r,value:"auto",default:!e.currentAudio}),{title:t.title||"Audio",html:s,selector:o}}(e,h,l);l()&&o.update(d,h,()=>c(s),(e,t)=>u("audio",e,t))}catch(h){if(l())throw h}}async function u(i,r,n){const o=++s,u=t.engine.loadSeq;a++;const d=()=>o===s&&c(u);try{"quality"===i?await t.switchM3u8Quality(r.value):await t.switchM3u8Audio(r.value),d()&&(await l(),d()&&(e.notice.show=`${n}: ${r.html}`))}catch(f){if(d()&&(await h(),d()))throw f}return r.html}function h(){return l().catch(e=>console.warn("MediaBunny HLS menu update:",e))}function d(){a++,s++;try{U([n.clear,o.clear])}catch(e){console.warn("MediaBunny HLS menu cleanup:",e)}}return e.on("video:loadedmetadata",h),e.on("restart",h),e.on("video:loadstart",d),e.on("video:error",d),e.on("destroy",function t(){if(!r){r=!0,d();try{U([()=>e.off("video:loadedmetadata",h),()=>e.off("restart",h),()=>e.off("video:loadstart",d),()=>e.off("video:error",d),()=>e.off("destLine truncated
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i,r,n,s,a,o,l,c,u,d,h,f,m,p,g,k,y,b,T,v,w,S,P,I,x,C,B,E,_,R,F,A=Object.defineProperty,M=Object.defineProperties,D=Object.getOwnPropertyDescriptors,N=Object.getOwnPropertySymbols,O=Object.prototype.hasOwnProperty,z=Object.prototype.propertyIsEnumerable,L=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),U=e=>{throw TypeError(e)},q=Math.pow,W=(e,t,i)=>t in e?A(e,t,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[t]=i,H=(e,t)=>{for(var i in t||(t={}))O.call(t,i)&&W(e,i,t[i]);if(N)for(var i of N(t))z.call(t,i)&&W(e,i,t[i]);return e},V=(e,t)=>M(e,D(t)),j=(e,t,i)=>t.has(e)||U("Cannot "+i),K=(e,t,i)=>(j(e,t,"read from private field"),i?i.call(e):t.get(e)),X=(e,t,i)=>t.has(e)?U("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,i),$=(e,t,i,r)=>(j(e,t,"write to private field"),r?r.call(e,i):t.set(e,i),i),G=(e,t,i)=>(j(e,t,"access private method"),i),Q=(e,t,i,r)=>({set _(r){$(e,t,r,i)},get _(){return K(e,t,r)}}),Y=(e,t,i)=>new Promise((r,n)=>{var s=e=>{try{o(i.next(e))}catch(t){n(t)}},a=e=>{try{o(i.throw(e))}catch(t){n(t)}},o=e=>e.done?r(e.value):Promise.resolve(e.value).then(s,a);o((i=i.apply(e,t)).next())}),J=function(e,t){this[0]=e,this[1]=t},Z=(e,t,i)=>{var r,n=(e,t,r,s)=>{try{var a=i[e](t),o=(t=a.value)instanceof J,l=a.done;Promise.resolve(o?t[0]:t).then(i=>o?n("return"===e?e:"next",t[1]?{done:i.done,value:i.value}:i,r,s):r({value:i,done:l})).catch(e=>n("throw",e,r,s))}catch(c){s(c)}},s=(e,t,i,s)=>a[e]=a=>(t=new Promise((t,i,s)=>(s=()=>n(e,a,t,i),r?r.then(s):s())),s=()=>r===i&&(r=0),r=i=t.then(s,s),t),a={};return i=i.apply(e,t),a[L("asyncIterator")]=()=>a,s("next"),s("throw"),s("return"),a},ee=e=>{var t,i=e[L("asyncIterator")],r=!1,n={};return null==i?(i=e[L("iterator")](),t=e=>n[e]=t=>i[e](t)):(i=i.call(e),t=e=>n[e]=t=>{if(r){if(r=!1,"throw"===e)throw t;return t}return r=!0,{done:!1,value:new J(new Promise(r=>{var n=i[e](t);n instanceof Object||U("Object expected"),r(n)}),1)}}),n[L("iterator")]=()=>n,t("next"),"throw"in i?t("throw"):n.throw=e=>{throw e},"return"in i&&t("return"),n},te=(e,t,i)=>(t=e[L("asyncIterator")])?t.call(e):(e=e[L("iterator")](),t={},(i=(i,r)=>(r=e[i])&&(t[i]=t=>new Promise((i,n,s)=>(t=r.call(e,t),s=t.done,Promise.resolve(t.value).then(e=>i({value:e,done:s}),n)))))("next"),i("return"),t);function ie(e,t){const i=new Map;return e.filter(e=>{const r=e[t];return void 0===r||!i.has(r)&&i.set(r,1)})}class re{constructor(){X(this,e,!1),this.listeners=new Map}addEventListener(t,i){var r;K(this,e)||(this.listeners.has(t)||this.listeners.set(t,[]),null==(r=this.listeners.get(t))||r.push(i))}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(t,i){var r;if(K(this,e))return;const n=Object.assign(new Event(t),{detail:i});null==(r=this.listeners.get(t))||r.forEach(t=>{K(this,e)||t(n)})}destroy(){$(this,e,!0),this.listeners.clear()}}function ne(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const se=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},ae=e=>e&&e[e.length-1],oe=e=>{let t=0;for(;0===e.readBits(1)&&t<32;)t++;if(t>=32)throw new Error("Invalid exponential-Golomb code.");return(1<<t)-1+e.readBits(t)},le=(e,t)=>{const i=t+1,r=Math.floor(Math.log2(i));e.writeBits(r,0),e.writeBits(1,1),e.writeBits(r,i-q(2,r))},ce=e=>{const t=oe(e);return 0===(1&t)?-(t>>1):t+1>>1},ue=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),de=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),he=new TextDecoder,fe=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),me={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},pe=fe(me),ge={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},ke=fe(ge),ye={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},be=fe(ye),Te=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,ve={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},we=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class Se{constructor(){this.currentPromise=Promise.resolve(),this.pending=0}acquire(){return Y(this,null,function*(){let e;const t=new Promise(t=>{let i=!1;e=()=>{i||(t(),this.pending--,i=!0)}}),i=this.currentPromise;return this.currentPromise=t,this.pending++,yield i,e})}}const Pe=/^[0-9a-fA-F]+$/,Ie=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),xe=e=>(e=(e=(e=(e=(e=e>>1&1431655765|(1431655765&e)<<1)>>2&858993459|(858993459&e)<<Line truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i,r,n,s,a,o,l,c,u,d,h,f,m,p,g,k,y,b,T,v,w,S,P,I,x,C,B,E,_,R,F,A=Object.defineProperty,M=Object.defineProperties,D=Object.getOwnPropertyDescriptors,N=Object.getOwnPropertySymbols,O=Object.prototype.hasOwnProperty,L=Object.prototype.propertyIsEnumerable,z=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),U=e=>{throw TypeError(e)},q=Math.pow,W=(e,t,i)=>t in e?A(e,t,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[t]=i,H=(e,t)=>{for(var i in t||(t={}))O.call(t,i)&&W(e,i,t[i]);if(N)for(var i of N(t))L.call(t,i)&&W(e,i,t[i]);return e},V=(e,t)=>M(e,D(t)),j=(e,t,i)=>t.has(e)||U("Cannot "+i),K=(e,t,i)=>(j(e,t,"read from private field"),i?i.call(e):t.get(e)),X=(e,t,i)=>t.has(e)?U("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,i),$=(e,t,i,r)=>(j(e,t,"write to private field"),r?r.call(e,i):t.set(e,i),i),G=(e,t,i)=>(j(e,t,"access private method"),i),Q=(e,t,i,r)=>({set _(r){$(e,t,r,i)},get _(){return K(e,t,r)}}),Y=(e,t,i)=>new Promise((r,n)=>{var s=e=>{try{o(i.next(e))}catch(t){n(t)}},a=e=>{try{o(i.throw(e))}catch(t){n(t)}},o=e=>e.done?r(e.value):Promise.resolve(e.value).then(s,a);o((i=i.apply(e,t)).next())}),J=function(e,t){this[0]=e,this[1]=t},Z=(e,t,i)=>{var r,n=(e,t,r,s)=>{try{var a=i[e](t),o=(t=a.value)instanceof J,l=a.done;Promise.resolve(o?t[0]:t).then(i=>o?n("return"===e?e:"next",t[1]?{done:i.done,value:i.value}:i,r,s):r({value:i,done:l})).catch(e=>n("throw",e,r,s))}catch(c){s(c)}},s=(e,t,i,s)=>a[e]=a=>(t=new Promise((t,i,s)=>(s=()=>n(e,a,t,i),r?r.then(s):s())),s=()=>r===i&&(r=0),r=i=t.then(s,s),t),a={};return i=i.apply(e,t),a[z("asyncIterator")]=()=>a,s("next"),s("throw"),s("return"),a},ee=e=>{var t,i=e[z("asyncIterator")],r=!1,n={};return null==i?(i=e[z("iterator")](),t=e=>n[e]=t=>i[e](t)):(i=i.call(e),t=e=>n[e]=t=>{if(r){if(r=!1,"throw"===e)throw t;return t}return r=!0,{done:!1,value:new J(new Promise(r=>{var n=i[e](t);n instanceof Object||U("Object expected"),r(n)}),1)}}),n[z("iterator")]=()=>n,t("next"),"throw"in i?t("throw"):n.throw=e=>{throw e},"return"in i&&t("return"),n},te=(e,t,i)=>(t=e[z("asyncIterator")])?t.call(e):(e=e[z("iterator")](),t={},(i=(i,r)=>(r=e[i])&&(t[i]=t=>new Promise((i,n,s)=>(t=r.call(e,t),s=t.done,Promise.resolve(t.value).then(e=>i({value:e,done:s}),n)))))("next"),i("return"),t);function ie(e){const t=[];for(const r of e)try{r()}catch(i){t.push(i)}if(t.length)throw t[0]}function re(e,t,i){const r={control:!1,setting:!1};let n=null;function s(i){r[i]&&(r[i]=!1,"control"===i?e.controls.cache.has(t)&&e.controls.remove(t):e.setting.find(t)&&e.setting.remove(t))}function a(){n=null,ie([()=>s("control"),()=>s("setting")])}return{update:function(o,l,c,u){if(!o)return void a();n=o;const d=()=>n===o&&c(),h=e=>Y(null,null,function*(){return d()&&o.selector.some(t=>t.value===e.value)?u(e,o.title):e.html});l.control?(r.control=!0,e.controls.update({name:t,position:"right",html:o.html,style:{padding:"0 10px"},selector:o.selector,onSelect:h})):s("control"),d()&&(l.setting?(r.setting=!0,e.setting.update({name:t,tooltip:o.html,html:o.title,icon:i,width:200,selector:o.selector,onSelect:h})):s("setting"))},clear:a}}function ne(e){const t=new Map,i=[];for(const r of e){const e=t.get(r.html);void 0!==r.html&&void 0!==e?r.default&&(i[e]=r):(t.set(r.html,i.length),i.push(r))}return i}function se({art:e,shim:t,option:i}){if(!i.m3u8)return;let r=!1,n=0,s=0;const a=re(e,"mediabunny-quality",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>'),o=re(e,"mediabunny-audio",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'),l=e=>!r&&!t.engine.destroyed&&t.engine.loadSeq===e;function c(){return Y(this,null,function*(){var e,s;if(r||t.engine.destroyed)return;const c=++n,d=t.engine.loadSeq,h=()=>c===n&&l(d);try{const r=yield t.getM3u8State();if(!h())return;const n=(null==(e=i.m3u8)?void 0:e.quality)||{},c=function(e,t,i){vaLine truncated
|
||||
+267
-207
@@ -23,164 +23,206 @@ var __privateWrapper = (obj, member, setter, getter) => ({
|
||||
var _closed, _closed2, _generation, _contexts, _nodes, _pump, _playTask, _playing, _loading, _onError, _AudioEngine_instances, load_fn, startPump_fn, play_fn, _generation2, _iteration, _loading2, _createSink, _usedSink, _destroyed, _preparing, _poster, _renderer, _onError2, _VideoEngine_instances, current_fn, context_fn, report_fn, release_fn, draw_fn, load_fn2, reset_fn, _playback;
|
||||
const $audio = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>';
|
||||
const $quality = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
|
||||
function uniqBy(array, property) {
|
||||
const seen = /* @__PURE__ */ new Map();
|
||||
return array.filter((item) => {
|
||||
const key = item[property];
|
||||
if (key === void 0) {
|
||||
return true;
|
||||
function releaseAll(actions) {
|
||||
const failures = [];
|
||||
for (const action of actions) {
|
||||
try {
|
||||
action();
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
return !seen.has(key) && seen.set(key, 1);
|
||||
});
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0];
|
||||
}
|
||||
function createMenu(art, name, icon) {
|
||||
const owned = { control: false, setting: false };
|
||||
let model = null;
|
||||
function remove(surface) {
|
||||
if (!owned[surface])
|
||||
return;
|
||||
owned[surface] = false;
|
||||
if (surface === "control") {
|
||||
if (art.controls.cache.has(name))
|
||||
art.controls.remove(name);
|
||||
} else if (art.setting.find(name)) {
|
||||
art.setting.remove(name);
|
||||
}
|
||||
}
|
||||
function clear() {
|
||||
model = null;
|
||||
releaseAll([() => remove("control"), () => remove("setting")]);
|
||||
}
|
||||
function update(next, config, active, select2) {
|
||||
if (!next) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
model = next;
|
||||
const valid = () => model === next && active();
|
||||
const onSelect = async (item) => {
|
||||
if (!valid() || !next.selector.some((entry) => entry.value === item.value))
|
||||
return item.html;
|
||||
return select2(item, next.title);
|
||||
};
|
||||
if (config.control) {
|
||||
owned.control = true;
|
||||
art.controls.update({ name, position: "right", html: next.html, style: { padding: "0 10px" }, selector: next.selector, onSelect });
|
||||
} else {
|
||||
remove("control");
|
||||
}
|
||||
if (!valid())
|
||||
return;
|
||||
if (config.setting) {
|
||||
owned.setting = true;
|
||||
art.setting.update({ name, tooltip: next.html, html: next.title, icon, width: 200, selector: next.selector, onSelect });
|
||||
} else {
|
||||
remove("setting");
|
||||
}
|
||||
}
|
||||
return { update, clear };
|
||||
}
|
||||
function unique(items) {
|
||||
const positions = /* @__PURE__ */ new Map();
|
||||
const output = [];
|
||||
for (const item of items) {
|
||||
const position = positions.get(item.html);
|
||||
if (item.html !== void 0 && position !== void 0) {
|
||||
if (item.default)
|
||||
output[position] = item;
|
||||
} else {
|
||||
positions.set(item.html, output.length);
|
||||
output.push(item);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
function qualityModel(state2, config, active) {
|
||||
if (!config.control && !config.setting || !state2?.levels.length)
|
||||
return null;
|
||||
const auto = config.auto || "Auto";
|
||||
const getName = config.getName || ((level) => level.name || `${level.height}P`);
|
||||
const html = state2.currentLevel ? getName(state2.currentLevel) : auto;
|
||||
const items = [];
|
||||
for (const level of state2.levels) {
|
||||
if (!active())
|
||||
return null;
|
||||
items.push({ html: getName(level), value: level.id, default: state2.currentLevel?.id === level.id });
|
||||
}
|
||||
const heights = new Map(state2.levels.map((level) => [level.id, level.height]));
|
||||
const selector = unique(items).sort((left, right) => (right.value === "auto" ? 0 : heights.get(right.value) || 0) - (left.value === "auto" ? 0 : heights.get(left.value) || 0));
|
||||
selector.push({ html: auto, value: "auto", default: !state2.currentLevel });
|
||||
return { title: config.title || "Quality", html, selector };
|
||||
}
|
||||
function audioModel(state2, config, active) {
|
||||
if (!config.control && !config.setting || !state2 || state2.audios.length < 2)
|
||||
return null;
|
||||
const auto = config.auto || "Auto";
|
||||
const getName = config.getName || ((track) => track.name || track.lang || track.language);
|
||||
const html = state2.currentAudio ? getName(state2.currentAudio) : auto;
|
||||
const items = [];
|
||||
for (const track of state2.audios) {
|
||||
if (!active())
|
||||
return null;
|
||||
items.push({ html: getName(track), value: track.id, default: state2.currentAudio?.id === track.id });
|
||||
}
|
||||
const selector = unique(items);
|
||||
selector.push({ html: auto, value: "auto", default: !state2.currentAudio });
|
||||
return { title: config.title || "Audio", html, selector };
|
||||
}
|
||||
function setupM3u8Controls({ art, shim, option }) {
|
||||
if (!option.m3u8)
|
||||
return;
|
||||
function removeControl(name) {
|
||||
if (art.controls.cache.has(name)) {
|
||||
art.controls.remove(name);
|
||||
}
|
||||
}
|
||||
function removeSetting(name) {
|
||||
if (art.setting.find(name)) {
|
||||
art.setting.remove(name);
|
||||
}
|
||||
}
|
||||
function clearQuality() {
|
||||
removeControl("mediabunny-quality");
|
||||
removeSetting("mediabunny-quality");
|
||||
}
|
||||
function clearAudio() {
|
||||
removeControl("mediabunny-audio");
|
||||
removeSetting("mediabunny-audio");
|
||||
}
|
||||
function canRender(config = {}) {
|
||||
return config.control || config.setting;
|
||||
}
|
||||
async function updateQuality(state2) {
|
||||
const config = option.m3u8?.quality || {};
|
||||
if (!canRender(config) || !state2?.levels.length) {
|
||||
clearQuality();
|
||||
return;
|
||||
}
|
||||
const auto = config.auto || "Auto";
|
||||
const title = config.title || "Quality";
|
||||
const getName = config.getName || ((level) => level.name || `${level.height}P`);
|
||||
const defaultHtml = state2.currentLevel ? getName(state2.currentLevel) : auto;
|
||||
const selector = uniqBy(
|
||||
state2.levels.map((item) => {
|
||||
return {
|
||||
html: getName(item),
|
||||
value: item.id,
|
||||
default: state2.currentLevel?.id === item.id
|
||||
};
|
||||
}),
|
||||
"html"
|
||||
).sort((a, b) => {
|
||||
const left = state2.levels.find((item) => item.id === a.value);
|
||||
const right = state2.levels.find((item) => item.id === b.value);
|
||||
return (right?.height || 0) - (left?.height || 0);
|
||||
});
|
||||
selector.push({
|
||||
html: auto,
|
||||
value: "auto",
|
||||
default: !state2.currentLevel
|
||||
});
|
||||
const onSelect = async (item) => {
|
||||
await shim.switchM3u8Quality(item.value);
|
||||
art.notice.show = `${title}: ${item.html}`;
|
||||
await update();
|
||||
return item.html;
|
||||
};
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: "mediabunny-quality",
|
||||
position: "right",
|
||||
html: defaultHtml,
|
||||
style: { padding: "0 10px" },
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: "mediabunny-quality",
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $quality,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
}
|
||||
async function updateAudio(state2) {
|
||||
const config = option.m3u8?.audio || {};
|
||||
if (!canRender(config) || !state2?.audios.length || state2.audios.length < 2) {
|
||||
clearAudio();
|
||||
return;
|
||||
}
|
||||
const auto = config.auto || "Auto";
|
||||
const title = config.title || "Audio";
|
||||
const getName = config.getName || ((track) => track.name || track.lang || track.language);
|
||||
const defaultHtml = state2.currentAudio ? getName(state2.currentAudio) : auto;
|
||||
const selector = uniqBy(
|
||||
state2.audios.map((item) => {
|
||||
return {
|
||||
html: getName(item),
|
||||
value: item.id,
|
||||
default: state2.currentAudio?.id === item.id
|
||||
};
|
||||
}),
|
||||
"html"
|
||||
);
|
||||
selector.push({
|
||||
html: auto,
|
||||
value: "auto",
|
||||
default: !state2.currentAudio
|
||||
});
|
||||
const onSelect = async (item) => {
|
||||
await shim.switchM3u8Audio(item.value);
|
||||
art.notice.show = `${title}: ${item.html}`;
|
||||
await update();
|
||||
return item.html;
|
||||
};
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: "mediabunny-audio",
|
||||
position: "right",
|
||||
html: defaultHtml,
|
||||
style: { padding: "0 10px" },
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: "mediabunny-audio",
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $audio,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect
|
||||
});
|
||||
}
|
||||
}
|
||||
let closed = false;
|
||||
let revision = 0;
|
||||
let selection = 0;
|
||||
const quality2 = createMenu(art, "mediabunny-quality", $quality);
|
||||
const audio2 = createMenu(art, "mediabunny-audio", $audio);
|
||||
const current = (source) => !closed && !shim.engine.destroyed && shim.engine.loadSeq === source;
|
||||
async function update() {
|
||||
const state2 = await shim.getM3u8State();
|
||||
if (!state2) {
|
||||
clearQuality();
|
||||
clearAudio();
|
||||
if (closed || shim.engine.destroyed)
|
||||
return;
|
||||
const version = ++revision;
|
||||
const source = shim.engine.loadSeq;
|
||||
const active = () => version === revision && current(source);
|
||||
try {
|
||||
const state2 = await shim.getM3u8State();
|
||||
if (!active())
|
||||
return;
|
||||
const qualityConfig = option.m3u8?.quality || {};
|
||||
const qualityView = qualityModel(state2, qualityConfig, active);
|
||||
if (!active())
|
||||
return;
|
||||
quality2.update(qualityView, qualityConfig, () => current(source), (item, title) => select2("quality", item, title));
|
||||
if (!active())
|
||||
return;
|
||||
const audioConfig = option.m3u8?.audio || {};
|
||||
const audioView = audioModel(state2, audioConfig, active);
|
||||
if (active())
|
||||
audio2.update(audioView, audioConfig, () => current(source), (item, title) => select2("audio", item, title));
|
||||
} catch (error) {
|
||||
if (active())
|
||||
throw error;
|
||||
}
|
||||
await Promise.all([
|
||||
updateQuality(state2),
|
||||
updateAudio(state2)
|
||||
]);
|
||||
}
|
||||
art.on("video:loadedmetadata", update);
|
||||
art.on("restart", update);
|
||||
async function select2(kind, item, title) {
|
||||
const intent = ++selection;
|
||||
const source = shim.engine.loadSeq;
|
||||
revision++;
|
||||
const active = () => intent === selection && current(source);
|
||||
try {
|
||||
if (kind === "quality")
|
||||
await shim.switchM3u8Quality(item.value);
|
||||
else
|
||||
await shim.switchM3u8Audio(item.value);
|
||||
if (active()) {
|
||||
await update();
|
||||
if (active())
|
||||
art.notice.show = `${title}: ${item.html}`;
|
||||
}
|
||||
} catch (error) {
|
||||
if (active()) {
|
||||
await refresh();
|
||||
if (active())
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return item.html;
|
||||
}
|
||||
function refresh() {
|
||||
return update().catch((error) => console.warn("MediaBunny HLS menu update:", error));
|
||||
}
|
||||
function invalidate() {
|
||||
revision++;
|
||||
selection++;
|
||||
try {
|
||||
releaseAll([quality2.clear, audio2.clear]);
|
||||
} catch (error) {
|
||||
console.warn("MediaBunny HLS menu cleanup:", error);
|
||||
}
|
||||
}
|
||||
function destroy() {
|
||||
if (closed)
|
||||
return;
|
||||
closed = true;
|
||||
invalidate();
|
||||
try {
|
||||
releaseAll([
|
||||
() => art.off("video:loadedmetadata", refresh),
|
||||
() => art.off("restart", refresh),
|
||||
() => art.off("video:loadstart", invalidate),
|
||||
() => art.off("video:error", invalidate),
|
||||
() => art.off("destroy", destroy)
|
||||
]);
|
||||
} catch (error) {
|
||||
console.warn("MediaBunny HLS listener cleanup:", error);
|
||||
}
|
||||
}
|
||||
art.on("video:loadedmetadata", refresh);
|
||||
art.on("restart", refresh);
|
||||
art.on("video:loadstart", invalidate);
|
||||
art.on("video:error", invalidate);
|
||||
art.on("destroy", destroy);
|
||||
return { update };
|
||||
}
|
||||
class EventTarget {
|
||||
@@ -24301,15 +24343,15 @@ function publish(events, current, names) {
|
||||
}
|
||||
function metadataBarrier(ready, current) {
|
||||
let video = false;
|
||||
let audio = false;
|
||||
let audio2 = false;
|
||||
let published = false;
|
||||
function complete(part) {
|
||||
if (!current() || published)
|
||||
return;
|
||||
if (part === "video")
|
||||
video = true;
|
||||
else audio = true;
|
||||
if (video && audio) {
|
||||
else audio2 = true;
|
||||
if (video && audio2) {
|
||||
published = true;
|
||||
ready();
|
||||
}
|
||||
@@ -24651,61 +24693,78 @@ play_fn = async function(task, generation) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
async function selectQuality(host, value) {
|
||||
const { input, media, loadSeq } = host;
|
||||
if (!media?.isHls || !input || host.destroyed)
|
||||
const requests = /* @__PURE__ */ new WeakMap();
|
||||
async function select(host, value, resolve) {
|
||||
const { input, loadSeq } = host;
|
||||
if (!host.media?.isHls || !input || host.destroyed)
|
||||
return;
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && host.media === media;
|
||||
let selection;
|
||||
try {
|
||||
const tracks = await input.getVideoTracks();
|
||||
if (!current())
|
||||
const token = {};
|
||||
requests.set(host, token);
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && requests.get(host) === token;
|
||||
while (current()) {
|
||||
const media = host.media;
|
||||
if (!media?.isHls)
|
||||
return;
|
||||
const videoTrack = value === "auto" ? await input.getPrimaryVideoTrack() : tracks.find((track) => track.id === value) ?? media.videoTrack;
|
||||
if (!current() || !videoTrack)
|
||||
return;
|
||||
let audioTrack = media.audioTrack;
|
||||
let audioMode = media.audioMode;
|
||||
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
||||
audioTrack = await videoTrack.getPrimaryPairableAudioTrack();
|
||||
audioMode = "auto";
|
||||
let selection;
|
||||
try {
|
||||
selection = await resolve(input, media, value, current);
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
if (host.media === media)
|
||||
throw error;
|
||||
continue;
|
||||
}
|
||||
selection = { videoTrack, audioTrack, videoMode: value === "auto" ? "auto" : "manual", audioMode };
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
throw error;
|
||||
if (host.media !== media)
|
||||
continue;
|
||||
if (selection) {
|
||||
try {
|
||||
await host.replaceTracks(selection);
|
||||
} catch (error) {
|
||||
if (current())
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (current())
|
||||
await host.replaceTracks(selection);
|
||||
}
|
||||
async function selectAudio(host, value) {
|
||||
const { input, media, loadSeq } = host;
|
||||
if (!media?.isHls || !input || host.destroyed)
|
||||
return;
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && host.media === media;
|
||||
let selection;
|
||||
try {
|
||||
const tracks = await input.getAudioTracks();
|
||||
if (!current())
|
||||
return;
|
||||
const audioTrack = value === "auto" ? media.videoTrack ? await media.videoTrack.getPrimaryPairableAudioTrack() : await input.getPrimaryAudioTrack() : tracks.find((track) => track.id === value) ?? media.audioTrack;
|
||||
if (!current() || !audioTrack)
|
||||
return;
|
||||
let videoTrack = media.videoTrack;
|
||||
let videoMode = media.videoMode;
|
||||
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
||||
videoTrack = await audioTrack.getPrimaryPairableVideoTrack();
|
||||
videoMode = "auto";
|
||||
}
|
||||
selection = { videoTrack, audioTrack, videoMode, audioMode: value === "auto" ? "auto" : "manual" };
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
throw error;
|
||||
async function quality(input, media, value, current) {
|
||||
const tracks = await input.getVideoTracks();
|
||||
if (!current())
|
||||
return null;
|
||||
const videoTrack = value === "auto" ? await input.getPrimaryVideoTrack() : tracks.find((track) => track.id === value) ?? media.videoTrack;
|
||||
if (!current() || !videoTrack)
|
||||
return null;
|
||||
let audioTrack = media.audioTrack;
|
||||
let audioMode = media.audioMode;
|
||||
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
||||
audioTrack = await videoTrack.getPrimaryPairableAudioTrack();
|
||||
audioMode = "auto";
|
||||
}
|
||||
if (current())
|
||||
await host.replaceTracks(selection);
|
||||
return { videoTrack, audioTrack, videoMode: value === "auto" ? "auto" : "manual", audioMode };
|
||||
}
|
||||
async function audio(input, media, value, current) {
|
||||
const tracks = await input.getAudioTracks();
|
||||
if (!current())
|
||||
return null;
|
||||
const audioTrack = value === "auto" ? media.videoTrack ? await media.videoTrack.getPrimaryPairableAudioTrack() : await input.getPrimaryAudioTrack() : tracks.find((track) => track.id === value) ?? media.audioTrack;
|
||||
if (!current() || !audioTrack)
|
||||
return null;
|
||||
let videoTrack = media.videoTrack;
|
||||
let videoMode = media.videoMode;
|
||||
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
||||
videoTrack = await audioTrack.getPrimaryPairableVideoTrack();
|
||||
videoMode = "auto";
|
||||
}
|
||||
return { videoTrack, audioTrack, videoMode, audioMode: value === "auto" ? "auto" : "manual" };
|
||||
}
|
||||
function selectQuality(host, value) {
|
||||
return select(host, value, quality);
|
||||
}
|
||||
function selectAudio(host, value) {
|
||||
return select(host, value, audio);
|
||||
}
|
||||
async function getTrackBitrate(track) {
|
||||
return await track.getAverageBitrate() ?? await track.getBitrate() ?? 0;
|
||||
@@ -24985,9 +25044,9 @@ class Playback {
|
||||
}
|
||||
seek(time, resume) {
|
||||
return this.position(async (current) => {
|
||||
const audio = this.host.audio.seek(time);
|
||||
const audio2 = this.host.audio.seek(time);
|
||||
const video = current() ? this.host.video.seek(time) : Promise.resolve();
|
||||
await Promise.all([audio, video]);
|
||||
await Promise.all([audio2, video]);
|
||||
}, ["seeked"], false, resume);
|
||||
}
|
||||
replace(media, time) {
|
||||
@@ -25438,8 +25497,8 @@ class VideoEngine {
|
||||
render() {
|
||||
__privateGet(this, _renderer).render();
|
||||
}
|
||||
start(audio) {
|
||||
__privateGet(this, _renderer).start(audio);
|
||||
start(audio2) {
|
||||
__privateGet(this, _renderer).start(audio2);
|
||||
}
|
||||
stop() {
|
||||
__privateGet(this, _renderer).stop();
|
||||
@@ -25692,8 +25751,8 @@ class MediaBunnyEngine {
|
||||
}, current);
|
||||
try {
|
||||
const video = this.video.load(media, metadata.video);
|
||||
const audio = current() ? this.audio.load(media, metadata.audio) : Promise.resolve();
|
||||
await Promise.all([video, audio]);
|
||||
const audio2 = current() ? this.audio.load(media, metadata.audio) : Promise.resolve();
|
||||
await Promise.all([video, audio2]);
|
||||
if (!current())
|
||||
return;
|
||||
this.readyState = 4;
|
||||
@@ -25729,13 +25788,14 @@ class MediaBunnyEngine {
|
||||
}
|
||||
async getHlsState() {
|
||||
const media = this.media;
|
||||
const source = this.loadSeq;
|
||||
if (this.destroyed)
|
||||
return null;
|
||||
try {
|
||||
const state2 = await getHlsState(media);
|
||||
return !this.destroyed && this.media === media ? state2 : null;
|
||||
return !this.destroyed && this.loadSeq === source && this.media === media ? state2 : null;
|
||||
} catch (error) {
|
||||
if (this.destroyed || this.media !== media)
|
||||
if (this.destroyed || this.loadSeq !== source || this.media !== media)
|
||||
return null;
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -199,14 +199,15 @@ export default class MediaBunnyEngine implements EnginePort {
|
||||
|
||||
async getHlsState() {
|
||||
const media = this.media
|
||||
const source = this.loadSeq
|
||||
if (this.destroyed)
|
||||
return null
|
||||
try {
|
||||
const state = await getHlsState(media)
|
||||
return !this.destroyed && this.media === media ? state : null
|
||||
return !this.destroyed && this.loadSeq === source && this.media === media ? state : null
|
||||
}
|
||||
catch (error) {
|
||||
if (this.destroyed || this.media !== media)
|
||||
if (this.destroyed || this.loadSeq !== source || this.media !== media)
|
||||
return null
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface EngineOptions {
|
||||
|
||||
// Stable coordinator-facing surface used by the public shim.
|
||||
export interface EnginePort {
|
||||
readonly loadSeq: number
|
||||
readonly destroyed: boolean
|
||||
readonly currentTime: number
|
||||
readonly duration: number
|
||||
readonly videoWidth: number
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { Input } from 'mediabunny'
|
||||
import type { PlaybackMedia } from './media'
|
||||
|
||||
export type SelectedTracks = Pick<PlaybackMedia, 'videoTrack' | 'audioTrack' | 'videoMode' | 'audioMode'>
|
||||
|
||||
interface SelectionHost {
|
||||
input: Input | null
|
||||
media: PlaybackMedia | null
|
||||
@@ -10,65 +9,86 @@ interface SelectionHost {
|
||||
loadSeq: number
|
||||
replaceTracks: (tracks: SelectedTracks) => Promise<void>
|
||||
}
|
||||
type ResolveTracks = (input: Input, media: PlaybackMedia, value: unknown, current: () => boolean) => Promise<SelectedTracks | null>
|
||||
|
||||
export async function selectQuality(host: SelectionHost, value: unknown): Promise<void> {
|
||||
const { input, media, loadSeq } = host
|
||||
if (!media?.isHls || !input || host.destroyed)
|
||||
const requests = new WeakMap<SelectionHost, object>()
|
||||
async function select(host: SelectionHost, value: unknown, resolve: ResolveTracks): Promise<void> {
|
||||
const { input, loadSeq } = host
|
||||
if (!host.media?.isHls || !input || host.destroyed)
|
||||
return
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && host.media === media
|
||||
let selection: SelectedTracks
|
||||
try {
|
||||
const tracks = await input.getVideoTracks()
|
||||
if (!current())
|
||||
const token = {}
|
||||
requests.set(host, token)
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && requests.get(host) === token
|
||||
while (current()) {
|
||||
const media: PlaybackMedia | null = host.media
|
||||
if (!media?.isHls)
|
||||
return
|
||||
const videoTrack = value === 'auto' ? await input.getPrimaryVideoTrack() : tracks.find(track => track.id === value) ?? media.videoTrack
|
||||
if (!current() || !videoTrack)
|
||||
return
|
||||
let audioTrack = media.audioTrack
|
||||
let audioMode = media.audioMode
|
||||
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
||||
audioTrack = await videoTrack.getPrimaryPairableAudioTrack()
|
||||
audioMode = 'auto'
|
||||
let selection: SelectedTracks | null
|
||||
try {
|
||||
selection = await resolve(input, media, value, current)
|
||||
}
|
||||
catch (error) {
|
||||
if (!current())
|
||||
return
|
||||
if (host.media === media)
|
||||
throw error
|
||||
continue
|
||||
}
|
||||
selection = { videoTrack, audioTrack, videoMode: value === 'auto' ? 'auto' : 'manual', audioMode }
|
||||
}
|
||||
catch (error) {
|
||||
if (!current())
|
||||
return
|
||||
throw error
|
||||
if (host.media !== media)
|
||||
continue
|
||||
if (selection) {
|
||||
try {
|
||||
await host.replaceTracks(selection)
|
||||
}
|
||||
catch (error) {
|
||||
if (current())
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if (current())
|
||||
await host.replaceTracks(selection)
|
||||
}
|
||||
|
||||
export async function selectAudio(host: SelectionHost, value: unknown): Promise<void> {
|
||||
const { input, media, loadSeq } = host
|
||||
if (!media?.isHls || !input || host.destroyed)
|
||||
return
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && host.media === media
|
||||
let selection: SelectedTracks
|
||||
try {
|
||||
const tracks = await input.getAudioTracks()
|
||||
if (!current())
|
||||
return
|
||||
const audioTrack = value === 'auto'
|
||||
? media.videoTrack ? await media.videoTrack.getPrimaryPairableAudioTrack() : await input.getPrimaryAudioTrack()
|
||||
: tracks.find(track => track.id === value) ?? media.audioTrack
|
||||
if (!current() || !audioTrack)
|
||||
return
|
||||
let videoTrack = media.videoTrack
|
||||
let videoMode = media.videoMode
|
||||
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
||||
videoTrack = await audioTrack.getPrimaryPairableVideoTrack()
|
||||
videoMode = 'auto'
|
||||
}
|
||||
selection = { videoTrack, audioTrack, videoMode, audioMode: value === 'auto' ? 'auto' : 'manual' }
|
||||
async function quality(input: Input, media: PlaybackMedia, value: unknown, current: () => boolean): Promise<SelectedTracks | null> {
|
||||
const tracks = await input.getVideoTracks()
|
||||
if (!current())
|
||||
return null
|
||||
const videoTrack = value === 'auto' ? await input.getPrimaryVideoTrack() : tracks.find(track => track.id === value) ?? media.videoTrack
|
||||
if (!current() || !videoTrack)
|
||||
return null
|
||||
let audioTrack = media.audioTrack
|
||||
let audioMode = media.audioMode
|
||||
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
||||
audioTrack = await videoTrack.getPrimaryPairableAudioTrack()
|
||||
audioMode = 'auto'
|
||||
}
|
||||
catch (error) {
|
||||
if (!current())
|
||||
return
|
||||
throw error
|
||||
}
|
||||
if (current())
|
||||
await host.replaceTracks(selection)
|
||||
return { videoTrack, audioTrack, videoMode: value === 'auto' ? 'auto' : 'manual', audioMode }
|
||||
}
|
||||
|
||||
async function audio(input: Input, media: PlaybackMedia, value: unknown, current: () => boolean): Promise<SelectedTracks | null> {
|
||||
const tracks = await input.getAudioTracks()
|
||||
if (!current())
|
||||
return null
|
||||
const audioTrack = value === 'auto'
|
||||
? media.videoTrack ? await media.videoTrack.getPrimaryPairableAudioTrack() : await input.getPrimaryAudioTrack()
|
||||
: tracks.find(track => track.id === value) ?? media.audioTrack
|
||||
if (!current() || !audioTrack)
|
||||
return null
|
||||
let videoTrack = media.videoTrack
|
||||
let videoMode = media.videoMode
|
||||
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
||||
videoTrack = await audioTrack.getPrimaryPairableVideoTrack()
|
||||
videoMode = 'auto'
|
||||
}
|
||||
return { videoTrack, audioTrack, videoMode, audioMode: value === 'auto' ? 'auto' : 'manual' }
|
||||
}
|
||||
|
||||
export function selectQuality(host: SelectionHost, value: unknown): Promise<void> {
|
||||
return select(host, value, quality)
|
||||
}
|
||||
|
||||
export function selectAudio(host: SelectionHost, value: unknown): Promise<void> {
|
||||
return select(host, value, audio)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { DisplayConfig, Label, MenuHost, MenuModel, SelectorItem } from './m3u8-types'
|
||||
|
||||
export function releaseAll(actions: (() => void)[]): void {
|
||||
const failures: unknown[] = []
|
||||
for (const action of actions) {
|
||||
try {
|
||||
action()
|
||||
}
|
||||
catch (error) { failures.push(error) }
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0]
|
||||
}
|
||||
|
||||
export default function createMenu(art: MenuHost, name: string, icon: string) {
|
||||
const owned = { control: false, setting: false }
|
||||
let model: MenuModel | null = null
|
||||
function remove(surface: 'control' | 'setting'): void {
|
||||
if (!owned[surface])
|
||||
return
|
||||
owned[surface] = false
|
||||
if (surface === 'control') {
|
||||
if (art.controls.cache.has(name))
|
||||
art.controls.remove(name)
|
||||
}
|
||||
else if (art.setting.find(name)) {
|
||||
art.setting.remove(name)
|
||||
}
|
||||
}
|
||||
function clear(): void {
|
||||
model = null
|
||||
releaseAll([() => remove('control'), () => remove('setting')])
|
||||
}
|
||||
function update(next: MenuModel | null, config: DisplayConfig, active: () => boolean, select: (item: SelectorItem, title: string) => Promise<Label>): void {
|
||||
if (!next) {
|
||||
clear()
|
||||
return
|
||||
}
|
||||
model = next
|
||||
const valid = () => model === next && active()
|
||||
const onSelect = async (item: SelectorItem): Promise<Label> => {
|
||||
if (!valid() || !next.selector.some(entry => entry.value === item.value))
|
||||
return item.html
|
||||
return select(item, next.title)
|
||||
}
|
||||
if (config.control) {
|
||||
owned.control = true
|
||||
art.controls.update({ name, position: 'right', html: next.html, style: { padding: '0 10px' }, selector: next.selector, onSelect })
|
||||
}
|
||||
else {
|
||||
remove('control')
|
||||
}
|
||||
if (!valid())
|
||||
return
|
||||
if (config.setting) {
|
||||
owned.setting = true
|
||||
art.setting.update({ name, tooltip: next.html, html: next.title, icon, width: 200, selector: next.selector, onSelect })
|
||||
}
|
||||
else {
|
||||
remove('setting')
|
||||
}
|
||||
}
|
||||
return { update, clear }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { ProxyOptions } from './engine-types'
|
||||
import type { Label, MenuModel, SelectorItem, State } from './m3u8-types'
|
||||
|
||||
type Options = NonNullable<ProxyOptions['m3u8']>
|
||||
|
||||
function unique(items: SelectorItem[]): SelectorItem[] {
|
||||
const positions = new Map<Label, number>()
|
||||
const output: SelectorItem[] = []
|
||||
for (const item of items) {
|
||||
const position = positions.get(item.html)
|
||||
if (item.html !== undefined && position !== undefined) {
|
||||
if (item.default)
|
||||
output[position] = item
|
||||
}
|
||||
else {
|
||||
positions.set(item.html, output.length)
|
||||
output.push(item)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
export function qualityModel(state: State | null, config: NonNullable<Options['quality']>, active: () => boolean): MenuModel | null {
|
||||
if ((!config.control && !config.setting) || !state?.levels.length)
|
||||
return null
|
||||
const auto = config.auto || 'Auto'
|
||||
const getName = config.getName || (level => level.name || `${level.height}P`)
|
||||
const html = state.currentLevel ? getName(state.currentLevel) : auto
|
||||
const items: SelectorItem[] = []
|
||||
for (const level of state.levels) {
|
||||
if (!active())
|
||||
return null
|
||||
items.push({ html: getName(level), value: level.id, default: state.currentLevel?.id === level.id })
|
||||
}
|
||||
const heights = new Map(state.levels.map(level => [level.id, level.height]))
|
||||
const selector = unique(items).sort((left, right) => (right.value === 'auto' ? 0 : heights.get(right.value) || 0) - (left.value === 'auto' ? 0 : heights.get(left.value) || 0))
|
||||
selector.push({ html: auto, value: 'auto', default: !state.currentLevel })
|
||||
return { title: config.title || 'Quality', html, selector }
|
||||
}
|
||||
|
||||
export function audioModel(state: State | null, config: NonNullable<Options['audio']>, active: () => boolean): MenuModel | null {
|
||||
if ((!config.control && !config.setting) || !state || state.audios.length < 2)
|
||||
return null
|
||||
const auto = config.auto || 'Auto'
|
||||
const getName = config.getName || (track => track.name || track.lang || track.language)
|
||||
const html = state.currentAudio ? getName(state.currentAudio) : auto
|
||||
const items: SelectorItem[] = []
|
||||
for (const track of state.audios) {
|
||||
if (!active())
|
||||
return null
|
||||
items.push({ html: getName(track), value: track.id, default: state.currentAudio?.id === track.id })
|
||||
}
|
||||
const selector = unique(items)
|
||||
selector.push({ html: auto, value: 'auto', default: !state.currentAudio })
|
||||
return { title: config.title || 'Audio', html, selector }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { HlsState, ProxyOptions } from './engine-types'
|
||||
|
||||
export type State = NonNullable<HlsState>
|
||||
export type Level = State['levels'][number]
|
||||
export type Audio = State['audios'][number]
|
||||
export type Label = string | null | undefined
|
||||
export interface DisplayConfig {
|
||||
control?: boolean
|
||||
setting?: boolean
|
||||
}
|
||||
export interface SelectorItem {
|
||||
html: Label
|
||||
value: number | 'auto'
|
||||
default: boolean
|
||||
}
|
||||
export interface MenuModel {
|
||||
title: string
|
||||
html: Label
|
||||
selector: SelectorItem[]
|
||||
}
|
||||
export interface MenuOption {
|
||||
name: string
|
||||
html: Label
|
||||
selector: SelectorItem[]
|
||||
onSelect: (item: SelectorItem) => Promise<Label>
|
||||
position?: 'right'
|
||||
style?: { padding: string }
|
||||
tooltip?: Label
|
||||
icon?: string
|
||||
width?: number
|
||||
}
|
||||
interface Registry {
|
||||
update: (menu: MenuOption) => unknown
|
||||
remove: (name: string) => unknown
|
||||
}
|
||||
export interface MenuHost {
|
||||
controls: Registry & { cache: { has: (name: string) => boolean } }
|
||||
setting: Registry & { find: (name: string) => unknown }
|
||||
notice: { show: string }
|
||||
on: (name: string, callback: () => unknown) => unknown
|
||||
off: (name: string, callback: () => unknown) => unknown
|
||||
}
|
||||
export interface MenuShim {
|
||||
engine: { readonly loadSeq: number, readonly destroyed: boolean }
|
||||
getM3u8State: () => Promise<HlsState>
|
||||
switchM3u8Quality: (value: unknown) => Promise<void>
|
||||
switchM3u8Audio: (value: unknown) => Promise<void>
|
||||
}
|
||||
export interface SetupOptions {
|
||||
art: MenuHost
|
||||
shim: MenuShim
|
||||
option: ProxyOptions
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import $audio from '../../artplayer-plugin-hls-control/src/audio.svg?raw'
|
||||
import $quality from '../../artplayer-plugin-hls-control/src/quality.svg?raw'
|
||||
|
||||
function uniqBy(array, property) {
|
||||
const seen = new Map()
|
||||
return array.filter((item) => {
|
||||
const key = item[property]
|
||||
if (key === undefined) {
|
||||
return true
|
||||
}
|
||||
return !seen.has(key) && seen.set(key, 1)
|
||||
})
|
||||
}
|
||||
|
||||
export function setupM3u8Controls({ art, shim, option }) {
|
||||
if (!option.m3u8)
|
||||
return
|
||||
|
||||
function removeControl(name) {
|
||||
if (art.controls.cache.has(name)) {
|
||||
art.controls.remove(name)
|
||||
}
|
||||
}
|
||||
|
||||
function removeSetting(name) {
|
||||
if (art.setting.find(name)) {
|
||||
art.setting.remove(name)
|
||||
}
|
||||
}
|
||||
|
||||
function clearQuality() {
|
||||
removeControl('mediabunny-quality')
|
||||
removeSetting('mediabunny-quality')
|
||||
}
|
||||
|
||||
function clearAudio() {
|
||||
removeControl('mediabunny-audio')
|
||||
removeSetting('mediabunny-audio')
|
||||
}
|
||||
|
||||
function canRender(config = {}) {
|
||||
return config.control || config.setting
|
||||
}
|
||||
|
||||
async function updateQuality(state) {
|
||||
const config = option.m3u8?.quality || {}
|
||||
if (!canRender(config) || !state?.levels.length) {
|
||||
clearQuality()
|
||||
return
|
||||
}
|
||||
|
||||
const auto = config.auto || 'Auto'
|
||||
const title = config.title || 'Quality'
|
||||
const getName = config.getName || (level => level.name || `${level.height}P`)
|
||||
const defaultHtml = state.currentLevel
|
||||
? getName(state.currentLevel)
|
||||
: auto
|
||||
|
||||
const selector = uniqBy(
|
||||
state.levels.map((item) => {
|
||||
return {
|
||||
html: getName(item),
|
||||
value: item.id,
|
||||
default: state.currentLevel?.id === item.id,
|
||||
}
|
||||
}),
|
||||
'html',
|
||||
).sort((a, b) => {
|
||||
const left = state.levels.find(item => item.id === a.value)
|
||||
const right = state.levels.find(item => item.id === b.value)
|
||||
return (right?.height || 0) - (left?.height || 0)
|
||||
})
|
||||
|
||||
selector.push({
|
||||
html: auto,
|
||||
value: 'auto',
|
||||
default: !state.currentLevel,
|
||||
})
|
||||
|
||||
const onSelect = async (item) => {
|
||||
await shim.switchM3u8Quality(item.value)
|
||||
art.notice.show = `${title}: ${item.html}`
|
||||
await update()
|
||||
return item.html
|
||||
}
|
||||
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: 'mediabunny-quality',
|
||||
position: 'right',
|
||||
html: defaultHtml,
|
||||
style: { padding: '0 10px' },
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: 'mediabunny-quality',
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $quality,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function updateAudio(state) {
|
||||
const config = option.m3u8?.audio || {}
|
||||
if (!canRender(config) || !state?.audios.length || state.audios.length < 2) {
|
||||
clearAudio()
|
||||
return
|
||||
}
|
||||
|
||||
const auto = config.auto || 'Auto'
|
||||
const title = config.title || 'Audio'
|
||||
const getName = config.getName || (track => track.name || track.lang || track.language)
|
||||
const defaultHtml = state.currentAudio
|
||||
? getName(state.currentAudio)
|
||||
: auto
|
||||
|
||||
const selector = uniqBy(
|
||||
state.audios.map((item) => {
|
||||
return {
|
||||
html: getName(item),
|
||||
value: item.id,
|
||||
default: state.currentAudio?.id === item.id,
|
||||
}
|
||||
}),
|
||||
'html',
|
||||
)
|
||||
|
||||
selector.push({
|
||||
html: auto,
|
||||
value: 'auto',
|
||||
default: !state.currentAudio,
|
||||
})
|
||||
|
||||
const onSelect = async (item) => {
|
||||
await shim.switchM3u8Audio(item.value)
|
||||
art.notice.show = `${title}: ${item.html}`
|
||||
await update()
|
||||
return item.html
|
||||
}
|
||||
|
||||
if (config.control) {
|
||||
art.controls.update({
|
||||
name: 'mediabunny-audio',
|
||||
position: 'right',
|
||||
html: defaultHtml,
|
||||
style: { padding: '0 10px' },
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
|
||||
if (config.setting) {
|
||||
art.setting.update({
|
||||
name: 'mediabunny-audio',
|
||||
tooltip: defaultHtml,
|
||||
html: title,
|
||||
icon: $audio,
|
||||
width: 200,
|
||||
selector,
|
||||
onSelect,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function update() {
|
||||
const state = await shim.getM3u8State()
|
||||
if (!state) {
|
||||
clearQuality()
|
||||
clearAudio()
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
updateQuality(state),
|
||||
updateAudio(state),
|
||||
])
|
||||
}
|
||||
|
||||
art.on('video:loadedmetadata', update)
|
||||
art.on('restart', update)
|
||||
|
||||
return { update }
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { Label, SelectorItem, SetupOptions } from './m3u8-types'
|
||||
import $audio from '../../artplayer-plugin-hls-control/src/audio.svg?raw'
|
||||
import $quality from '../../artplayer-plugin-hls-control/src/quality.svg?raw'
|
||||
import createMenu, { releaseAll } from './m3u8-menu'
|
||||
import { audioModel, qualityModel } from './m3u8-model'
|
||||
|
||||
export function setupM3u8Controls({ art, shim, option }: SetupOptions) {
|
||||
if (!option.m3u8)
|
||||
return
|
||||
let closed = false
|
||||
let revision = 0
|
||||
let selection = 0
|
||||
const quality = createMenu(art, 'mediabunny-quality', $quality)
|
||||
const audio = createMenu(art, 'mediabunny-audio', $audio)
|
||||
const current = (source: number) => !closed && !shim.engine.destroyed && shim.engine.loadSeq === source
|
||||
|
||||
async function update(): Promise<void> {
|
||||
if (closed || shim.engine.destroyed)
|
||||
return
|
||||
const version = ++revision
|
||||
const source = shim.engine.loadSeq
|
||||
const active = () => version === revision && current(source)
|
||||
try {
|
||||
const state = await shim.getM3u8State()
|
||||
if (!active())
|
||||
return
|
||||
const qualityConfig = option.m3u8?.quality || {}
|
||||
const qualityView = qualityModel(state, qualityConfig, active)
|
||||
if (!active())
|
||||
return
|
||||
quality.update(qualityView, qualityConfig, () => current(source), (item, title) => select('quality', item, title))
|
||||
if (!active())
|
||||
return
|
||||
const audioConfig = option.m3u8?.audio || {}
|
||||
const audioView = audioModel(state, audioConfig, active)
|
||||
if (active())
|
||||
audio.update(audioView, audioConfig, () => current(source), (item, title) => select('audio', item, title))
|
||||
}
|
||||
catch (error) {
|
||||
if (active())
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function select(kind: 'quality' | 'audio', item: SelectorItem, title: string): Promise<Label> {
|
||||
const intent = ++selection
|
||||
const source = shim.engine.loadSeq
|
||||
revision++
|
||||
const active = () => intent === selection && current(source)
|
||||
try {
|
||||
if (kind === 'quality')
|
||||
await shim.switchM3u8Quality(item.value)
|
||||
else
|
||||
await shim.switchM3u8Audio(item.value)
|
||||
if (active()) {
|
||||
await update()
|
||||
if (active())
|
||||
art.notice.show = `${title}: ${item.html}`
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (active()) {
|
||||
await refresh()
|
||||
if (active())
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return item.html
|
||||
}
|
||||
|
||||
function refresh(): Promise<void> {
|
||||
return update().catch(error => console.warn('MediaBunny HLS menu update:', error))
|
||||
}
|
||||
function invalidate(): void {
|
||||
revision++
|
||||
selection++
|
||||
try {
|
||||
releaseAll([quality.clear, audio.clear])
|
||||
}
|
||||
catch (error) { console.warn('MediaBunny HLS menu cleanup:', error) }
|
||||
}
|
||||
function destroy(): void {
|
||||
if (closed)
|
||||
return
|
||||
closed = true
|
||||
invalidate()
|
||||
try {
|
||||
releaseAll([
|
||||
() => art.off('video:loadedmetadata', refresh),
|
||||
() => art.off('restart', refresh),
|
||||
() => art.off('video:loadstart', invalidate),
|
||||
() => art.off('video:error', invalidate),
|
||||
() => art.off('destroy', destroy),
|
||||
])
|
||||
}
|
||||
catch (error) { console.warn('MediaBunny HLS listener cleanup:', error) }
|
||||
}
|
||||
art.on('video:loadedmetadata', refresh)
|
||||
art.on('restart', refresh)
|
||||
art.on('video:loadstart', invalidate)
|
||||
art.on('video:error', invalidate)
|
||||
art.on('destroy', destroy)
|
||||
return { update }
|
||||
}
|
||||
@@ -4,5 +4,5 @@
|
||||
"allowJs": false,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable", "ESNext.Disposable"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "types/**/*.d.ts"]
|
||||
"include": ["src/**/*.ts", "types/**/*.d.ts", "../../types/assets.d.ts"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"task": "PKG-MB-07",
|
||||
"sourceCommit": "ccee9157a70725e7f6bf77dc43ff2566bfa14540",
|
||||
"sourceSha256LF": {
|
||||
"packages/artplayer-proxy-mediabunny/src/m3u8.js": "4639f09e0c743dc2b24078f48ca1e58eaaedaa3f5e6b863583cbd4bc4a7d3142",
|
||||
"packages/artplayer-proxy-mediabunny/src/hls-selection.ts": "a8632c0ea7d0c59434de005eee81bbf8d1dea8ae73c690673b603e5d6fa3cfd6",
|
||||
"packages/artplayer-proxy-mediabunny/src/hls-state.ts": "ff9ffd742f9763349df1f50eac6b6bd488a80d05aaab8ca198f387f0d17d5e25",
|
||||
"packages/artplayer-proxy-mediabunny/src/MediaBunnyEngine.ts": "3d51f6ecd247d1622809b5b679b906eb9b5ad4f9332a5d52dd38e5e2e29f38e9",
|
||||
"packages/artplayer-proxy-mediabunny/src/playback.ts": "e2a65782c89627198752ff1fe691d4eea0bef215d883a59f0bdcd3e904a4fb47",
|
||||
"packages/artplayer-proxy-mediabunny/src/index.js": "4e1be8937908ff6e6ef8a4e863e22b4468312cf31d66ac299322e475757773bb",
|
||||
"packages/artplayer-proxy-mediabunny/dist/artplayer-proxy-mediabunny.js": "c80fd9015dd5d9c338ea780948dcb82f97a32f8395de20e19daf5cc65f05eb57"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,70 @@
|
||||
# PKG-MB-07:HLS配对、选择意图与UI拓扑
|
||||
|
||||
任务通过完整CI后标记done,并立即以专用本地提交交付。来源ccee9157a70725e7f6bf77dc43ff2566bfa14540,
|
||||
冻结源码及main见[来源基线](../baselines/mb-hls-source.json),最终检查、报告和哈希见
|
||||
[验证记录](../baselines/mb-hls-validation.json)。
|
||||
|
||||
## 实际结构
|
||||
|
||||
删除m3u8.js,迁移为m3u8.ts,拆出m3u8-model.ts、m3u8-menu.ts、m3u8-types.ts。
|
||||
协调模块管理refresh版本、选择意图、source与事件;model生成label、排序和selector;
|
||||
menu拥有controls/setting两个独立入口,types限定所需UI/shim能力,避免接收不受约束的art。
|
||||
包内30个生产TS模块,剩index.js由MB-08接续。tsconfig引用现有共享assets.d.ts声明SVG导入;
|
||||
没有新依赖,public Option/Result/factory声明未修改。
|
||||
|
||||
hls-selection.ts统一质量/音频请求的意图标识与source/Input检查,并分别保留配对策略。
|
||||
较新的请求使尚在查询的旧请求失效,包括质量和音频交叉选择。已进入replaceTracks的操作
|
||||
仍由Playback控制,到下一个replacement开始才被替代,不声称点击即同步终止SDK解码。
|
||||
当先前replacement在最新查询期间安装另一份media时,最新查询对新的media重新计算,
|
||||
避免简单的media相等检查丢弃最新意图,或Auto音轨按旧视频配对。当前查询失败仍拒绝;
|
||||
过期查询/替换失败被观察,不覆盖当前选择。状态getter同时检查source代数和media身份。
|
||||
|
||||
## 兼容与生命周期修复
|
||||
|
||||
保留原有mediabunny-quality/audio名称、右侧control、setting宽度200、图标、间距、
|
||||
标题/Auto/getName默认值、质量降序、通知格式、onSelect Promise与label返回值。
|
||||
Auto仍是操作入口,显示与高亮基于实际选择轨道;自动模式下也不会只按mode伪造高亮。
|
||||
相同label仍合并,但合并组中保留实际选中的ID,修复选中项被前一个同名条目遮蔽的问题。
|
||||
排序改为提前建立height索引,避免比较器反复find数组。
|
||||
|
||||
refresh版本与选择意图分开:迟到状态不覆盖新模型,同一可见菜单在首个选择未完成时
|
||||
仍接受下一次点击;自身loadedmetadata刷新不使有效选择的完成失效。被替换菜单或旧source
|
||||
回调不再切轨、通知或刷新。选择失败恢复实际轨道高亮后保持原拒绝,恢复期间被新意图替代的
|
||||
旧失败不再外溢;事件自动refresh明确观察拒绝。getName和registry回调的同步切源/销毁
|
||||
在每个后续阶段前重新检查,避免重建已关闭UI。
|
||||
|
||||
control/setting分别拥有与移除。关闭一侧会清理它并保留另一侧;单音轨/纯视频清理audio,
|
||||
非HLS清理两类菜单;loadstart/error/destroy立即使旧工作失效并清理已拥有入口。
|
||||
清理尝试全部入口和全部监听器,即使其中一个抛错也继续,并保证入口的shim销毁仍执行。
|
||||
这些模块不对外新增canvas属性或API;EnginePort补充已有只读source/destroyed能力用于内部检查。
|
||||
|
||||
## 复现与验证
|
||||
|
||||
- 首批27条旧main断言5通过/22失败;最终43条旧main为11通过/32失败,无取消或跳过。
|
||||
候选全部通过,覆盖乱序刷新、旧回调、单入口关闭、失败恢复、清理异常、实际选中ID、
|
||||
连续/跨类型轨道选择和配对期间media变化。配对和失败恢复的中间候选缺陷另有复现日志。
|
||||
- 候选包专项226通过;main/legacy各202通过,每个产物命令含198候选断言及4冻结正常事件对照。
|
||||
- 新增30浏览器场景:两套真实核心×五类场景×三个引擎。Chromium/Firefox共20项真实
|
||||
MediaBunny解析/解码和DOM菜单验证;Windows WebKit10项明确媒体能力缺失对照。
|
||||
- 同30条浏览器断言对旧main:14通过/16失败。四个正常菜单场景和十个能力对照通过;
|
||||
失败分布在单侧control关闭、旧source/destroy状态写回、最新选择被早期请求覆盖。
|
||||
单侧关闭失败是明确等待目标UI被删除未满足,其余是状态/写入次数/最终轨道断言。
|
||||
- 真实菜单场景点击quality控件和audio设置项,再切Auto,验证实际配对与两个入口高亮;
|
||||
每次替换恰好一套loadedmetadata/loadeddata/canplay/canplaythrough/seeked,三次共三套。
|
||||
- 首次Firefox检查在解码完成前读取事件计数(为0)而失败,改用原超时等待精确完成序列;
|
||||
下一轮暴露轮询未处理状态getter合法的临时null,改为等待非null目标值。没有放宽事件数量、
|
||||
静默跳过或拉长超时,失败报告/results均保留。
|
||||
- 最终构建main运行全部七个MediaBunny浏览器文件;详细结果在验证记录中核验。
|
||||
- 严格TS、lint、三格式build、ESM默认factory及dist/docs逐字节一致性单独检查。
|
||||
根test:unit与test:mediabunny加入HLS断言,完整CI持续执行。
|
||||
|
||||
最终完整CI1387项(1232单元+14工程+141基线)通过,另44重复契约;320生产TS。
|
||||
全部七个MediaBunny浏览器文件108项通过,其中29项为Windows WebKit能力缺失对照。
|
||||
最终源码/测试/产物/报告哈希已核验,main与浏览器所测文件字节一致,dist与docs三格式一致。
|
||||
|
||||
## 后续与回退
|
||||
|
||||
MB-LIFE-01仍open,剩长播放、完整媒体/核心/插件组合和物理设备证据归MB-09;编码能力/
|
||||
undecodable fallback与公共shim/factory声明由MB-08及MB-CAP-01/MB-READY-01处理。
|
||||
MB-10验证demo、真实安装包和MPL/source通知。本项通过不构成npm发布验收。
|
||||
回退revert专用提交,历史冻结源码与报告保留。下一项MB-08完成入口TS、能力与声明边界。
|
||||
@@ -71,3 +71,12 @@ main/legacy各159通过。最终78浏览器含16新增原生音频场景及8新
|
||||
同24音频浏览器断言对旧main20通过/4失败,失败为实际暂停后的节点清理;其余为回归对照。
|
||||
短时1×/2×帧/音频时钟偏差验证不替代长播放、听觉及设备验收;全部报告/results已归档。
|
||||
完整CI1344项及另44重复契约通过,316生产TS;完成后立即专用提交,MB-LIFE-01剩余范围仍open。
|
||||
|
||||
## PKG-MB-07 HLS配对、选择与UI拓扑
|
||||
|
||||
实际m3u8迁移并拆成四个TS模块,包内30生产TS,仅index.js由08接续。
|
||||
[变更](changes/2026-09-12-PKG-MB-07-hls.md)记录刷新/选择/source归属、稳定media配对、
|
||||
兼容默认值、清理及回退;[证据](baselines/mb-hls-validation.json)记录43条断言旧main
|
||||
11通过/32失败,候选专项226及main/legacy各202通过。最终108浏览器含20新增原生HLS
|
||||
新旧核心/UI/SDK场景与10新增能力对照。旧main同30项14通过/16失败,未把能力缺失计作播放。
|
||||
全部报告/results已归档;完整CI1387项及44重复契约通过,320生产TS;完成后立即专用提交,MB-LIFE-01剩余组合/长播范围仍open。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 112 / doing 7 / blocked 0 / done 103 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 111 / doing 7 / blocked 0 / done 104 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -335,7 +335,7 @@
|
||||
| PKG-MB-04 | artplayer-proxy-mediabunny<br>整理 VideoShim/EventTarget/Engine 协调 | PKG-MB-03, CORE-06, CORE-11 | shim 描述符、事件桥、加载状态和操作隔离 | 公开属性/同步异步返回与事件重入顺序保持 | H | done |
|
||||
| PKG-MB-05 | artplayer-proxy-mediabunny<br>整理视频解码、seek 和帧释放 | PKG-MB-04 | VideoEngine 调度、队列、晚帧处理和资源释放 | 暂停/seek/切源无旧帧覆盖,默认丢帧策略不变 | H | done |
|
||||
| PKG-MB-06 | artplayer-proxy-mediabunny<br>整理音频解码、时钟与同步 | PKG-MB-04 | AudioEngine 时钟、缓冲和 AudioContext 归属 | AV sync、倍速、无音轨、静音/音量和暂停恢复通过 | H | done |
|
||||
| PKG-MB-07 | artplayer-proxy-mediabunny<br>整理 HLS 配对轨道与 selector | PKG-MB-05, PKG-MB-06, CORE-14 | m3u8 配对、质量/音频选择及拓扑清理 | 实际选择高亮、切到无轨道来源清理、无重复 readiness | H | todo |
|
||||
| PKG-MB-07 | artplayer-proxy-mediabunny<br>整理 HLS 配对轨道与 selector | PKG-MB-05, PKG-MB-06, CORE-14 | m3u8 配对、质量/音频选择及拓扑清理 | 实际选择高亮、切到无轨道来源清理、无重复 readiness | H | done |
|
||||
| PKG-MB-08 | artplayer-proxy-mediabunny<br>完成 TS 与媒体能力声明 | PKG-MB-07, ENG-04, ENG-06 | 8 个自有 JS 模块迁移与 Result/shim 类型 | 解码器/Stream/DOM 类型清楚,旧 Option 和 art.mediabunny 使用保持 | H | todo |
|
||||
| PKG-MB-09 | artplayer-proxy-mediabunny<br>完成新旧核心和真实媒体组合 | PKG-MB-08, CORE-22 | 跨浏览器能力、长播放、DPiP、HLS 音轨/质量报告 | 资源释放、事件顺序、AV sync 与支持范围满足基线 | H | todo |
|
||||
| PKG-MB-10 | artplayer-proxy-mediabunny<br>完成分发和文档 | PKG-MB-09, ENG-07 | mediabunny.js、README、依赖版本、三产物和许可 | tarball 可消费、无意外依赖升级、旧调用与回退可用 | H | todo |
|
||||
@@ -526,4 +526,5 @@
|
||||
- PKG-MB-04: [记录](baselines/mb-coordination-source.json) [记录](changes/2026-09-12-PKG-MB-04-shim-checkpoint.md) [记录](baselines/mb-shim-checkpoint.json) [记录](changes/2026-09-12-PKG-MB-04-coordination.md) [记录](baselines/mb-coordination-validation.json)
|
||||
- PKG-MB-05: [记录](baselines/mb-video-source.json) [记录](changes/2026-09-12-PKG-MB-05-video.md) [记录](baselines/mb-video-validation.json)
|
||||
- PKG-MB-06: [记录](baselines/mb-audio-source.json) [记录](changes/2026-09-12-PKG-MB-06-audio.md) [记录](baselines/mb-audio-validation.json)
|
||||
- PKG-MB-07: [记录](baselines/mb-hls-source.json) [记录](changes/2026-09-12-PKG-MB-07-hls.md) [记录](baselines/mb-hls-validation.json)
|
||||
- PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md)
|
||||
@@ -1,5 +1,18 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-MB-07完成:HLS配对、选择意图与UI拓扑
|
||||
|
||||
m3u8实际迁移为四个TS模块,包内30生产TS,仅剩index.js待MB-08。
|
||||
旧查询/菜单回调不再覆盖当前source;control/setting各自清理,失败恢复实际轨道高亮。
|
||||
质量/音频共享最新选择意图,media在配对查询期间变化会重新计算,保留匹配轨道与正常默认行为。
|
||||
43断言旧main11通过/32失败,候选专项226、main/legacy各202通过;旧main30项HLS浏览器
|
||||
14通过/16失败,候选20新增原生新旧核心/UI/SDK场景和10能力对照通过。最终全部七个
|
||||
MediaBunny文件108浏览器通过,29项为能力对照,不计作播放。完整CI1387及44重复契约通过,
|
||||
320生产TS;源码/产物/报告哈希及三格式复制一致。
|
||||
见[变更](changes/2026-09-12-PKG-MB-07-hls.md)与[证据](baselines/mb-hls-validation.json)。
|
||||
222项:104 done、7 doing、111 todo;立即专用本地提交,下一项MB-08入口、能力与声明。
|
||||
长播放、全组合、设备及安装包仍待后续;未放行npm发布。
|
||||
|
||||
## PKG-MB-06完成:音频解码、时钟与资源归属
|
||||
|
||||
AudioEngine实际迁移TS,拆清clock、context、nodes、pump、task职责,删除最后音频声明桥。
|
||||
|
||||
+4
-2
@@ -3648,7 +3648,7 @@
|
||||
"PKG-MB-07",
|
||||
"PKG-MB-09"
|
||||
],
|
||||
"compatibleResolution": "Reproduce pending operations and destroy/source-switch races before migrating each owner; preserve public event and promise behavior while preventing stale resource use. MB-02 now reproduces late load rejection and uncancelled waiting/loadstart timers after destroy in all three historical implementations using a controlled host. Other interleavings and full-core native behavior are not yet verified. Expanded MB-02 baseline reproduces play-after-pause/destroy, rejected-play false state, seek-after-destroy and retained successful-load timeout timers in all three actual historical implementations; Range and active-timeout normal controls remain separate. MB-03 candidate owns pending Input before track selection and cancels load/HEAD/timers on source replacement, timeout and destroy; source/main/legacy regressions pass. Pending play/seek, late decoder work and full readiness coordination remain MB-04/05/06, so this risk stays open. MB-04 shim checkpoint fixes synthetic RAF retention and terminal event-listener cleanup, including engine cleanup exceptions. Closing the event bridge does not prove pending engine operations stopped; coordination remains in progress. MB-04 now guards play/seek/replacement completion, preserves explicit pause, observes stale failures and isolates reentrant event sequences. Decoder-internal late frames/audio and HLS UI races remain MB-05/06/07; risk stays open. MB-05 migrates VideoEngine and owns iterators, late first/second seek frames, pending reads, per-instance RAF and poster callbacks; same native race assertions fail on old main and pass on candidate. Independent CanvasSink pools also prevent multiple obsolete seek samples from overwriting queued current-frame pixels. Audio nodes, long-run AV sync and HLS UI topology remain MB-06/07/09. MB-06 migrates AudioEngine into six checked modules, owns context/gain/nodes/iterators and removable cancellation waits, preserves clock and gain formulas, and invalidates pending resume/decoding across pause/source/destroy. Same old main fails 29 of 37 audio assertions and four native pause-cleanup cases; candidate passes with short native 1x/2x AV-clock samples. Long-run AV sync, full combinations and HLS UI topology remain MB-07/09, so risk stays open.",
|
||||
"compatibleResolution": "Reproduce pending operations and destroy/source-switch races before migrating each owner; preserve public event and promise behavior while preventing stale resource use. MB-02 now reproduces late load rejection and uncancelled waiting/loadstart timers after destroy in all three historical implementations using a controlled host. Other interleavings and full-core native behavior are not yet verified. Expanded MB-02 baseline reproduces play-after-pause/destroy, rejected-play false state, seek-after-destroy and retained successful-load timeout timers in all three actual historical implementations; Range and active-timeout normal controls remain separate. MB-03 candidate owns pending Input before track selection and cancels load/HEAD/timers on source replacement, timeout and destroy; source/main/legacy regressions pass. Pending play/seek, late decoder work and full readiness coordination remain MB-04/05/06, so this risk stays open. MB-04 shim checkpoint fixes synthetic RAF retention and terminal event-listener cleanup, including engine cleanup exceptions. Closing the event bridge does not prove pending engine operations stopped; coordination remains in progress. MB-04 now guards play/seek/replacement completion, preserves explicit pause, observes stale failures and isolates reentrant event sequences. Decoder-internal late frames/audio and HLS UI races remain MB-05/06/07; risk stays open. MB-05 migrates VideoEngine and owns iterators, late first/second seek frames, pending reads, per-instance RAF and poster callbacks; same native race assertions fail on old main and pass on candidate. Independent CanvasSink pools also prevent multiple obsolete seek samples from overwriting queued current-frame pixels. Audio nodes, long-run AV sync and HLS UI topology remain MB-06/07/09. MB-06 migrates AudioEngine into six checked modules, owns context/gain/nodes/iterators and removable cancellation waits, preserves clock and gain formulas, and invalidates pending resume/decoding across pause/source/destroy. Same old main fails 29 of 37 audio assertions and four native pause-cleanup cases; candidate passes with short native 1x/2x AV-clock samples. Long-run AV sync, full combinations and HLS UI topology remain MB-07/09, so risk stays open. MB-07 migrates HLS UI to four checked modules, guards stale refresh/selection callbacks, cleans each surface and owns terminal listeners. Quality/audio queries share a latest-intent token and retry pairing against changed media snapshots. Forty-three old-main assertions yield 11 pass/32 fail; sixteen old native UI/query cases fail and twenty candidate native cases pass across old/new cores. Long-run AV sync and full combinations remain MB-09; capability/type/package gates remain MB-08/10.",
|
||||
"closureCriteria": "Historical failing interleavings and candidate cancellation/cleanup pass with real media and event evidence, including long playback and track topology.",
|
||||
"status": "open",
|
||||
"evidence": [
|
||||
@@ -3666,7 +3666,9 @@
|
||||
"refactor/changes/2026-09-12-PKG-MB-05-video.md",
|
||||
"refactor/baselines/mb-video-validation.json",
|
||||
"refactor/changes/2026-09-12-PKG-MB-06-audio.md",
|
||||
"refactor/baselines/mb-audio-validation.json"
|
||||
"refactor/baselines/mb-audio-validation.json",
|
||||
"refactor/changes/2026-09-12-PKG-MB-07-hls.md",
|
||||
"refactor/baselines/mb-hls-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+6
-2
@@ -3524,11 +3524,15 @@
|
||||
"PKG-MB-06",
|
||||
"CORE-14"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "m3u8 配对、质量/音频选择及拓扑清理",
|
||||
"acceptance": "实际选择高亮、切到无轨道来源清理、无重复 readiness",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"baselines/mb-hls-source.json",
|
||||
"changes/2026-09-12-PKG-MB-07-hls.md",
|
||||
"baselines/mb-hls-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-MB-08",
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { mbCandidate } from '../helpers/mediabunny.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementation = await mbCandidate()
|
||||
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
|
||||
const media = new Map(Object.entries(manifest.files).map(([name, expected]) => {
|
||||
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
|
||||
assert.equal(hash(bytes), expected.sha256)
|
||||
return [name, bytes]
|
||||
}))
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
for (const scenario of ['menus', 'topology', 'stale-source', 'stale-destroy', 'latest']) {
|
||||
test(`MediaBunny ${implementation.name}: ${core} core native HLS ${scenario}`, async ({ page, browserName }, testInfo) => {
|
||||
await page.route('**/mb-ui-hls/**', async (route) => {
|
||||
const name = new URL(route.request().url()).pathname.split('/').at(-1)
|
||||
const body = media.get(name)
|
||||
await route.fulfill({ status: body ? 200 : 404, body: body || 'Missing fixture', contentType: name.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : 'video/mp2t' })
|
||||
})
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports;
|
||||
${implementation.code}; window.mbFactory = module.exports.default || module.exports; })();` })
|
||||
await page.evaluate(() => {
|
||||
window.mbEvents = []
|
||||
window.mbErrors = []
|
||||
window.mbWrites = []
|
||||
window.mbOption = { m3u8: { quality: { control: true, setting: true }, audio: { control: true, setting: true } } }
|
||||
window.mbFlush = () => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
||||
window.art = new window.Artplayer({ container: '.player', url: '/mb-ui-hls/master.m3u8', muted: true, setting: true, proxy: window.mbFactory(window.mbOption) })
|
||||
window.mbCanvas = window.art.video
|
||||
for (const event of ['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'seeked', 'error'])
|
||||
window.art.on(`video:${event}`, () => window.mbEvents.push(event))
|
||||
document.querySelector('#play').onclick = () => window.art.play().catch(error => window.mbErrors.push(error.message))
|
||||
})
|
||||
const capabilities = await page.evaluate(() => ({ AudioContext: typeof AudioContext, webkitAudioContext: typeof window.webkitAudioContext, AudioDecoder: typeof AudioDecoder, VideoDecoder: typeof VideoDecoder, coreVersion: window.Artplayer.version }))
|
||||
let outcome = 'unverified'
|
||||
let result
|
||||
try {
|
||||
if (capabilities.AudioContext === 'undefined' && capabilities.webkitAudioContext === 'undefined') {
|
||||
expect(browserName).toBe('webkit')
|
||||
await expect.poll(() => page.evaluate(() => window.mbCanvas.error?.code)).toBe(4)
|
||||
expect(await page.evaluate(() => window.art.isReady)).toBe(false)
|
||||
expect(await page.locator('.art-control-mediabunny-quality').count()).toBe(0)
|
||||
outcome = 'unsupported-capability-control'
|
||||
return
|
||||
}
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await expect(page.locator('.art-control-mediabunny-quality .art-selector-value')).toHaveText('180P')
|
||||
await expect(page.locator('.art-control-mediabunny-audio .art-selector-value')).toHaveText('English')
|
||||
await page.evaluate(() => {
|
||||
for (const name of ['controls', 'setting']) {
|
||||
const registry = window.art[name]
|
||||
const update = registry.update.bind(registry)
|
||||
registry.update = (option) => {
|
||||
if (option.name?.startsWith('mediabunny-'))
|
||||
window.mbWrites.push({ surface: name, name: option.name })
|
||||
return update(option)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (scenario === 'menus') {
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.mbCanvas.currentTime)).toBeGreaterThan(0.2)
|
||||
await page.evaluate(() => {
|
||||
window.art.pause()
|
||||
window.mbEvents.length = 0
|
||||
})
|
||||
const quality = page.locator('.art-control-mediabunny-quality')
|
||||
await quality.hover()
|
||||
await quality.locator('.art-selector-item').filter({ hasText: /^90P$/ }).click()
|
||||
await expect.poll(() => page.evaluate(() => window.mbCanvas.getM3u8State().then(state => state?.currentLevel?.height))).toBe(90)
|
||||
await expect(page.locator('.art-control-mediabunny-quality .art-selector-value')).toHaveText('90P')
|
||||
await expect.poll(() => page.evaluate(() => Object.fromEntries(['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'seeked'].map(name => [name, window.mbEvents.filter(event => event === name).length])))).toEqual({ loadedmetadata: 1, loadeddata: 1, canplay: 1, canplaythrough: 1, seeked: 1 })
|
||||
await page.evaluate(() => {
|
||||
const menu = window.art.setting.find('mediabunny-audio')
|
||||
window.art.setting.show = true
|
||||
window.art.setting.render(menu.selector)
|
||||
menu.selector.find(item => item.html === 'French').$item.click()
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.mbCanvas.getM3u8State().then(state => state?.currentAudio?.lang))).toBe('fr')
|
||||
await expect(page.locator('.art-control-mediabunny-audio .art-selector-value')).toHaveText('French')
|
||||
await expect.poll(() => page.evaluate(() => window.mbEvents.filter(event => event === 'seeked').length)).toBe(2)
|
||||
await page.evaluate(() => {
|
||||
window.art.setting.show = false
|
||||
})
|
||||
await quality.hover()
|
||||
await quality.locator('.art-selector-item').filter({ hasText: /^Auto$/ }).click()
|
||||
await expect.poll(() => page.evaluate(() => window.mbCanvas.getM3u8State().then(state => state?.videoMode))).toBe('auto')
|
||||
await expect(page.locator('.art-control-mediabunny-quality .art-selector-value')).toHaveText('180P')
|
||||
await expect.poll(() => page.evaluate(() => Object.fromEntries(['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'seeked'].map(name => [name, window.mbEvents.filter(event => event === name).length])))).toEqual({ loadedmetadata: 3, loadeddata: 3, canplay: 3, canplaythrough: 3, seeked: 3 })
|
||||
result = await page.evaluate(async () => {
|
||||
const state = await window.mbCanvas.getM3u8State()
|
||||
const quality = window.art.controls.cache.get('mediabunny-quality').option
|
||||
const audio = window.art.setting.find('mediabunny-audio')
|
||||
return { currentLevel: state.currentLevel.id, qualitySelected: quality.selector.filter(item => item.default).map(item => item.value), currentAudio: state.currentAudio.id, audioSelected: audio.selector.filter(item => item.default).map(item => item.value), paired: state.currentLevel.track.canBePairedWith(state.currentAudio.track), errors: window.mbErrors }
|
||||
})
|
||||
expect(result.qualitySelected).toEqual([result.currentLevel])
|
||||
expect(result.audioSelected).toEqual([result.currentAudio])
|
||||
expect(result.paired).toBe(true)
|
||||
expect(result.errors).toEqual([])
|
||||
}
|
||||
else if (scenario === 'topology') {
|
||||
await page.evaluate(() => {
|
||||
window.mbOption.m3u8.quality.control = false
|
||||
window.mbOption.m3u8.audio.control = false
|
||||
window.art.emit('restart')
|
||||
})
|
||||
await expect(page.locator('.art-control-mediabunny-quality')).toHaveCount(0)
|
||||
await expect(page.locator('.art-control-mediabunny-audio')).toHaveCount(0)
|
||||
expect(await page.evaluate(() => Boolean(window.art.setting.find('mediabunny-quality') && window.art.setting.find('mediabunny-audio')))).toBe(true)
|
||||
await page.evaluate(async () => {
|
||||
window.mbOption.m3u8.quality.control = true
|
||||
window.mbOption.m3u8.audio.control = true
|
||||
await window.mbCanvas.engine.load('/mb-ui-hls/video-only.m3u8')
|
||||
})
|
||||
await expect(page.locator('.art-control-mediabunny-quality')).toHaveCount(1)
|
||||
await expect(page.locator('.art-control-mediabunny-audio')).toHaveCount(0)
|
||||
expect(await page.evaluate(() => Boolean(window.art.setting.find('mediabunny-audio')))).toBe(false)
|
||||
await page.evaluate(() => window.mbCanvas.engine.load('/test/pattern.mp4'))
|
||||
await expect(page.locator('[class*="art-control-mediabunny-"]')).toHaveCount(0)
|
||||
expect(await page.evaluate(() => Boolean(window.art.setting.find('mediabunny-quality') || window.art.setting.find('mediabunny-audio')))).toBe(false)
|
||||
result = { settingsSurviveControlDisable: true, audioRemovedForVideoOnly: true, allRemovedForMp4: true }
|
||||
}
|
||||
else if (scenario.startsWith('stale-')) {
|
||||
await page.evaluate(() => {
|
||||
const engine = window.mbCanvas.engine
|
||||
const getState = engine.getHlsState.bind(engine)
|
||||
let first = true
|
||||
engine.getHlsState = async () => {
|
||||
const state = await getState()
|
||||
if (first) {
|
||||
first = false
|
||||
return new Promise((resolve) => {
|
||||
window.mbRelease = () => resolve(state)
|
||||
})
|
||||
}
|
||||
return state
|
||||
}
|
||||
window.art.emit('restart')
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => typeof window.mbRelease)).toBe('function')
|
||||
result = await page.evaluate(async (scenario) => {
|
||||
if (scenario === 'stale-source')
|
||||
await window.mbCanvas.engine.load('/test/pattern.mp4')
|
||||
else window.art.destroy(false)
|
||||
const before = window.mbWrites.length
|
||||
window.mbRelease()
|
||||
await window.mbFlush()
|
||||
return { before, after: window.mbWrites.length, menus: document.querySelectorAll('[class*="art-control-mediabunny-"]').length }
|
||||
}, scenario)
|
||||
expect(result.after).toBe(result.before)
|
||||
expect(result.menus).toBe(0)
|
||||
}
|
||||
else {
|
||||
await page.evaluate(async () => {
|
||||
const engine = window.mbCanvas.engine
|
||||
const input = engine.input
|
||||
const getTracks = input.getVideoTracks.bind(input)
|
||||
const tracks = await getTracks()
|
||||
window.mbDesired = tracks.find(track => track.id !== engine.media.videoTrack.id).id
|
||||
let calls = 0
|
||||
input.getVideoTracks = () => {
|
||||
calls++
|
||||
if (calls === 1)
|
||||
return new Promise((resolve) => { window.mbReleaseOlder = () => resolve(tracks) })
|
||||
if (calls === 2)
|
||||
return new Promise((resolve) => { window.mbReleaseNewer = () => resolve(tracks) })
|
||||
return getTracks()
|
||||
}
|
||||
window.mbOlder = window.mbCanvas.switchM3u8Quality(engine.media.videoTrack.id)
|
||||
window.mbNewer = window.mbCanvas.switchM3u8Quality(window.mbDesired)
|
||||
window.mbReleaseOlder()
|
||||
await window.mbOlder
|
||||
window.mbReleaseNewer()
|
||||
await window.mbNewer
|
||||
})
|
||||
result = await page.evaluate(async () => {
|
||||
const state = await window.mbCanvas.getM3u8State()
|
||||
return { desired: window.mbDesired, selected: state.currentLevel.id, ready: window.mbCanvas.readyState, events: window.mbEvents }
|
||||
})
|
||||
expect(result.selected).toBe(result.desired)
|
||||
expect(result.ready).toBe(4)
|
||||
}
|
||||
outcome = 'native-hls-ui-and-selection-observed'
|
||||
}
|
||||
finally {
|
||||
const state = await page.evaluate(() => {
|
||||
window.mbRelease?.()
|
||||
window.mbReleaseOlder?.()
|
||||
window.mbReleaseNewer?.()
|
||||
window.mbContext = window.mbCanvas.engine.audio.audioContext
|
||||
window.art.destroy(false)
|
||||
return { events: window.mbEvents, errors: window.mbErrors, writes: window.mbWrites, error: window.mbCanvas.error, destroyed: window.mbCanvas.engine.destroyed }
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.mbContext?.state || 'not-created')).toMatch(/^(?:closed|not-created)$/)
|
||||
await testInfo.attach('mediabunny-hls-ui', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), core, scenario, capabilities, outcome, result, state, scope: 'Actual old/candidate core, SDK HLS parsing and native decoding; rendered control/settings clicks, topology and controlled state/query delays. Not installed-package or long-run/device acceptance.' }) })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Actual proxy, controlled SDK/UI interleavings and immutable old artifact controls.
|
||||
import test from 'node:test'
|
||||
import { mbCandidate, mbEnvironment } from './helpers/mediabunny.js'
|
||||
|
||||
const implementation = await mbCandidate()
|
||||
const flush = () => new Promise(resolve => setImmediate(resolve))
|
||||
function deferred() {
|
||||
let resolve, reject
|
||||
const promise = new Promise((yes, no) => {
|
||||
resolve = yes
|
||||
reject = no
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
function state(selected = 1) {
|
||||
const levels = [
|
||||
{ id: 1, index: 0, name: '720P', height: 720, bitrate: 2000 },
|
||||
{ id: 2, index: 1, name: '1080P', height: 1080, bitrate: 4000 },
|
||||
]
|
||||
const audios = [
|
||||
{ id: 3, index: 0, name: 'English', lang: 'en', language: 'en', bitrate: 128 },
|
||||
{ id: 4, index: 1, name: 'French', lang: 'fr', language: 'fr', bitrate: 128 },
|
||||
]
|
||||
return { levels, audios, currentLevel: levels.find(track => track.id === selected), currentAudio: audios[0], videoMode: 'auto', audioMode: 'auto' }
|
||||
}
|
||||
function environment(option = { m3u8: { quality: { control: true, setting: true }, audio: { control: true, setting: true } } }) {
|
||||
const env = mbEnvironment(implementation)
|
||||
const calls = []
|
||||
const warnings = []
|
||||
const notices = []
|
||||
const controls = new Map()
|
||||
const settings = new Map()
|
||||
env.globals.console = { ...console, warn: (...args) => warnings.push(args), error() {} }
|
||||
const registry = (cache, kind) => ({
|
||||
cache,
|
||||
find: name => cache.get(name),
|
||||
update(menu) {
|
||||
calls.push([kind, 'update', menu.name])
|
||||
cache.set(menu.name, menu)
|
||||
},
|
||||
remove(name) {
|
||||
calls.push([kind, 'remove', name])
|
||||
cache.delete(name)
|
||||
},
|
||||
})
|
||||
env.art.controls = registry(controls, 'control')
|
||||
env.art.setting = registry(settings, 'setting')
|
||||
env.art.notice = {
|
||||
get show() { return notices.at(-1) },
|
||||
set show(value) { notices.push(value) },
|
||||
}
|
||||
env.art.off = (name, callback) => {
|
||||
env.handlers.set(name, (env.handlers.get(name) || []).filter(item => item !== callback))
|
||||
}
|
||||
const canvas = env.factory(option)(env.art)
|
||||
canvas.engine.getHlsState = async () => state()
|
||||
const refresh = async () => {
|
||||
await Promise.all((env.handlers.get('video:loadedmetadata') || []).map(callback => callback()))
|
||||
}
|
||||
return { ...env, canvas, option, controls, settings, calls, warnings, notices, refresh }
|
||||
}
|
||||
|
||||
test('MediaBunny HLS retains normal menu names, ordering, defaults and notices', async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
const quality = env.controls.get('mediabunny-quality')
|
||||
assert.equal(quality.html, '720P')
|
||||
assert.deepEqual(Array.from(quality.selector, item => [item.value, item.default]), [[2, false], [1, true], ['auto', false]])
|
||||
assert.equal(env.settings.get('mediabunny-quality').width, 200)
|
||||
assert.equal(env.settings.get('mediabunny-audio').html, 'Audio')
|
||||
const selected = []
|
||||
env.canvas.engine.selectHlsQuality = async value => selected.push(value)
|
||||
assert.equal(await quality.onSelect(quality.selector[0]), '1080P')
|
||||
assert.deepEqual(selected, [2])
|
||||
assert.deepEqual(env.notices, ['Quality: 1080P'])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny disabled HLS integration installs no menu callbacks', () => {
|
||||
const env = environment({})
|
||||
assert.equal(env.handlers.get('restart'), undefined)
|
||||
assert.equal(env.controls.size, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny HLS clears all menus for a settled non-HLS source', async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
env.canvas.engine.getHlsState = async () => null
|
||||
await env.refresh()
|
||||
assert.equal(env.controls.size + env.settings.size, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
for (const surface of ['control', 'setting']) {
|
||||
test(`MediaBunny HLS removes disabled ${surface} while preserving the other surface`, async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
env.option.m3u8.quality[surface] = false
|
||||
env.option.m3u8.audio[surface] = false
|
||||
await env.refresh()
|
||||
assert.equal(env[surface === 'control' ? 'controls' : 'settings'].size, 0)
|
||||
assert.equal(env[surface === 'control' ? 'settings' : 'controls'].size, 2)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
for (const ending of ['newer', 'source', 'destroy']) {
|
||||
test(`MediaBunny HLS pending state cannot paint after ${ending}`, async () => {
|
||||
const env = environment()
|
||||
const pending = deferred()
|
||||
env.canvas.engine.getHlsState = () => pending.promise
|
||||
const refresh = env.refresh()
|
||||
if (ending === 'newer') {
|
||||
env.canvas.engine.getHlsState = async () => state(2)
|
||||
await env.refresh()
|
||||
}
|
||||
else if (ending === 'source') {
|
||||
env.canvas.engine.loadSeq++
|
||||
}
|
||||
else {
|
||||
env.art.emit('destroy')
|
||||
}
|
||||
pending.resolve(state(1))
|
||||
await refresh
|
||||
if (ending === 'newer')
|
||||
assert.equal(env.controls.get('mediabunny-quality').html, '1080P')
|
||||
else assert.equal(env.controls.size + env.settings.size, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
for (const event of ['video:loadstart', 'video:error', 'destroy']) {
|
||||
test(`MediaBunny HLS ${event} clears rendered menus immediately`, async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
env.art.emit(event)
|
||||
assert.equal(env.controls.size + env.settings.size, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
for (const ending of ['menu', 'source', 'destroy']) {
|
||||
test(`MediaBunny stale HLS selection callback is inert after ${ending}`, async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
const menu = env.controls.get('mediabunny-quality')
|
||||
const calls = []
|
||||
env.canvas.engine.selectHlsQuality = async value => calls.push(value)
|
||||
if (ending === 'menu')
|
||||
await env.refresh()
|
||||
else if (ending === 'source')
|
||||
env.canvas.engine.loadSeq++
|
||||
else env.art.emit('destroy')
|
||||
assert.equal(await menu.onSelect(menu.selector[0]), '1080P')
|
||||
assert.deepEqual(calls, [])
|
||||
assert.deepEqual(env.notices, [])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
for (const ending of ['newer', 'source', 'destroy']) {
|
||||
test(`MediaBunny pending HLS selection cannot notify after ${ending}`, async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
const menu = env.controls.get('mediabunny-quality')
|
||||
const pending = deferred()
|
||||
env.canvas.engine.selectHlsQuality = () => pending.promise
|
||||
const selecting = menu.onSelect(menu.selector[0])
|
||||
if (ending === 'newer') {
|
||||
env.canvas.engine.selectHlsQuality = async () => {}
|
||||
await menu.onSelect(menu.selector[1])
|
||||
}
|
||||
else if (ending === 'source') {
|
||||
env.canvas.engine.loadSeq++
|
||||
}
|
||||
else {
|
||||
env.art.emit('destroy')
|
||||
}
|
||||
const notices = env.notices.slice()
|
||||
pending.resolve()
|
||||
await selecting
|
||||
assert.deepEqual(env.notices, notices)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny background HLS refresh failure is observed without an unhandled rejection', async () => {
|
||||
const env = environment()
|
||||
const failure = new Error('state unavailable')
|
||||
env.canvas.engine.getHlsState = async () => {
|
||||
throw failure
|
||||
}
|
||||
await env.refresh()
|
||||
assert.equal(env.warnings.length, 1)
|
||||
assert.equal(env.warnings[0][1], failure)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny current HLS selection failure retains its Promise rejection without success notice', async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
const menu = env.controls.get('mediabunny-quality')
|
||||
const failure = new Error('selection unavailable')
|
||||
env.canvas.engine.selectHlsQuality = async () => {
|
||||
throw failure
|
||||
}
|
||||
await assert.rejects(menu.onSelect(menu.selector[0]), error => error === failure)
|
||||
assert.deepEqual(env.notices, [])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny same-label levels retain the actually selected ID and one highlight', async () => {
|
||||
const env = environment()
|
||||
env.option.m3u8.quality.getName = () => 'Same'
|
||||
env.canvas.engine.getHlsState = async () => state(2)
|
||||
await env.refresh()
|
||||
const menu = env.controls.get('mediabunny-quality')
|
||||
assert.deepEqual(Array.from(menu.selector, item => [item.value, item.default]), [[2, true], ['auto', false]])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny HLS single audio topology removes only audio menus', async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
const single = state()
|
||||
single.audios.length = 1
|
||||
env.canvas.engine.getHlsState = async () => single
|
||||
await env.refresh()
|
||||
assert.deepEqual([...env.controls.keys()], ['mediabunny-quality'])
|
||||
assert.deepEqual([...env.settings.keys()], ['mediabunny-quality'])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny HLS name callback destroy cannot recreate controls or settings', async () => {
|
||||
const env = environment()
|
||||
env.option.m3u8.quality.getName = (level) => {
|
||||
env.art.emit('destroy')
|
||||
return level.name
|
||||
}
|
||||
await env.refresh()
|
||||
assert.equal(env.controls.size + env.settings.size, 0)
|
||||
})
|
||||
|
||||
test('MediaBunny HLS control update reentry cannot write later settings or audio menus', async () => {
|
||||
const env = environment()
|
||||
const update = env.art.controls.update
|
||||
env.art.controls.update = (menu) => {
|
||||
update(menu)
|
||||
env.art.emit('destroy')
|
||||
}
|
||||
await env.refresh()
|
||||
assert.equal(env.controls.size + env.settings.size, 0)
|
||||
})
|
||||
|
||||
test('MediaBunny HLS destruction removes its refresh listeners and preserves proxy teardown', async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
const metadataBefore = env.handlers.get('video:loadedmetadata').length
|
||||
env.art.emit('destroy')
|
||||
assert.equal(env.canvas.engine.destroyed, true)
|
||||
assert.equal(env.art.mediabunny, undefined)
|
||||
assert.equal(env.handlers.get('video:loadedmetadata').length, metadataBefore - 1)
|
||||
assert.equal(env.handlers.get('restart')?.length || 0, 0)
|
||||
})
|
||||
|
||||
function selectionEnvironment() {
|
||||
const env = environment({})
|
||||
const pairs = []
|
||||
const videos = [1, 2].map(id => ({ id, canBePairedWith: () => true }))
|
||||
const audios = [3, 4].map(id => ({ id, canBePairedWith: () => true }))
|
||||
env.canvas.engine.input = { getVideoTracks: async () => videos, getAudioTracks: async () => audios }
|
||||
env.canvas.engine.media = { isHls: true, videoTrack: videos[0], audioTrack: audios[0], videoMode: 'auto', audioMode: 'auto' }
|
||||
env.canvas.engine.replaceTracks = async (tracks) => {
|
||||
pairs.push(tracks)
|
||||
env.canvas.engine.media = { ...env.canvas.engine.media, ...tracks }
|
||||
}
|
||||
return { ...env, engine: env.canvas.engine, videos, audios, pairs }
|
||||
}
|
||||
|
||||
for (const [method, query, field, ids] of [['selectHlsQuality', 'getVideoTracks', 'videoTrack', [1, 2]], ['selectHlsAudio', 'getAudioTracks', 'audioTrack', [3, 4]]]) {
|
||||
test(`MediaBunny ${method} honors the latest request when the older query resolves first`, async () => {
|
||||
const env = selectionEnvironment()
|
||||
const first = deferred()
|
||||
const second = deferred()
|
||||
const values = await env.engine.input[query]()
|
||||
let queries = 0
|
||||
env.engine.input[query] = () => (++queries === 1 ? first.promise : second.promise)
|
||||
const older = env.engine[method](ids[0])
|
||||
const newer = env.engine[method](ids[1])
|
||||
first.resolve(values)
|
||||
await flush()
|
||||
second.resolve(values)
|
||||
await Promise.all([older, newer])
|
||||
assert.deepEqual(env.pairs.map(pair => pair[field].id), [ids[1]])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny later audio intent supersedes an unresolved quality query', async () => {
|
||||
const env = selectionEnvironment()
|
||||
const pending = deferred()
|
||||
env.engine.input.getVideoTracks = () => pending.promise
|
||||
const older = env.engine.selectHlsQuality(2)
|
||||
const newer = env.engine.selectHlsAudio(4)
|
||||
pending.resolve(env.videos)
|
||||
await Promise.all([older, newer])
|
||||
assert.equal(env.pairs.length, 1)
|
||||
assert.equal(env.pairs[0].audioTrack.id, 4)
|
||||
assert.equal(env.pairs[0].videoTrack.id, 1)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny HLS selection survives its own loadedmetadata refresh without a duplicate notice', async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
const menu = env.controls.get('mediabunny-quality')
|
||||
env.canvas.engine.selectHlsQuality = async () => {
|
||||
env.canvas.engine.getHlsState = async () => state(2)
|
||||
await env.refresh()
|
||||
}
|
||||
await menu.onSelect(menu.selector[0])
|
||||
assert.equal(env.controls.get('mediabunny-quality').html, '1080P')
|
||||
assert.deepEqual(env.notices, ['Quality: 1080P'])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny HLS selection invalidates an older state read before it can paint', async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
const menu = env.controls.get('mediabunny-quality')
|
||||
const read = deferred()
|
||||
const switchTrack = deferred()
|
||||
env.canvas.engine.getHlsState = () => read.promise
|
||||
const updating = env.refresh()
|
||||
env.canvas.engine.selectHlsQuality = () => switchTrack.promise
|
||||
const selecting = menu.onSelect(menu.selector[0])
|
||||
const writes = env.calls.length
|
||||
read.resolve(state())
|
||||
await updating
|
||||
assert.equal(env.calls.length, writes)
|
||||
env.canvas.engine.getHlsState = async () => state(2)
|
||||
switchTrack.resolve()
|
||||
await selecting
|
||||
assert.equal(env.controls.get('mediabunny-quality').html, '1080P')
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny failed HLS selection restores highlight from actual tracks before rejecting', async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
const menu = env.controls.get('mediabunny-quality')
|
||||
for (const item of menu.selector) item.default = item.value === 2
|
||||
const failure = new Error('track failed')
|
||||
env.canvas.engine.selectHlsQuality = async () => {
|
||||
throw failure
|
||||
}
|
||||
await assert.rejects(menu.onSelect(menu.selector[0]), error => error === failure)
|
||||
assert.deepEqual(Array.from(env.controls.get('mediabunny-quality').selector).filter(item => item.default).map(item => item.value), [1])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny stale HLS selection rejection is observed without disturbing the current source', async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
const menu = env.controls.get('mediabunny-quality')
|
||||
const pending = deferred()
|
||||
env.canvas.engine.selectHlsQuality = () => pending.promise
|
||||
const selecting = menu.onSelect(menu.selector[0])
|
||||
env.canvas.engine.loadSeq++
|
||||
pending.reject(new Error('old source closed'))
|
||||
await selecting
|
||||
assert.equal(env.notices.length + env.warnings.length, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny HLS clears all owned surfaces even when one registry cleanup fails', async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
const failure = new Error('control cleanup failed')
|
||||
const remove = env.art.controls.remove
|
||||
env.art.controls.remove = (name) => {
|
||||
remove(name)
|
||||
throw failure
|
||||
}
|
||||
env.art.emit('destroy')
|
||||
assert.equal(env.controls.size + env.settings.size, 0)
|
||||
assert.equal(env.canvas.engine.destroyed, true)
|
||||
assert.equal(env.handlers.get('restart').length, 0)
|
||||
assert.equal(env.warnings[0][1], failure)
|
||||
})
|
||||
|
||||
test('MediaBunny HLS failing listener removal still attempts remaining removals and proxy teardown', async () => {
|
||||
const env = environment()
|
||||
const off = env.art.off
|
||||
const removed = []
|
||||
env.art.off = (name, callback) => {
|
||||
removed.push(name)
|
||||
off(name, callback)
|
||||
if (name === 'restart')
|
||||
throw new Error('off failed')
|
||||
}
|
||||
env.art.emit('destroy')
|
||||
assert.equal(removed.length, 5)
|
||||
assert.equal(env.canvas.engine.destroyed, true)
|
||||
assert.equal(env.warnings.length, 1)
|
||||
})
|
||||
|
||||
test('MediaBunny runtime HLS disable clears menus on the next refresh', async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
env.option.m3u8 = undefined
|
||||
await env.refresh()
|
||||
assert.equal(env.controls.size + env.settings.size, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny HLS custom labels retain Auto text while highlighting the actual selected track', async () => {
|
||||
const env = environment()
|
||||
env.option.m3u8.audio = { control: true, setting: true, title: 'Language', auto: 'Automatic', getName: track => track.lang }
|
||||
await env.refresh()
|
||||
const menu = env.controls.get('mediabunny-audio')
|
||||
assert.equal(menu.html, 'en')
|
||||
assert.deepEqual(Array.from(menu.selector, item => [item.html, item.default]), [['en', true], ['fr', false], ['Automatic', false]])
|
||||
const values = []
|
||||
env.canvas.engine.selectHlsAudio = async value => values.push(value)
|
||||
await menu.onSelect(menu.selector[2])
|
||||
assert.deepEqual(values, ['auto'])
|
||||
assert.deepEqual(env.notices, ['Language: Automatic'])
|
||||
assert.equal(env.controls.get('mediabunny-audio').html, 'en')
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
for (const kind of ['quality', 'audio']) {
|
||||
test(`MediaBunny ${kind} pairing fallback preserves a coherent pair and resets only the counterpart mode`, async () => {
|
||||
const env = selectionEnvironment()
|
||||
env.engine.media.videoMode = 'manual'
|
||||
env.engine.media.audioMode = 'manual'
|
||||
if (kind === 'quality') {
|
||||
env.videos[1].canBePairedWith = () => false
|
||||
env.videos[1].getPrimaryPairableAudioTrack = async () => env.audios[1]
|
||||
await env.engine.selectHlsQuality(2)
|
||||
assert.deepEqual([env.pairs[0].videoTrack.id, env.pairs[0].audioTrack.id, env.pairs[0].videoMode, env.pairs[0].audioMode], [2, 4, 'manual', 'auto'])
|
||||
}
|
||||
else {
|
||||
env.audios[1].canBePairedWith = () => false
|
||||
env.audios[1].getPrimaryPairableVideoTrack = async () => env.videos[1]
|
||||
await env.engine.selectHlsAudio(4)
|
||||
assert.deepEqual([env.pairs[0].videoTrack.id, env.pairs[0].audioTrack.id, env.pairs[0].videoMode, env.pairs[0].audioMode], [2, 4, 'auto', 'manual'])
|
||||
}
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny latest selection still applies when an earlier replacement changes media during its query', async () => {
|
||||
const env = selectionEnvironment()
|
||||
const replacing = deferred()
|
||||
const query = deferred()
|
||||
env.engine.replaceTracks = async (tracks) => {
|
||||
env.pairs.push(tracks)
|
||||
if (env.pairs.length === 1)
|
||||
await replacing.promise
|
||||
env.engine.media = { ...env.engine.media, ...tracks }
|
||||
}
|
||||
const older = env.engine.selectHlsQuality(1)
|
||||
await flush()
|
||||
env.engine.input.getVideoTracks = () => query.promise
|
||||
const latest = env.engine.selectHlsQuality(2)
|
||||
replacing.resolve()
|
||||
await older
|
||||
query.resolve(env.videos)
|
||||
await latest
|
||||
assert.equal(env.engine.media.videoTrack.id, 2)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny obsolete selection query rejection cannot replace the newer selection result', async () => {
|
||||
const env = selectionEnvironment()
|
||||
const query = deferred()
|
||||
env.engine.input.getVideoTracks = () => query.promise
|
||||
const older = env.engine.selectHlsQuality(2)
|
||||
await env.engine.selectHlsAudio(4)
|
||||
query.reject(new Error('obsolete query'))
|
||||
await older
|
||||
assert.equal(env.engine.media.audioTrack.id, 4)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny Auto audio resolves against the current video when media changes during pairing', async () => {
|
||||
const env = selectionEnvironment()
|
||||
const pending = deferred()
|
||||
env.videos[0].getPrimaryPairableAudioTrack = () => pending.promise
|
||||
env.videos[1].getPrimaryPairableAudioTrack = async () => env.audios[1]
|
||||
const selecting = env.engine.selectHlsAudio('auto')
|
||||
await flush()
|
||||
env.engine.media = { ...env.engine.media, videoTrack: env.videos[1] }
|
||||
pending.resolve(env.audios[0])
|
||||
await selecting
|
||||
assert.equal(env.pairs[0].videoTrack.id, 2)
|
||||
assert.equal(env.pairs[0].audioTrack.id, 4)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
for (const ending of ['resolve', 'reject']) {
|
||||
test(`MediaBunny state ${ending} is discarded when the source generation changes before media replacement`, async () => {
|
||||
const env = environment({})
|
||||
delete env.canvas.engine.getHlsState
|
||||
const query = deferred()
|
||||
env.canvas.engine.media = { isHls: true, input: { getVideoTracks: () => query.promise }, videoTrack: null }
|
||||
const reading = env.canvas.engine.getHlsState()
|
||||
env.canvas.engine.loadSeq++
|
||||
if (ending === 'resolve')
|
||||
query.resolve([])
|
||||
else query.reject(new Error('obsolete state'))
|
||||
assert.equal(await reading, null)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny selection failure superseded during UI recovery does not reject after the newer intent', async () => {
|
||||
const env = environment()
|
||||
await env.refresh()
|
||||
const menu = env.controls.get('mediabunny-quality')
|
||||
const recovery = deferred()
|
||||
env.canvas.engine.getHlsState = () => recovery.promise
|
||||
env.canvas.engine.selectHlsQuality = async () => {
|
||||
throw new Error('first selection failed')
|
||||
}
|
||||
const older = menu.onSelect(menu.selector[0]).then(value => ({ value }), error => ({ error }))
|
||||
await flush()
|
||||
env.canvas.engine.getHlsState = async () => state()
|
||||
env.canvas.engine.selectHlsQuality = async () => {}
|
||||
await menu.onSelect(menu.selector[1])
|
||||
recovery.resolve(state())
|
||||
assert.equal((await older).error, undefined)
|
||||
assert.deepEqual(env.notices, ['Quality: 720P'])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
Reference in new issue
Block a user