mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-18 19:01:20 -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
@@ -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"]
|
||||
}
|
||||
Reference in new issue
Block a user