mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-11 05:26:15 -08:00
refactor(mediabunny): [PKG-MB-03] type inputs and cancel owned loads
This commit is contained in:
1 parent
03451c029b
commit
adba8a3d12
35 files changed
+11260
-3327
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?(t.artplayerProxyMediabunny=t(),define(function(){return t.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";function e(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 t{constructor(){this.listeners=new Map}addEventListener(e,t){this.listeners.has(e)||this.listeners.set(e,[]),this.listeners.get(e).push(t)}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(e,t){const i=new Event(e);i.detail=t;const r=this.listeners.get(e);r&&r.forEach(e=>e(i))}}function i(e){if(!e)throw new Error("Assertion failed.")}const r=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},a=e=>e&&e[e.length-1],s=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)},n=e=>{const t=s(e);return 0===(1&t)?-(t>>1):t+1>>1},o=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),c=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),l=new TextDecoder,u=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),d={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},h=u(d),f={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},m=u(f),p={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},g=u(p),k=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class b{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 y=/^[0-9a-fA-F]+$/,w=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),T=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,S=(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},P=(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},v=(e,t,i)=>{const r=P(e,i(t),i);e.splice(r+1,0,t)},I=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},x=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},C=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},B=e=>{throw new Error(`Unexpected value: ${e}`)},E=(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},_=(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)},A=(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}}),F=(e,t,i)=>Math.max(t,Math.min(i,e)),R="und",D=e=>{const t=Math.round(e);return Math.abs(e/t-1)<10*Number.EPSILON?t:e},M=(e,t)=>Math.round(e*t)/t,N=(e,t)=>Math.floor(e/t)*t,O=/^[a-z]{3}$/,L=e=>O.test(e),z=1e6*(1+Number.EPSILON),U=(e,t)=>{const i={...e,...t};if(e.headers||t.headers){const r=e.headers?q(e.headers):{},a=t.headers?q(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},q=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},W=async(e,t,i,r,a)=>{let s=0;for(;;)try{return await e(t,i)}catch(n){if(a())throw n;s++;const e=r(s,n,t);if(null===e)throw n;if(console.error("Retrying failed fetch. Error:",n),!Number.isFinite(e)||e<0)throw new TypeError("Retry delay must be a non-negative finite number.");if(e>0&&await ce(1e3*e),a())throw n}};class V{constructor(){this.currentPromise=Promise.resolve()}call(e){return this.currentPromise=this.currentPromise.then(e)}}let H=null;const K=()=>null!==H?H:H=!("undefined"===typeof navigator||!(navigator.vendor?.match(/apple/i)||/AppleWebKit/.test(navigator.userAgent)&&!/Chrome/.test(navigator.userAgent)||/\b(iPad|iPhone|iPod)\b/.test(navigator.userAgent)));let j=null;const X=()=>null!==j?j:j="undefined"!==typeof navigator&&navigator.userAgent?.includes("Firefox");let $=null;const Q=()=>null!==$?$:$=!("undefined"===typeof navigator||!navigator.vendor?.includes("Google Inc")&&!/Chrome/.test(navigator.userAgent));let G=null;const Y=()=>{if(null!==G)return G;if("undefined"===typeof navigator)return null;const e=/\bChrome\/(\d+)/.exec(naLine 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";function e(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 t{constructor(){this.listeners=new Map}addEventListener(e,t){this.listeners.has(e)||this.listeners.set(e,[]),this.listeners.get(e).push(t)}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(e,t){const i=new Event(e);i.detail=t;const r=this.listeners.get(e);r&&r.forEach(e=>e(i))}}function i(e){if(!e)throw new Error("Assertion failed.")}const r=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},a=e=>e&&e[e.length-1],s=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)},n=(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)},o=e=>{const t=s(e);return 0===(1&t)?-(t>>1):t+1>>1},c=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),l=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),u=new TextDecoder,d=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),h={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},f=d(h),m={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},p=d(m),g={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},k=d(g),y=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,b={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},w=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class T{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 S=/^[0-9a-fA-F]+$/,v=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),P=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,I=(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},x=(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},C=(e,t,i)=>{const r=x(e,i(t),i);e.splice(r+1,0,t)},B=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},E=(e,t)=>{const i=e.indexOf(t);-1!==i&&e.splice(i,1)},_=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},R=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},F=e=>{throw new Error(`Unexpected value: ${e}`)},A=(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},D=(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)},M=(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}}),N=(e,t,i)=>Math.max(t,Math.min(i,e)),O="und",L=e=>{const t=Math.round(e);return Math.abs(e/t-1)<10*Number.EPSILON?t:e},z=(e,t)=>Math.round(e/t)*t,U=(e,t)=>Math.round(e*t)/t,q=(e,t)=>Math.floor(e/t)*t,W=e=>{let t=0;for(;0!==e;)e&=e-1,t++;return t},H=/^[a-z]{3}$/,V=e=>H.test(e),j=1e6*(1+Number.EPSILON),K=(e,t)=>{const i={...e,...t};if(e.headers||t.headers){const r=e.headers?X(e.headers):{},a=t.headers?X(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},X=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},$=async(e,t,i,r,a)=>{let s=0;for(;;)try{return await e(t,i)}catch(n){if(a())throw n;s++;const e=r(s,n,t);if(null===e)throw n;if(be._error("Retrying failed fetch. Error:",n),!Number.isFinite(e)||e<0)throw new TypeError("Retry delay must be a non-negative finite number.");if(e>0&&await pe(1e3*e),a())throw n}};class G{constructor(){this.currentPromise=Promise.resolve()}call(e){return this.currentPromise=this.currentPromise.then(e)}}let Q=null;const Y=()=>null!==Q?Q:Q=!("undefined"===typeof navigator||!(navigator.vendor?.match(/apple/i)||/AppleWebKit/.test(navigator.userAgent)&&!/Chrome/.test(navigator.userAgent)||/\b(iPad|iPhone|iPod)\b/.test(navigator.userAgent)));let J=null;coLine 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?(t.artplayerProxyMediabunny=t(),define(function(){return t.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.defineProperties,i=Object.getOwnPropertyDescriptors,r=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,a=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),o=Math.pow,l=(t,i,r)=>i in t?e(t,i,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[i]=r,c=(e,t)=>{for(var i in t||(t={}))n.call(t,i)&&l(e,i,t[i]);if(r)for(var i of r(t))s.call(t,i)&&l(e,i,t[i]);return e},u=(e,r)=>t(e,i(r)),d=(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())}),h=function(e,t){this[0]=e,this[1]=t},f=(e,t,i)=>{var r=(e,t,n,s)=>{try{var a=i[e](t),o=(t=a.value)instanceof h,l=a.done;Promise.resolve(o?t[0]:t).then(i=>o?r("return"===e?e:"next",t[1]?{done:i.done,value:i.value}:i,n,s):n({value:i,done:l})).catch(e=>r("throw",e,n,s))}catch(c){s(c)}},n=e=>s[e]=t=>new Promise((i,n)=>r(e,t,i,n)),s={};return i=i.apply(e,t),s[a("asyncIterator")]=()=>s,n("next"),n("throw"),n("return"),s},m=e=>{var t,i=e[a("asyncIterator")],r=!1,n={};return null==i?(i=e[a("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 h(new Promise(r=>{var n=i[e](t);n instanceof Object||(e=>{throw TypeError(e)})("Object expected"),r(n)}),1)}}),n[a("iterator")]=()=>n,t("next"),"throw"in i?t("throw"):n.throw=e=>{throw e},"return"in i&&t("return"),n},p=(e,t,i)=>(t=e[a("asyncIterator")])?t.call(e):(e=e[a("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 g(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 k{constructor(){this.listeners=new Map}addEventListener(e,t){this.listeners.has(e)||this.listeners.set(e,[]),this.listeners.get(e).push(t)}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(e,t){const i=new Event(e);i.detail=t;const r=this.listeners.get(e);r&&r.forEach(e=>e(i))}}function y(e){if(!e)throw new Error("Assertion failed.")}const b=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},T=e=>e&&e[e.length-1],S=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)},v=e=>{const t=S(e);return 0===(1&t)?-(t>>1):t+1>>1},w=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),P=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),I=new TextDecoder,x=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),C={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},B=x(C),E={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},_=x(E),A={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},F=x(A),R=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class D{constructor(){this.currentPromise=Promise.resolve(),this.pending=0}acquire(){return d(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 M=/^[0-9a-fA-F]+$/,N=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),O=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,L=(e,t,i)=>{let r=0,n=e.length-1,s=-1;for(;r<=n;){const a=r+n>>1,o=i(e[a]);o===t?(s=a,n=a-1):o<t?r=a+1:n=a-1}return s},z=(e,t,i)=>{let r=0,n=e.length-1,s=-1;for(;r<=n;){const a=r+(n-r+1)/2|0;i(e[a])<=t?(s=a,r=a+1):n=a-1}return s},U=(e,t,i)=>{const r=z(e,i(t),i);e.splice(r+1,0,t)},q=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},W=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},V=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},H=e=>{throw new Error(`Unexpected value: ${e}`)},K=(e,t,i)=>{const r=e.getUint8(t),n=e.getUint8(t+1),s=e.getUint8(t+2);return i?r|n<<8|s<<16:r<<16|n<<8|s},j=(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)},X=(e,t)=>({next(){return d(this,null,function*(){const i=yield e.next();return i.done?{value:void 0,done:!0}:{value:t(i.value),done:!1}})}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=Object.defineProperty,t=Object.defineProperties,i=Object.getOwnPropertyDescriptors,r=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,a=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),o=Math.pow,l=(t,i,r)=>i in t?e(t,i,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[i]=r,c=(e,t)=>{for(var i in t||(t={}))n.call(t,i)&&l(e,i,t[i]);if(r)for(var i of r(t))s.call(t,i)&&l(e,i,t[i]);return e},u=(e,r)=>t(e,i(r)),d=(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())}),h=function(e,t){this[0]=e,this[1]=t},f=(e,t,i)=>{var r,n=(e,t,r,s)=>{try{var a=i[e](t),o=(t=a.value)instanceof h,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)=>o[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),o={};return i=i.apply(e,t),o[a("asyncIterator")]=()=>o,s("next"),s("throw"),s("return"),o},m=e=>{var t,i=e[a("asyncIterator")],r=!1,n={};return null==i?(i=e[a("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 h(new Promise(r=>{var n=i[e](t);n instanceof Object||(e=>{throw TypeError(e)})("Object expected"),r(n)}),1)}}),n[a("iterator")]=()=>n,t("next"),"throw"in i?t("throw"):n.throw=e=>{throw e},"return"in i&&t("return"),n},p=(e,t,i)=>(t=e[a("asyncIterator")])?t.call(e):(e=e[a("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 g(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 k{constructor(){this.listeners=new Map}addEventListener(e,t){this.listeners.has(e)||this.listeners.set(e,[]),this.listeners.get(e).push(t)}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(e,t){const i=new Event(e);i.detail=t;const r=this.listeners.get(e);r&&r.forEach(e=>e(i))}}function y(e){if(!e)throw new Error("Assertion failed.")}const b=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},T=e=>e&&e[e.length-1],v=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)},S=(e,t)=>{const i=t+1,r=Math.floor(Math.log2(i));e.writeBits(r,0),e.writeBits(1,1),e.writeBits(r,i-o(2,r))},w=e=>{const t=v(e);return 0===(1&t)?-(t>>1):t+1>>1},P=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),I=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),x=new TextDecoder,C=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),B={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},E=C(B),_={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},R=C(_),F={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},A=C(F),D=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,M={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},N=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class O{constructor(){this.currentPromise=Promise.resolve(),this.pending=0}acquire(){return d(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 L=/^[0-9a-fA-F]+$/,z=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),U=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,q=(e,t,i)=>{let r=0,n=e.length-1,s=-1;for(;r<=n;){const a=r+n>>1,o=i(e[a]);o===t?(s=a,n=a-1):o<t?r=a+1:n=a-1}return s},W=(e,t,i)=>{let r=0,n=e.length-1,s=-1;for(;r<=n;){const a=r+(n-r+1)/2|0;i(e[a])<=t?(s=a,r=a+1):n=a-1}return s},H=(e,t,i)=>{const r=W(e,i(t),i);e.splice(r+1,0,t)},V=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},j=(e,t)=>{const i=e.indexOf(t);-1!==i&&e.splice(i,1)},K=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},X=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},$=e=>{throw new ErrorLine truncated
|
||||
File diff suppressed because it is too large.
Load diff
+3
-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/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-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": "node --test test/mediabunny.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
@@ -108,6 +108,7 @@
|
||||
"yaml": "2.8.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/@types/dom-webcodecs": "0.1.19",
|
||||
"dts-bundle-generator/typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
# MediaBunny proxy maintenance
|
||||
|
||||
The public entry is `src/index.js`: an optional-options factory returns the existing
|
||||
ArtPlayer proxy callback and an HTMLCanvasElement. Public declarations remain in
|
||||
`types/artplayer-proxy-mediabunny.d.ts`. The input migration does not change those
|
||||
declarations, the factory signature, canvas forwarding, or package entrypoints.
|
||||
|
||||
## Current module map
|
||||
|
||||
| Module | Responsibility |
|
||||
| --- | --- |
|
||||
| `input.ts` | String/Blob/ReadableStream normalization, SDK Source/SourceRef pass-through, HLS detection and Input construction |
|
||||
| `media.ts` | Internal SDK-backed media and track-mode types; not public consumer types |
|
||||
| `tracks.ts` | Primary video and pairable audio selection, metadata/computed/live duration |
|
||||
| `hls-state.ts` | HLS track snapshots, bitrate/name/language values and actual selected track identity |
|
||||
| `preflight.ts` | Optional HEAD Range check, historical warning/error policy, cancellation and stale-result guard |
|
||||
| `load-session.ts` | Ownership of the pending/active SDK Input, HEAD AbortController, deferred load events and deadline |
|
||||
| `MediaBunnyEngine.js` | Current coordinator; connects session cancellation to load, source replacement, errors and destroy |
|
||||
| `VideoEngine.js` / `AudioEngine.js` | Current native decoding, rendering, seeking, Web Audio clock and scheduling |
|
||||
| `VideoShim.js` / `EventTarget.js` | Current video-like descriptors and event forwarding |
|
||||
| `m3u8.js` | Current ArtPlayer control/setting integration |
|
||||
|
||||
The six TypeScript modules use strict checking with `skipLibCheck: false` and no
|
||||
ambient Node types. Remaining JavaScript is explicitly scheduled in PKG-MB-04/05/06/07/08.
|
||||
Do not describe the whole package as migrated yet.
|
||||
|
||||
## Input and cancellation flow
|
||||
|
||||
Each `engine.load` invalidates the previous generation and cancels its session.
|
||||
The new session owns an Input immediately after construction, before asynchronous
|
||||
track selection. `engine.input` and `engine.media` become visible only after selection.
|
||||
The session continues to own the Input during playback; these fields are aliases,
|
||||
not separate disposal owners. Repeated cancellation/disposal is harmless.
|
||||
|
||||
Source replacement, timeout and destroy release that Input through SDK `Input.dispose()`.
|
||||
MediaBunny controls its media fetch cancellation; do not pass a signal in UrlSource's
|
||||
`requestInit` (the SDK explicitly excludes it). The separate HEAD check uses its own
|
||||
AbortController. Without that constructor, stale HEAD results/errors are still ignored,
|
||||
although the network request cannot be physically aborted by this adapter.
|
||||
|
||||
Cancellation settles the old load Promise without waiting for a stalled read. Its late
|
||||
fulfillment/rejection remains observed but cannot update the current load. Active errors
|
||||
retain `code: 4` and the SDK message; active timeout retains `Load timeout` and networkState 3.
|
||||
Successful completion clears its deadline. Normal deferred waiting/loadstart ordering
|
||||
remains; cancellation clears both callbacks. Destroy makes future loads inert.
|
||||
|
||||
Range behavior remains opt-in: no/`none` accept-ranges emits an error with a
|
||||
`RangeNotSupported` detail and returns false, without setting the engine error property.
|
||||
Network failure warns and continues; HLS strings and non-string sources skip HEAD.
|
||||
HLS matching preserves the old case-insensitive `.m3u8` suffix/query/hash rule.
|
||||
One explicit InputOptions assertion leaves unsupported values to SDK validation, preserving
|
||||
the old error owner rather than silently converting arbitrary inputs.
|
||||
|
||||
## Types and dependencies
|
||||
|
||||
The locked runtime SDK remains MediaBunny 1.56.1. Its pinned WebCodecs declaration
|
||||
0.1.13 duplicates types now present in TypeScript 5.9.3's DOM library. The root Yarn
|
||||
resolution selects `@types/dom-webcodecs@0.1.19` for both dependency paths; the SDK
|
||||
itself is unchanged. This is a build-time declaration correction, not a browser polyfill.
|
||||
The package typecheck adds ESNext.Disposable for SDK declarations while keeping the
|
||||
ES2020 target; production source does not use `using` or require Symbol.dispose.
|
||||
Public consumer declarations do not import these SDK types; preserve that boundary
|
||||
until the separate consumer-compatibility task verifies any proposed change.
|
||||
|
||||
## Validation and remaining work
|
||||
|
||||
```sh
|
||||
yarn test:mediabunny
|
||||
node node_modules/typescript/bin/tsc -p packages/artplayer-proxy-mediabunny/tsconfig.json
|
||||
yarn build artplayer-proxy-mediabunny
|
||||
yarn test:browser test/browser/mediabunny-inputs.spec.js test/browser/mediabunny.spec.js test/browser/mediabunny-load.spec.js
|
||||
```
|
||||
|
||||
Node tests use actual SDK parsing and controlled lifecycle interleavings. Browser tests
|
||||
include native decoding and separate pending ReadableStream cancellation with real timers.
|
||||
`ARTPLAYER_MB_BASELINE=1` runs candidate load assertions against the immutable old main
|
||||
to reproduce failures. `ARTPLAYER_MB_ARTIFACT` selects a built UMD main/legacy bundle;
|
||||
`ARTPLAYER_MB_BROWSER_CANDIDATE=1` restricts browser runs to that candidate. Browser
|
||||
reports record the implementation hash; archive the report and results before rerunning.
|
||||
|
||||
The source of historical truth is `refactor/baselines/mb-release.json` and its Git/npm
|
||||
inputs, not a rebuilt dist. Package-specific evidence and remaining tasks are in
|
||||
`refactor/mb-validation.md`. The full refactor plan is at the repository root.
|
||||
|
||||
PKG-MB-04 still owns duplicate/trackless readiness, reentrant event sequences and
|
||||
pending play/seek coordination. PKG-MB-05/06 own late frame and audio callbacks,
|
||||
including operations already inside decoders at cancellation; releasing Input alone
|
||||
does not prove those resources are fully handled. PKG-MB-07 owns HLS UI/selection
|
||||
races. 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.
|
||||
@@ -9,3 +9,7 @@ mediabunny proxy for ArtPlayer, including `m3u8` playback through Mediabunny's H
|
||||
## License
|
||||
|
||||
MIT © Harvey Zhao
|
||||
|
||||
## Maintenance
|
||||
|
||||
See [ARCHITECTURE.md](ARCHITECTURE.md) for current modules, resource ownership, compatibility boundaries and validation commands.
|
||||
@@ -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?(t.artplayerProxyMediabunny=t(),define(function(){return t.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";function e(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 t{constructor(){this.listeners=new Map}addEventListener(e,t){this.listeners.has(e)||this.listeners.set(e,[]),this.listeners.get(e).push(t)}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(e,t){const i=new Event(e);i.detail=t;const r=this.listeners.get(e);r&&r.forEach(e=>e(i))}}function i(e){if(!e)throw new Error("Assertion failed.")}const r=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},a=e=>e&&e[e.length-1],s=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)},n=e=>{const t=s(e);return 0===(1&t)?-(t>>1):t+1>>1},o=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),c=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),l=new TextDecoder,u=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),d={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},h=u(d),f={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},m=u(f),p={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},g=u(p),k=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class b{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 y=/^[0-9a-fA-F]+$/,w=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),T=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,S=(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},P=(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},v=(e,t,i)=>{const r=P(e,i(t),i);e.splice(r+1,0,t)},I=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},x=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},C=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},B=e=>{throw new Error(`Unexpected value: ${e}`)},E=(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},_=(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)},A=(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}}),F=(e,t,i)=>Math.max(t,Math.min(i,e)),R="und",D=e=>{const t=Math.round(e);return Math.abs(e/t-1)<10*Number.EPSILON?t:e},M=(e,t)=>Math.round(e*t)/t,N=(e,t)=>Math.floor(e/t)*t,O=/^[a-z]{3}$/,L=e=>O.test(e),z=1e6*(1+Number.EPSILON),U=(e,t)=>{const i={...e,...t};if(e.headers||t.headers){const r=e.headers?q(e.headers):{},a=t.headers?q(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},q=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},W=async(e,t,i,r,a)=>{let s=0;for(;;)try{return await e(t,i)}catch(n){if(a())throw n;s++;const e=r(s,n,t);if(null===e)throw n;if(console.error("Retrying failed fetch. Error:",n),!Number.isFinite(e)||e<0)throw new TypeError("Retry delay must be a non-negative finite number.");if(e>0&&await ce(1e3*e),a())throw n}};class V{constructor(){this.currentPromise=Promise.resolve()}call(e){return this.currentPromise=this.currentPromise.then(e)}}let H=null;const K=()=>null!==H?H:H=!("undefined"===typeof navigator||!(navigator.vendor?.match(/apple/i)||/AppleWebKit/.test(navigator.userAgent)&&!/Chrome/.test(navigator.userAgent)||/\b(iPad|iPhone|iPod)\b/.test(navigator.userAgent)));let j=null;const X=()=>null!==j?j:j="undefined"!==typeof navigator&&navigator.userAgent?.includes("Firefox");let $=null;const Q=()=>null!==$?$:$=!("undefined"===typeof navigator||!navigator.vendor?.includes("Google Inc")&&!/Chrome/.test(navigator.userAgent));let G=null;const Y=()=>{if(null!==G)return G;if("undefined"===typeof navigator)return null;const e=/\bChrome\/(\d+)/.exec(naLine 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";function e(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 t{constructor(){this.listeners=new Map}addEventListener(e,t){this.listeners.has(e)||this.listeners.set(e,[]),this.listeners.get(e).push(t)}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(e,t){const i=new Event(e);i.detail=t;const r=this.listeners.get(e);r&&r.forEach(e=>e(i))}}function i(e){if(!e)throw new Error("Assertion failed.")}const r=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},a=e=>e&&e[e.length-1],s=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)},n=(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)},o=e=>{const t=s(e);return 0===(1&t)?-(t>>1):t+1>>1},c=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),l=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),u=new TextDecoder,d=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),h={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},f=d(h),m={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},p=d(m),g={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},k=d(g),y=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,b={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},w=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class T{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 S=/^[0-9a-fA-F]+$/,v=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),P=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,I=(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},x=(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},C=(e,t,i)=>{const r=x(e,i(t),i);e.splice(r+1,0,t)},B=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},E=(e,t)=>{const i=e.indexOf(t);-1!==i&&e.splice(i,1)},_=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},R=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},F=e=>{throw new Error(`Unexpected value: ${e}`)},A=(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},D=(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)},M=(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}}),N=(e,t,i)=>Math.max(t,Math.min(i,e)),O="und",L=e=>{const t=Math.round(e);return Math.abs(e/t-1)<10*Number.EPSILON?t:e},z=(e,t)=>Math.round(e/t)*t,U=(e,t)=>Math.round(e*t)/t,q=(e,t)=>Math.floor(e/t)*t,W=e=>{let t=0;for(;0!==e;)e&=e-1,t++;return t},H=/^[a-z]{3}$/,V=e=>H.test(e),j=1e6*(1+Number.EPSILON),K=(e,t)=>{const i={...e,...t};if(e.headers||t.headers){const r=e.headers?X(e.headers):{},a=t.headers?X(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},X=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},$=async(e,t,i,r,a)=>{let s=0;for(;;)try{return await e(t,i)}catch(n){if(a())throw n;s++;const e=r(s,n,t);if(null===e)throw n;if(be._error("Retrying failed fetch. Error:",n),!Number.isFinite(e)||e<0)throw new TypeError("Retry delay must be a non-negative finite number.");if(e>0&&await pe(1e3*e),a())throw n}};class G{constructor(){this.currentPromise=Promise.resolve()}call(e){return this.currentPromise=this.currentPromise.then(e)}}let Q=null;const Y=()=>null!==Q?Q:Q=!("undefined"===typeof navigator||!(navigator.vendor?.match(/apple/i)||/AppleWebKit/.test(navigator.userAgent)&&!/Chrome/.test(navigator.userAgent)||/\b(iPad|iPhone|iPod)\b/.test(navigator.userAgent)));let J=null;coLine 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?(t.artplayerProxyMediabunny=t(),define(function(){return t.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e=Object.defineProperty,t=Object.defineProperties,i=Object.getOwnPropertyDescriptors,r=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,a=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),o=Math.pow,l=(t,i,r)=>i in t?e(t,i,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[i]=r,c=(e,t)=>{for(var i in t||(t={}))n.call(t,i)&&l(e,i,t[i]);if(r)for(var i of r(t))s.call(t,i)&&l(e,i,t[i]);return e},u=(e,r)=>t(e,i(r)),d=(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())}),h=function(e,t){this[0]=e,this[1]=t},f=(e,t,i)=>{var r=(e,t,n,s)=>{try{var a=i[e](t),o=(t=a.value)instanceof h,l=a.done;Promise.resolve(o?t[0]:t).then(i=>o?r("return"===e?e:"next",t[1]?{done:i.done,value:i.value}:i,n,s):n({value:i,done:l})).catch(e=>r("throw",e,n,s))}catch(c){s(c)}},n=e=>s[e]=t=>new Promise((i,n)=>r(e,t,i,n)),s={};return i=i.apply(e,t),s[a("asyncIterator")]=()=>s,n("next"),n("throw"),n("return"),s},m=e=>{var t,i=e[a("asyncIterator")],r=!1,n={};return null==i?(i=e[a("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 h(new Promise(r=>{var n=i[e](t);n instanceof Object||(e=>{throw TypeError(e)})("Object expected"),r(n)}),1)}}),n[a("iterator")]=()=>n,t("next"),"throw"in i?t("throw"):n.throw=e=>{throw e},"return"in i&&t("return"),n},p=(e,t,i)=>(t=e[a("asyncIterator")])?t.call(e):(e=e[a("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 g(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 k{constructor(){this.listeners=new Map}addEventListener(e,t){this.listeners.has(e)||this.listeners.set(e,[]),this.listeners.get(e).push(t)}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(e,t){const i=new Event(e);i.detail=t;const r=this.listeners.get(e);r&&r.forEach(e=>e(i))}}function y(e){if(!e)throw new Error("Assertion failed.")}const b=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},T=e=>e&&e[e.length-1],S=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)},v=e=>{const t=S(e);return 0===(1&t)?-(t>>1):t+1>>1},w=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),P=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),I=new TextDecoder,x=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),C={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},B=x(C),E={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},_=x(E),A={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},F=x(A),R=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class D{constructor(){this.currentPromise=Promise.resolve(),this.pending=0}acquire(){return d(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 M=/^[0-9a-fA-F]+$/,N=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),O=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,L=(e,t,i)=>{let r=0,n=e.length-1,s=-1;for(;r<=n;){const a=r+n>>1,o=i(e[a]);o===t?(s=a,n=a-1):o<t?r=a+1:n=a-1}return s},z=(e,t,i)=>{let r=0,n=e.length-1,s=-1;for(;r<=n;){const a=r+(n-r+1)/2|0;i(e[a])<=t?(s=a,r=a+1):n=a-1}return s},U=(e,t,i)=>{const r=z(e,i(t),i);e.splice(r+1,0,t)},q=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},W=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},V=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},H=e=>{throw new Error(`Unexpected value: ${e}`)},K=(e,t,i)=>{const r=e.getUint8(t),n=e.getUint8(t+1),s=e.getUint8(t+2);return i?r|n<<8|s<<16:r<<16|n<<8|s},j=(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)},X=(e,t)=>({next(){return d(this,null,function*(){const i=yield e.next();return i.done?{value:void 0,done:!0}:{value:t(i.value),done:!1}})}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=Object.defineProperty,t=Object.defineProperties,i=Object.getOwnPropertyDescriptors,r=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,s=Object.prototype.propertyIsEnumerable,a=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),o=Math.pow,l=(t,i,r)=>i in t?e(t,i,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[i]=r,c=(e,t)=>{for(var i in t||(t={}))n.call(t,i)&&l(e,i,t[i]);if(r)for(var i of r(t))s.call(t,i)&&l(e,i,t[i]);return e},u=(e,r)=>t(e,i(r)),d=(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())}),h=function(e,t){this[0]=e,this[1]=t},f=(e,t,i)=>{var r,n=(e,t,r,s)=>{try{var a=i[e](t),o=(t=a.value)instanceof h,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)=>o[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),o={};return i=i.apply(e,t),o[a("asyncIterator")]=()=>o,s("next"),s("throw"),s("return"),o},m=e=>{var t,i=e[a("asyncIterator")],r=!1,n={};return null==i?(i=e[a("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 h(new Promise(r=>{var n=i[e](t);n instanceof Object||(e=>{throw TypeError(e)})("Object expected"),r(n)}),1)}}),n[a("iterator")]=()=>n,t("next"),"throw"in i?t("throw"):n.throw=e=>{throw e},"return"in i&&t("return"),n},p=(e,t,i)=>(t=e[a("asyncIterator")])?t.call(e):(e=e[a("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 g(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 k{constructor(){this.listeners=new Map}addEventListener(e,t){this.listeners.has(e)||this.listeners.set(e,[]),this.listeners.get(e).push(t)}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(e,t){const i=new Event(e);i.detail=t;const r=this.listeners.get(e);r&&r.forEach(e=>e(i))}}function y(e){if(!e)throw new Error("Assertion failed.")}const b=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},T=e=>e&&e[e.length-1],v=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)},S=(e,t)=>{const i=t+1,r=Math.floor(Math.log2(i));e.writeBits(r,0),e.writeBits(1,1),e.writeBits(r,i-o(2,r))},w=e=>{const t=v(e);return 0===(1&t)?-(t>>1):t+1>>1},P=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),I=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),x=new TextDecoder,C=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),B={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},E=C(B),_={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},R=C(_),F={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},A=C(F),D=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,M={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},N=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class O{constructor(){this.currentPromise=Promise.resolve(),this.pending=0}acquire(){return d(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 L=/^[0-9a-fA-F]+$/,z=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),U=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,q=(e,t,i)=>{let r=0,n=e.length-1,s=-1;for(;r<=n;){const a=r+n>>1,o=i(e[a]);o===t?(s=a,n=a-1):o<t?r=a+1:n=a-1}return s},W=(e,t,i)=>{let r=0,n=e.length-1,s=-1;for(;r<=n;){const a=r+(n-r+1)/2|0;i(e[a])<=t?(s=a,r=a+1):n=a-1}return s},H=(e,t,i)=>{const r=W(e,i(t),i);e.splice(r+1,0,t)},V=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},j=(e,t)=>{const i=e.indexOf(t);-1!==i&&e.splice(i,1)},K=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},X=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},$=e=>{throw new ErrorLine truncated
|
||||
+4485
-1568
File diff suppressed because it is too large.
Load diff
@@ -3,7 +3,10 @@
|
||||
* Coordinates audio and video playback
|
||||
*/
|
||||
import AudioEngine from './AudioEngine.js'
|
||||
import { createInput, getHlsState, resolveDuration, selectPlaybackTracks } from './input.js'
|
||||
import { getHlsState } from './hls-state'
|
||||
import { createInput } from './input'
|
||||
import LoadSession from './load-session'
|
||||
import { resolveDuration, selectPlaybackTracks } from './tracks'
|
||||
import VideoEngine from './VideoEngine.js'
|
||||
|
||||
export default class MediaBunnyEngine {
|
||||
@@ -34,6 +37,8 @@ export default class MediaBunnyEngine {
|
||||
this.loadSeq = 0
|
||||
this.input = null
|
||||
this.media = null
|
||||
this.loadSession = null
|
||||
this.destroyed = false
|
||||
|
||||
// Listen to ended event
|
||||
events.addEventListener?.('ended', () => {
|
||||
@@ -43,41 +48,50 @@ export default class MediaBunnyEngine {
|
||||
}
|
||||
|
||||
async load(src) {
|
||||
if (this.destroyed)
|
||||
return
|
||||
const id = ++this.loadSeq
|
||||
|
||||
this.loadSession?.cancel()
|
||||
if (id !== this.loadSeq || this.destroyed)
|
||||
return
|
||||
this.pause()
|
||||
if (id !== this.loadSeq || this.destroyed)
|
||||
return
|
||||
this.disposeInput()
|
||||
const session = new LoadSession()
|
||||
this.loadSession = session
|
||||
this.ended = false
|
||||
this.error = null
|
||||
this.networkState = 2 // NETWORK_LOADING
|
||||
this.readyState = 0 // HAVE_NOTHING
|
||||
|
||||
setTimeout(() => this.events.emit('waiting'), 0)
|
||||
setTimeout(() => this.events.emit('loadstart'), 0)
|
||||
session.defer(() => this.events.emit('waiting'))
|
||||
session.defer(() => this.events.emit('loadstart'))
|
||||
|
||||
const loadTimeout = Number.isFinite(this.option.loadTimeout)
|
||||
? this.option.loadTimeout
|
||||
: 0
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
this.performLoad(src, id),
|
||||
loadTimeout > 0 ? this.createTimeout(loadTimeout) : new Promise(() => {}),
|
||||
])
|
||||
await session.wait(this.performLoad(src, id, session), loadTimeout)
|
||||
}
|
||||
catch (err) {
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
|
||||
this.loadSeq++
|
||||
const failed = ++this.loadSeq
|
||||
session.cancel()
|
||||
if (failed !== this.loadSeq || this.destroyed)
|
||||
return
|
||||
this.disposeInput()
|
||||
this.error = { code: 4, message: err.message }
|
||||
this.networkState = 3 // NETWORK_NO_SOURCE
|
||||
this.events.emit('error')
|
||||
}
|
||||
}
|
||||
|
||||
async performLoad(src, id) {
|
||||
if (!(await this.video.preflight(src)))
|
||||
async performLoad(src, id, session = this.loadSession) {
|
||||
if (!(await this.video.preflight(src, session?.signal, () => id === this.loadSeq)) || id !== this.loadSeq)
|
||||
return
|
||||
|
||||
const input = createInput(src)
|
||||
@@ -86,14 +100,10 @@ export default class MediaBunnyEngine {
|
||||
this.audio.handleNoAudioTrack()
|
||||
return
|
||||
}
|
||||
|
||||
session.own(input)
|
||||
const media = await selectPlaybackTracks(input, src)
|
||||
if (id !== this.loadSeq) {
|
||||
input.dispose()
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
}
|
||||
|
||||
this.disposeInput()
|
||||
this.input = input
|
||||
this.media = media
|
||||
|
||||
@@ -139,10 +149,8 @@ export default class MediaBunnyEngine {
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
|
||||
this.error = { code: 4, message: err.message }
|
||||
this.networkState = 3
|
||||
this.events.emit('error')
|
||||
console.error('MediaBunny load error:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +161,7 @@ export default class MediaBunnyEngine {
|
||||
}
|
||||
|
||||
disposeInput() {
|
||||
this.input?.dispose()
|
||||
this.loadSession?.disposeInput()
|
||||
this.input = null
|
||||
this.media = null
|
||||
}
|
||||
@@ -334,6 +342,11 @@ export default class MediaBunnyEngine {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.destroyed)
|
||||
return
|
||||
this.destroyed = true
|
||||
this.loadSeq++
|
||||
this.loadSession?.cancel()
|
||||
this.pause()
|
||||
this.disposeInput()
|
||||
this.audio.destroy()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import {
|
||||
CanvasSink,
|
||||
} from 'mediabunny'
|
||||
import { isHlsSource } from './input.js'
|
||||
import { preflightRange } from './preflight'
|
||||
|
||||
export default class VideoEngine {
|
||||
constructor({
|
||||
@@ -51,23 +51,8 @@ export default class VideoEngine {
|
||||
this.isFetching = false
|
||||
}
|
||||
|
||||
async preflight(url) {
|
||||
if (!this.preflightRange || typeof url !== 'string' || isHlsSource(url))
|
||||
return true
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { method: 'HEAD' })
|
||||
const acceptRanges = res.headers.get('accept-ranges')
|
||||
if (!acceptRanges || acceptRanges === 'none') {
|
||||
this.events.emit('error', new Event('RangeNotSupported'))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('Preflight check failed:', e)
|
||||
return true
|
||||
}
|
||||
async preflight(url, signal, isCurrent) {
|
||||
return preflightRange(url, this.preflightRange, this.events, signal, isCurrent)
|
||||
}
|
||||
|
||||
drawPoster() {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { InputTrack } from 'mediabunny'
|
||||
import type { PlaybackMedia } from './media'
|
||||
|
||||
async function getTrackBitrate(track: InputTrack): Promise<number> {
|
||||
return await track.getAverageBitrate() ?? await track.getBitrate() ?? 0
|
||||
}
|
||||
|
||||
export async function getHlsState(media: PlaybackMedia | null) {
|
||||
if (!media?.isHls || !media.input)
|
||||
return null
|
||||
|
||||
const [videoTracks, pairableAudioTracks] = await Promise.all([
|
||||
media.input.getVideoTracks(),
|
||||
media.videoTrack ? media.videoTrack.getPairableAudioTracks() : [],
|
||||
])
|
||||
const [levels, audios] = await Promise.all([
|
||||
Promise.all(videoTracks.map(async (track, index) => ({
|
||||
id: track.id,
|
||||
track,
|
||||
index,
|
||||
name: await track.getName(),
|
||||
height: await track.getDisplayHeight(),
|
||||
bitrate: await getTrackBitrate(track),
|
||||
}))),
|
||||
Promise.all(pairableAudioTracks.map(async (track, index) => {
|
||||
const name = await track.getName()
|
||||
const language = await track.getLanguageCode()
|
||||
return {
|
||||
id: track.id,
|
||||
track,
|
||||
index,
|
||||
name,
|
||||
lang: language,
|
||||
language,
|
||||
bitrate: await getTrackBitrate(track),
|
||||
}
|
||||
})),
|
||||
])
|
||||
return {
|
||||
levels,
|
||||
audios,
|
||||
currentLevel: media.videoTrack ? levels.find(item => item.id === media.videoTrack?.id) ?? null : null,
|
||||
currentAudio: media.audioTrack ? audios.find(item => item.id === media.audioTrack?.id) ?? null : null,
|
||||
videoMode: media.videoMode ?? 'auto',
|
||||
audioMode: media.audioMode ?? 'auto',
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
import {
|
||||
ALL_FORMATS,
|
||||
BlobSource,
|
||||
HLS_FORMATS,
|
||||
Input,
|
||||
ReadableStreamSource,
|
||||
UrlSource,
|
||||
} from 'mediabunny'
|
||||
|
||||
const M3U8_RE = /\.m3u8(?:$|[?#])/i
|
||||
|
||||
export function isHlsSource(src) {
|
||||
return typeof src === 'string' && M3U8_RE.test(src)
|
||||
}
|
||||
|
||||
export function normalizeSource(src) {
|
||||
if (typeof src === 'string')
|
||||
return new UrlSource(src)
|
||||
if (src instanceof Blob)
|
||||
return new BlobSource(src)
|
||||
if (typeof ReadableStream !== 'undefined' && src instanceof ReadableStream) {
|
||||
return new ReadableStreamSource(src)
|
||||
}
|
||||
return src ?? null
|
||||
}
|
||||
|
||||
export function createInput(src) {
|
||||
const source = normalizeSource(src)
|
||||
if (!source)
|
||||
return null
|
||||
|
||||
return new Input({
|
||||
source,
|
||||
formats: isHlsSource(src) ? HLS_FORMATS : ALL_FORMATS,
|
||||
})
|
||||
}
|
||||
|
||||
export async function resolveDuration({ input, videoTrack, audioTrack }) {
|
||||
const tracks = [videoTrack, audioTrack].filter(Boolean)
|
||||
const referenceTrack = videoTrack || audioTrack
|
||||
const isLive = Boolean(referenceTrack && await referenceTrack.isLive())
|
||||
|
||||
let duration = Number.NaN
|
||||
if (tracks.length > 0) {
|
||||
duration = await input.getDurationFromMetadata(tracks, { skipLiveWait: true })
|
||||
if (duration === null && !isLive) {
|
||||
duration = await input.computeDuration(tracks, { skipLiveWait: true })
|
||||
}
|
||||
}
|
||||
|
||||
return isLive ? Number.POSITIVE_INFINITY : duration ?? Number.NaN
|
||||
}
|
||||
|
||||
export async function selectPlaybackTracks(input, src) {
|
||||
const videoTrack = await input.getPrimaryVideoTrack()
|
||||
const audioTrack = videoTrack
|
||||
? await videoTrack.getPrimaryPairableAudioTrack()
|
||||
: await input.getPrimaryAudioTrack()
|
||||
const duration = await resolveDuration({ input, videoTrack, audioTrack })
|
||||
|
||||
return {
|
||||
input,
|
||||
videoTrack,
|
||||
audioTrack,
|
||||
duration,
|
||||
isLive: Number.isFinite(duration) ? false : Boolean((videoTrack || audioTrack) && await (videoTrack || audioTrack).isLive()),
|
||||
isHls: isHlsSource(src),
|
||||
videoMode: isHlsSource(src) ? 'auto' : 'manual',
|
||||
audioMode: isHlsSource(src) ? 'auto' : 'manual',
|
||||
}
|
||||
}
|
||||
|
||||
async function getTrackBitrate(track) {
|
||||
return await track.getAverageBitrate() ?? await track.getBitrate() ?? 0
|
||||
}
|
||||
|
||||
export async function getHlsState(media) {
|
||||
if (!media?.isHls || !media.input)
|
||||
return null
|
||||
|
||||
const [videoTracks, pairableAudioTracks] = await Promise.all([
|
||||
media.input.getVideoTracks(),
|
||||
media.videoTrack
|
||||
? media.videoTrack.getPairableAudioTracks()
|
||||
: [],
|
||||
])
|
||||
|
||||
const [levels, audios] = await Promise.all([
|
||||
Promise.all(videoTracks.map(async (track, index) => ({
|
||||
id: track.id,
|
||||
track,
|
||||
index,
|
||||
name: await track.getName(),
|
||||
height: await track.getDisplayHeight(),
|
||||
bitrate: await getTrackBitrate(track),
|
||||
}))),
|
||||
Promise.all(pairableAudioTracks.map(async (track, index) => {
|
||||
const name = await track.getName()
|
||||
const language = await track.getLanguageCode()
|
||||
return {
|
||||
id: track.id,
|
||||
track,
|
||||
index,
|
||||
name,
|
||||
lang: language,
|
||||
language,
|
||||
bitrate: await getTrackBitrate(track),
|
||||
}
|
||||
})),
|
||||
])
|
||||
|
||||
return {
|
||||
levels,
|
||||
audios,
|
||||
currentLevel: media.videoTrack ? levels.find(item => item.id === media.videoTrack.id) ?? null : null,
|
||||
currentAudio: media.audioTrack ? audios.find(item => item.id === media.audioTrack.id) ?? null : null,
|
||||
videoMode: media.videoMode ?? 'auto',
|
||||
audioMode: media.audioMode ?? 'auto',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { InputOptions } from 'mediabunny'
|
||||
import {
|
||||
ALL_FORMATS,
|
||||
BlobSource,
|
||||
HLS_FORMATS,
|
||||
Input,
|
||||
ReadableStreamSource,
|
||||
UrlSource,
|
||||
} from 'mediabunny'
|
||||
|
||||
const M3U8_RE = /\.m3u8(?:$|[?#])/i
|
||||
|
||||
export function isHlsSource(src: unknown): boolean {
|
||||
return typeof src === 'string' && M3U8_RE.test(src)
|
||||
}
|
||||
|
||||
export function normalizeSource(src: unknown): unknown {
|
||||
if (typeof src === 'string')
|
||||
return new UrlSource(src)
|
||||
if (src instanceof Blob)
|
||||
return new BlobSource(src)
|
||||
if (typeof ReadableStream !== 'undefined' && src instanceof ReadableStream)
|
||||
return new ReadableStreamSource(src)
|
||||
return src ?? null
|
||||
}
|
||||
|
||||
export function createInput(src: unknown): Input | null {
|
||||
const source = normalizeSource(src)
|
||||
if (!source)
|
||||
return null
|
||||
|
||||
return new Input({
|
||||
// Preserve SDK Source/SourceRef pass-through and its historical validation errors.
|
||||
source: source as InputOptions['source'],
|
||||
formats: isHlsSource(src) ? HLS_FORMATS : ALL_FORMATS,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { Input } from 'mediabunny'
|
||||
|
||||
export default class LoadSession {
|
||||
private active = true
|
||||
private input: Input | null = null
|
||||
private timers = new Set<ReturnType<typeof setTimeout>>()
|
||||
private deadline: ReturnType<typeof setTimeout> | undefined
|
||||
private controller = typeof AbortController === 'undefined' ? undefined : new AbortController()
|
||||
private resolveCancellation!: () => void
|
||||
private cancellation = new Promise<void>((resolve) => {
|
||||
this.resolveCancellation = resolve
|
||||
})
|
||||
|
||||
get signal(): AbortSignal | undefined {
|
||||
return this.controller?.signal
|
||||
}
|
||||
|
||||
own(input: Input): void {
|
||||
if (this.active)
|
||||
this.input = input
|
||||
else input.dispose()
|
||||
}
|
||||
|
||||
defer(callback: () => void): void {
|
||||
if (!this.active)
|
||||
return
|
||||
const timer = setTimeout(() => {
|
||||
this.timers.delete(timer)
|
||||
if (this.active)
|
||||
callback()
|
||||
}, 0)
|
||||
this.timers.add(timer)
|
||||
}
|
||||
|
||||
async wait(operation: Promise<void>, milliseconds: number): Promise<void> {
|
||||
try {
|
||||
await Promise.race([
|
||||
operation,
|
||||
this.cancellation,
|
||||
milliseconds > 0 ? this.timeout(milliseconds) : new Promise<void>(() => {}),
|
||||
])
|
||||
}
|
||||
finally {
|
||||
if (this.deadline !== undefined) {
|
||||
clearTimeout(this.deadline)
|
||||
this.deadline = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timeout(milliseconds: number): Promise<never> {
|
||||
return new Promise((_, reject) => {
|
||||
this.deadline = setTimeout(() => reject(new Error('Load timeout')), milliseconds)
|
||||
})
|
||||
}
|
||||
|
||||
disposeInput(): void {
|
||||
const input = this.input
|
||||
this.input = null
|
||||
input?.dispose()
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
if (!this.active)
|
||||
return
|
||||
this.active = false
|
||||
for (const timer of this.timers) clearTimeout(timer)
|
||||
this.timers.clear()
|
||||
if (this.deadline !== undefined) {
|
||||
clearTimeout(this.deadline)
|
||||
this.deadline = undefined
|
||||
}
|
||||
this.resolveCancellation()
|
||||
this.controller?.abort()
|
||||
this.disposeInput()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Input, InputAudioTrack, InputVideoTrack } from 'mediabunny'
|
||||
|
||||
export type TrackMode = 'auto' | 'manual'
|
||||
|
||||
export interface PlaybackMedia {
|
||||
input: Input
|
||||
videoTrack: InputVideoTrack | null
|
||||
audioTrack: InputAudioTrack | null
|
||||
duration: number
|
||||
isLive: boolean
|
||||
isHls: boolean
|
||||
videoMode: TrackMode
|
||||
audioMode: TrackMode
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { isHlsSource } from './input'
|
||||
|
||||
interface RangeEvents {
|
||||
emit: (type: string, detail?: Event) => void
|
||||
}
|
||||
|
||||
export async function preflightRange(url: unknown, enabled: boolean, events: RangeEvents, signal?: AbortSignal, isCurrent = (): boolean => true): Promise<boolean> {
|
||||
if (signal?.aborted || !isCurrent())
|
||||
return false
|
||||
if (!enabled || typeof url !== 'string' || isHlsSource(url))
|
||||
return true
|
||||
|
||||
try {
|
||||
const res = await fetch(url, signal ? { method: 'HEAD', signal } : { method: 'HEAD' })
|
||||
if (signal?.aborted || !isCurrent())
|
||||
return false
|
||||
const acceptRanges = res.headers.get('accept-ranges')
|
||||
if (!acceptRanges || acceptRanges === 'none') {
|
||||
events.emit('error', new Event('RangeNotSupported'))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
if (signal?.aborted || !isCurrent())
|
||||
return false
|
||||
console.warn('Preflight check failed:', error)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { Input, InputAudioTrack, InputVideoTrack } from 'mediabunny'
|
||||
import type { PlaybackMedia } from './media'
|
||||
import { isHlsSource } from './input'
|
||||
|
||||
export async function resolveDuration({ input, videoTrack, audioTrack }: Pick<PlaybackMedia, 'input' | 'videoTrack' | 'audioTrack'>): Promise<number> {
|
||||
const tracks = [videoTrack, audioTrack].filter((track): track is InputVideoTrack | InputAudioTrack => track !== null)
|
||||
const referenceTrack = videoTrack || audioTrack
|
||||
const isLive = Boolean(referenceTrack && await referenceTrack.isLive())
|
||||
|
||||
let duration: number | null = Number.NaN
|
||||
if (tracks.length > 0) {
|
||||
duration = await input.getDurationFromMetadata(tracks, { skipLiveWait: true })
|
||||
if (duration === null && !isLive)
|
||||
duration = await input.computeDuration(tracks, { skipLiveWait: true })
|
||||
}
|
||||
return isLive ? Number.POSITIVE_INFINITY : duration ?? Number.NaN
|
||||
}
|
||||
|
||||
export async function selectPlaybackTracks(input: Input, src: unknown): Promise<PlaybackMedia> {
|
||||
const videoTrack = await input.getPrimaryVideoTrack()
|
||||
const audioTrack = videoTrack
|
||||
? await videoTrack.getPrimaryPairableAudioTrack()
|
||||
: await input.getPrimaryAudioTrack()
|
||||
const duration = await resolveDuration({ input, videoTrack, audioTrack })
|
||||
const referenceTrack = videoTrack || audioTrack
|
||||
const isHls = isHlsSource(src)
|
||||
|
||||
return {
|
||||
input,
|
||||
videoTrack,
|
||||
audioTrack,
|
||||
duration,
|
||||
isLive: Number.isFinite(duration) ? false : Boolean(referenceTrack && await referenceTrack.isLive()),
|
||||
isHls,
|
||||
videoMode: isHls ? 'auto' : 'manual',
|
||||
audioMode: isHls ? 'auto' : 'manual',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": false,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable", "ESNext.Disposable"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "types/**/*.d.ts"]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,62 @@
|
||||
# PKG-MB-03 输入、Range 与加载取消
|
||||
|
||||
## 结构与兼容
|
||||
|
||||
原input.js拆为input.ts、media.ts、tracks.ts、hls-state.ts;新增preflight.ts和load-session.ts,
|
||||
共六个strict生产TS模块。原Engine/VideoEngine仅接入新边界,主协调、视频、音频和HLS UI的
|
||||
其余迁移仍分别归04/05/06/07。实际维护地图见包内ARCHITECTURE.md。
|
||||
|
||||
保留String/Blob/ReadableStream、SDK Source/SourceRef转交和SDK对错误输入的验证;
|
||||
HLS匹配、成对音轨、live/metadata/computed时长、语言别名和默认auto/manual模式保持。
|
||||
公开factory、Canvas精确Result、types及main/module/legacy路径均未更改。
|
||||
|
||||
API-05/10的明确缺陷修正:旧加载在切源/销毁/超时后无需等待底层操作即可settle,
|
||||
不能再发晚error/Range事件或持有等待定时器;成功加载清除deadline;销毁后load无副作用。
|
||||
当前活动错误仍code4,超时仍Load timeout/networkState3。合法正常Range行为保留:
|
||||
缺头/none发RangeNotSupported detail但不设置engine.error,网络失败警告后继续,HLS跳过HEAD。
|
||||
|
||||
Input在创建后、异步选轨前就属于LoadSession;engine.input/media只在选轨后暴露,
|
||||
避免旧实现未绑定Input期间无法取消。Input.dispose由唯一session负责,幂等释放;
|
||||
SDK声明明确不允许UrlSource.requestInit.signal,媒体请求交给Input.dispose取消。
|
||||
独立HEAD使用AbortController和代次守卫;无AbortController仍隔离过期结果,不能声称物理中止请求。
|
||||
|
||||
## 类型与依赖决策
|
||||
|
||||
首次严格检查实际复现SDK依赖dom-webcodecs0.1.13与TS5.9.3 DOM的重复声明和BufferSource冲突,
|
||||
以及缺失Disposable。未开启skipLibCheck,也未使用自制any类型掩盖SDK。
|
||||
通过Yarn registry确认并固定@types/dom-webcodecs0.1.19(最低TS5.8),root resolutions覆盖两个
|
||||
传递路径;yarn.lock记录完整性。SDK运行时仍1.56.1、原始四文件哈希未改。
|
||||
Yarn会明确提示替换SDK精确0.1.13,这是有记录的仅声明覆盖,不是无意漂移。
|
||||
包内lib追加ESNext.Disposable以检查SDK声明;target仍ES2020,源代码未引入using/Symbol.dispose。
|
||||
公共声明不引用SDK,不把内部编译器版本要求传给旧消费者。
|
||||
|
||||
## 验证
|
||||
|
||||
32项候选专项:12项来源/轨道/时长/HLS与真实SDK解析,20项加载正常及竞态回归。
|
||||
同一20项加载断言跑冻结旧main:6正常通过、14失败;候选20通过。原24项历史基线继续通过,
|
||||
yarn test:mediabunny共56项已接入test:unit。main与legacy各20项通过;三格式由仓库build生成。
|
||||
|
||||
首轮21项候选源码浏览器检查通过,包含12次实际播放、7次Windows WebKit能力对照、
|
||||
2次待修复的无轨道就绪行为。构建产物的完整矩阵另加真实浏览器Stream取消(无模拟timer),
|
||||
最终报告、结果和源码/产物哈希见[验证证据](../baselines/mb-input-validation.json)。
|
||||
|
||||
测试初稿的空源操作在受控环境触发真实AudioContext缺失,仅在对应隔离Range用例中明确控制
|
||||
无音轨处理;跨VM对象原型改为字段比较;MP4 fixture实际8秒修正了测试草稿2秒假设。
|
||||
这些失败日志与SDK声明冲突日志保留,均未修改冻结基线或扩大媒体成功定义。
|
||||
|
||||
## 剩余范围与回退
|
||||
|
||||
本步没有宣称解决所有生命周期问题。重复/无轨道就绪、事件重入、pending play/seek、
|
||||
已经进入解码器的晚帧/音频回调、长期AV sync、HLS UI并发、完整安装与MPL通知仍有责任任务。
|
||||
旧createTimeout内部方法仍在Engine供后续协调迁移审查,正常load已不通过它创建无人管理的timer。
|
||||
PKG-MB-09/10及全局发布复盘未完成。保持Canvas/Ambilight类型决策待确认;未推送或发布。
|
||||
|
||||
回退以本任务独立提交为单位,同时回退六模块、JS接入、产物、Yarn声明覆盖、测试和文档;
|
||||
不要覆盖03451c02的历史媒体基线。完成状态以任务表、最终验证证据和独立commit为准。
|
||||
|
||||
首轮完整CI的单元/工程通过,基线中第三方来源清单仍指向已拆分input.js。
|
||||
已同步third-party.json的六模块来源路径与验证范围,专项台账及完整CI随后复跑;未降低台账校验。
|
||||
|
||||
最终完整CI1217项通过(1062单元、14工程、141基线),另44重复契约,296生产TS严格检查。
|
||||
最终72项浏览器无失败/重试/跳过;候选30项为12次播放、9次原生Stream取消、7次能力对照、
|
||||
2次已登记trackless就绪缺口。源码、产物、测试哈希与报告匹配,docs/compiled与dist三格式逐字节一致。
|
||||
@@ -25,6 +25,14 @@ play/load/seek转发测试刻意截获engine方法;不得宣传为真实播放
|
||||
|
||||
## 后续顺序
|
||||
|
||||
MB-03实施将input分离为六个strict TS模块,接入加载session、Range取消和SDK Input释放。
|
||||
实际文件地图见包内ARCHITECTURE.md,过程见[输入迁移](changes/2026-09-12-PKG-MB-03-input.md)。
|
||||
`yarn test:mediabunny`现在包括24历史、20候选加载及12输入/轨道检查;20加载断言可通过
|
||||
`ARTPLAYER_MB_BASELINE=1`重跑冻结main,正常控制6通过、缺陷回归14失败。
|
||||
浏览器默认包含两个发布版与候选,并增加mediabunny-load.spec.js的三种真实Stream取消;
|
||||
`ARTPLAYER_MB_ARTIFACT`指定构建的UMD文件,`ARTPLAYER_MB_BROWSER_CANDIDATE=1`可仅跑候选。
|
||||
每轮须读取报告确认具体数量和结果;配置了用例不等于该轮已运行。
|
||||
|
||||
1. MB-02的输入、seek、HLS轨道、无轨道和失败样本见行为基线。42项浏览器检查须区分
|
||||
20次实际播放、12次能力对照、6次历史输入失败及4次无轨道就绪行为;24项生命周期测试含18个历史缺陷。
|
||||
后续修改须复跑对应正常和负例,不把全部负例变为候选豁免;并发选轨、晚帧和音频回调的完整修复分别在04/05/06/07。
|
||||
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 116 / doing 7 / blocked 0 / done 99 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 115 / doing 7 / blocked 0 / done 100 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -331,7 +331,7 @@
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-MB-01 | artplayer-proxy-mediabunny<br>核对 shim 与媒体契约 | BASE-05 | 全部 option、art.mediabunny、canvas/shim 属性及事件表 | 公开暴露成员和实际发布依赖/资源范围明确;核实 third-party.json 所记录依赖 MPL-2.0 元数据与实际 bundle 的许可/通知范围,不从根 MIT 推断 | H | done |
|
||||
| PKG-MB-02 | artplayer-proxy-mediabunny<br>建立真实媒体与事件基线 | PKG-MB-01, ENG-05, ENG-08 | MP4/WebM/HLS/Blob/Stream、seek/错误/轨道切换样本 | ready 事件顺序、音画同步、超时和释放可重跑 | H | done |
|
||||
| PKG-MB-03 | artplayer-proxy-mediabunny<br>整理 input 与加载取消 | PKG-MB-02, CORE-09 | input.js 源检测、输入资源、Range 与取消 | HLS 检测在 input 边界,旧来源类型和超时保持 | H | todo |
|
||||
| PKG-MB-03 | artplayer-proxy-mediabunny<br>整理 input 与加载取消 | PKG-MB-02, CORE-09 | input.js 源检测、输入资源、Range 与取消 | HLS 检测在 input 边界,旧来源类型和超时保持 | H | done |
|
||||
| PKG-MB-04 | artplayer-proxy-mediabunny<br>整理 VideoShim/EventTarget/Engine 协调 | PKG-MB-03, CORE-06, CORE-11 | shim 描述符、事件桥、加载状态和操作隔离 | 公开属性/同步异步返回与事件重入顺序保持 | H | todo |
|
||||
| PKG-MB-05 | artplayer-proxy-mediabunny<br>整理视频解码、seek 和帧释放 | PKG-MB-04 | VideoEngine 调度、队列、晚帧处理和资源释放 | 暂停/seek/切源无旧帧覆盖,默认丢帧策略不变 | H | todo |
|
||||
| PKG-MB-06 | artplayer-proxy-mediabunny<br>整理音频解码、时钟与同步 | PKG-MB-04 | AudioEngine 时钟、缓冲和 AudioContext 归属 | AV sync、倍速、无音轨、静音/音量和暂停恢复通过 | H | todo |
|
||||
@@ -522,4 +522,5 @@
|
||||
- PKG-CANVAS-04: [记录](changes/2026-09-12-PKG-CANVAS-04-types.md) [记录](baselines/canvas-types-validation.json) [记录](canvas-validation.md)
|
||||
- PKG-MB-01: [记录](baselines/mb-release.json) [记录](baselines/mb-surface.json) [记录](baselines/mb-contract.md) [记录](baselines/mb-contract-validation.json) [记录](mb-validation.md) [记录](changes/2026-09-12-PKG-MB-01-contract.md)
|
||||
- PKG-MB-02: [记录](changes/2026-09-12-PKG-MB-02-checkpoint.md) [记录](baselines/mb-media-checkpoint.json) [记录](mb-validation.md) [记录](changes/2026-09-12-PKG-MB-02-baseline.md) [记录](baselines/mb-behavior-validation.json)
|
||||
- PKG-MB-03: [记录](changes/2026-09-12-PKG-MB-03-input.md) [记录](baselines/mb-input-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,16 @@
|
||||
# 进度与证据
|
||||
|
||||
## 最新完成:PKG-MB-03 输入、Range与加载取消
|
||||
|
||||
六个strict TS模块已接入生产,修复切源/销毁/超时后的待加载Input、HEAD及timer资源与陈旧回调。
|
||||
同一20项加载断言在冻结版6通过/14失败,候选source/main/legacy各20通过;来源/轨道12项与
|
||||
旧基线24项继续通过。实际SDK保持1.56.1,WebCodecs补充声明固定0.1.19解决严格DOM声明冲突。
|
||||
三格式构建与导出通过,72项最终浏览器检查通过;候选30项含12播放、9原生Stream取消、7能力
|
||||
失败对照、2待修复无轨道就绪。完整CI1217项(1062单元+14工程+141基线),另44重复契约,296生产TS。
|
||||
见[步骤记录](changes/2026-09-12-PKG-MB-03-input.md)和[验证证据](baselines/mb-input-validation.json)。
|
||||
下一步MB-04主协调/事件/VideoShim;晚帧、音频、HLS UI和完整发布验收仍未完成。
|
||||
222项:100 done、7 doing、115 todo。Canvas/Ambilight类型决定仍待确认;独立本地commit,不推送/发布。
|
||||
|
||||
## 最新完成:PKG-MB-02 媒体、事件与资源基线
|
||||
|
||||
24项历史生命周期检查通过,已接入统一CI;42项真实浏览器检查包含20次播放、12次环境能力
|
||||
|
||||
+4
-3
@@ -755,7 +755,7 @@
|
||||
"PKG-MB-09"
|
||||
],
|
||||
"evidence": [
|
||||
"packages/artplayer-proxy-mediabunny/src/input.js",
|
||||
"packages/artplayer-proxy-mediabunny/src/input.ts",
|
||||
"packages/artplayer-proxy-mediabunny/package.json"
|
||||
],
|
||||
"compatibleResolution": "Preserve native-video-like events, HLS input and topology, codec capability failures and AV sync; actual library/codec environment still requires verification.",
|
||||
@@ -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.",
|
||||
"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.",
|
||||
"closureCriteria": "Historical failing interleavings and candidate cancellation/cleanup pass with real media and event evidence, including long playback and track topology.",
|
||||
"status": "open",
|
||||
"evidence": [
|
||||
@@ -3658,7 +3658,8 @@
|
||||
"refactor/changes/2026-09-12-PKG-MB-02-checkpoint.md",
|
||||
"refactor/baselines/mb-media-checkpoint.json",
|
||||
"refactor/changes/2026-09-12-PKG-MB-02-baseline.md",
|
||||
"refactor/baselines/mb-behavior-validation.json"
|
||||
"refactor/baselines/mb-behavior-validation.json",
|
||||
"refactor/changes/2026-09-12-PKG-MB-03-input.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+5
-2
@@ -3439,11 +3439,14 @@
|
||||
"PKG-MB-02",
|
||||
"CORE-09"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "input.js 源检测、输入资源、Range 与取消",
|
||||
"acceptance": "HLS 检测在 input 边界,旧来源类型和超时保持",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-12-PKG-MB-03-input.md",
|
||||
"baselines/mb-input-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-MB-04",
|
||||
|
||||
@@ -1172,11 +1172,16 @@
|
||||
"PKG-MB-09"
|
||||
],
|
||||
"sourceFiles": [
|
||||
"packages/artplayer-proxy-mediabunny/src/input.js",
|
||||
"packages/artplayer-proxy-mediabunny/src/input.ts",
|
||||
"packages/artplayer-proxy-mediabunny/src/media.ts",
|
||||
"packages/artplayer-proxy-mediabunny/src/tracks.ts",
|
||||
"packages/artplayer-proxy-mediabunny/src/hls-state.ts",
|
||||
"packages/artplayer-proxy-mediabunny/src/preflight.ts",
|
||||
"packages/artplayer-proxy-mediabunny/src/load-session.ts",
|
||||
"packages/artplayer-proxy-mediabunny/package.json"
|
||||
],
|
||||
"contract": "Preserve native-video-like events, HLS input and topology, codec capability failures and AV sync; actual library/codec environment still requires verification.",
|
||||
"validationStatus": "PKG-MB-01 verifies actual 1.0.0/1.2.0 archives and installed 1.56.1 MPL-2.0 provenance, recorded in mb-release.json and mb-contract.md. Historical embedded SDK version is unknown; notices are MB-LICENSE-01/MB-10. Native codec, SDK media and AV sync remain MB-02/09.",
|
||||
"validationStatus": "PKG-MB-01 verifies actual 1.0.0/1.2.0 archives and installed 1.56.1 MPL-2.0 provenance, recorded in mb-release.json and mb-contract.md. Historical embedded SDK version is unknown; notices are MB-LICENSE-01/MB-10. Native codec, SDK media and AV sync remain MB-02/09. MB-02 freezes actual native input behavior; MB-03 candidate has 72 browser checks including 12 candidate plays and nine native pending-stream cancellations, six strict TS modules and a build-only WebCodecs 0.1.19 resolution. Full AV sync/decoder cleanup and release notices remain MB-04/05/06/09/10.",
|
||||
"demoPathsSource": "refactor/baselines/demo-inventory.json"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
Use the pinned Node/Yarn toolchain from `../refactor/toolchain-setup.md`.
|
||||
|
||||
`yarn test:mediabunny` runs historical lifecycle observations, candidate load cancellation,
|
||||
and real SDK input parsing/track contracts; all are in `test:unit`. Candidate failures
|
||||
can be reproduced against the frozen main with `ARTPLAYER_MB_BASELINE=1`. This does not
|
||||
replace native playback or installed consumers. See `refactor/mb-validation.md` and the
|
||||
proxy package's `ARCHITECTURE.md` for browser and artifact selection.
|
||||
|
||||
`yarn test:ads` runs source, verified npm 1.0.6 and the frozen unpublished 2.1.0
|
||||
Ads bundle against controlled clock/host contracts. It is included in `test:unit`.
|
||||
The helper uses the real option validator but does not simulate media decoding or layout.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { mbHistorical } from '../helpers/mediabunny.js'
|
||||
import { mbBrowserImplementations } from '../helpers/mediabunny.js'
|
||||
import { tracklessMp4 } from '../helpers/trackless-mp4.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementations = (await mbHistorical()).filter(item => item.name.startsWith('published'))
|
||||
const implementations = await mbBrowserImplementations()
|
||||
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
|
||||
const hls = new Map(Object.entries(manifest.files).map(([name, expected]) => {
|
||||
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
|
||||
@@ -98,7 +98,13 @@ for (const implementation of implementations) {
|
||||
else if (capabilities.AudioContext === 'undefined' && capabilities.webkitAudioContext === 'undefined') {
|
||||
expect(browserName).toBe('webkit')
|
||||
expect(capabilities).toEqual({ VideoDecoder: 'undefined', AudioDecoder: 'undefined', AudioContext: 'undefined', webkitAudioContext: 'undefined' })
|
||||
expect(loaded.error).toEqual({ code: 4, message: 'undefined is not a constructor (evaluating \'new t\')' })
|
||||
if (implementation.name.startsWith('candidate')) {
|
||||
expect(loaded.error.code).toBe(4)
|
||||
expect(loaded.error.message).toMatch(/^undefined is not a constructor \(evaluating 'new [\w$]+(?:\(\))?'\)$/)
|
||||
}
|
||||
else {
|
||||
expect(loaded.error).toEqual({ code: 4, message: 'undefined is not a constructor (evaluating \'new t\')' })
|
||||
}
|
||||
expect(loaded.readyState).toBeLessThan(4)
|
||||
outcome = 'unsupported-capability-control'
|
||||
}
|
||||
@@ -107,9 +113,9 @@ for (const implementation of implementations) {
|
||||
expect(loaded.readyState).toBe(4)
|
||||
if (input === 'trackless') {
|
||||
expect(loaded.dimensions).toEqual([0, 0])
|
||||
expect(await page.evaluate(() => window.mbEvents.filter(item => item.name === 'video:loadedmetadata').length)).toBe(implementation.name === 'published-1.2.0' ? 2 : 1)
|
||||
expect(await page.evaluate(() => window.mbEvents.filter(item => item.name === 'video:loadedmetadata').length)).toBe(implementation.name === 'published-1.0.0' ? 1 : 2)
|
||||
expect(await page.evaluate(() => window.mbEvents.filter(item => item.name === 'video:canplay').length)).toBe(1)
|
||||
outcome = 'historical-trackless-readiness-control'
|
||||
outcome = implementation.name.startsWith('candidate') ? 'candidate-trackless-readiness-gap' : 'historical-trackless-readiness-control'
|
||||
return
|
||||
}
|
||||
const readiness = await page.evaluate(() => window.mbEvents.map(item => item.name).filter(name => ['video:loadedmetadata', 'video:loadeddata', 'video:canplay', 'video:canplaythrough'].includes(name)))
|
||||
@@ -208,7 +214,7 @@ for (const implementation of implementations) {
|
||||
await expect.poll(() => page.evaluate(() => window.mbContext?.state || 'not-created')).toMatch(/^(?:closed|not-created)$/)
|
||||
expect(await page.evaluate(() => window.mbPendingFrames.size)).toBe(0)
|
||||
const cleanup = await page.evaluate(() => ({ audioContext: window.mbContext?.state || 'not-created', pendingFrames: window.mbPendingFrames.size, streamCancelled: window.mbStreamCancelled || false }))
|
||||
await testInfo.attach('mediabunny-native-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), input, capabilities, outcome, state, cleanup, hlsManifest: input === 'hls' ? manifest : null, tracklessFixture: input === 'trackless' ? { parentSha256: hash(pattern), sha256: hash(trackless) } : null, scope: 'Actual published proxy with native browser media and controlled ArtPlayer host using actual core utilities/config; frame timestamps sampled against the SDK audio clock, not acoustic/long-run AV-sync acceptance. Cleanup checks settled playback, not pending-operation races.' }) })
|
||||
await testInfo.attach('mediabunny-native-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), input, capabilities, outcome, state, cleanup, hlsManifest: input === 'hls' ? manifest : null, tracklessFixture: input === 'trackless' ? { parentSha256: hash(pattern), sha256: hash(trackless) } : null, scope: 'Identified proxy with native browser media and controlled ArtPlayer host using actual core utilities/config; frame timestamps sampled against the SDK audio clock, not acoustic/long-run AV-sync acceptance. Cleanup checks settled playback, not pending-operation races. Candidate trackless readiness remains MB-READY-01, not release acceptance.' }) })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { mbCandidate } from '../helpers/mediabunny.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementation = await mbCandidate()
|
||||
for (const ending of ['destroy', 'source', 'timeout']) {
|
||||
test(`MediaBunny ${implementation.name}: native pending stream cancelled by ${ending}`, async ({ page }, testInfo) => {
|
||||
await page.goto('/test/player.html?core=published')
|
||||
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports;
|
||||
${implementation.code}; window.mbFactory = module.exports.default || module.exports; })();` })
|
||||
await page.evaluate(() => {
|
||||
const listeners = new Map()
|
||||
window.mbCancelled = []
|
||||
window.mbEvents = []
|
||||
window.mbHost = {
|
||||
constructor: window.Artplayer,
|
||||
option: { url: '', autoSize: false },
|
||||
template: { $player: document.querySelector('.player') },
|
||||
on(name, callback) {
|
||||
const list = listeners.get(name) || []
|
||||
list.push(callback)
|
||||
listeners.set(name, list)
|
||||
},
|
||||
emit(name, ...args) {
|
||||
window.mbEvents.push(name)
|
||||
for (const callback of listeners.get(name) || []) callback(...args)
|
||||
},
|
||||
}
|
||||
window.mbCanvas = window.mbFactory({ loadTimeout: 1000 })(window.mbHost)
|
||||
window.mbStream = new ReadableStream({ cancel() {
|
||||
window.mbCancelled.push('old')
|
||||
} })
|
||||
window.mbSettled = false
|
||||
window.mbLoad = window.mbCanvas.engine.load(window.mbStream).then(() => window.mbSettled = true)
|
||||
})
|
||||
try {
|
||||
await expect.poll(() => page.evaluate(() => window.mbStream.locked)).toBe(true)
|
||||
expect(await page.evaluate(() => window.mbSettled)).toBe(false)
|
||||
await page.evaluate((ending) => {
|
||||
if (ending === 'destroy')
|
||||
window.mbHost.emit('destroy')
|
||||
if (ending === 'source') {
|
||||
window.mbReplacement = new ReadableStream({ cancel() {
|
||||
window.mbCancelled.push('new')
|
||||
} })
|
||||
window.mbNextLoad = window.mbCanvas.engine.load(window.mbReplacement)
|
||||
}
|
||||
}, ending)
|
||||
await expect.poll(() => page.evaluate(() => window.mbSettled)).toBe(true)
|
||||
expect(await page.evaluate(() => window.mbCancelled)).toEqual(['old'])
|
||||
const state = await page.evaluate(() => ({ error: window.mbCanvas.error, readyState: window.mbCanvas.readyState, events: window.mbEvents, input: window.mbCanvas.engine.input === null }))
|
||||
expect(state.input).toBe(true)
|
||||
expect(state.readyState).toBe(0)
|
||||
expect(state.events.filter(name => ['video:loadedmetadata', 'video:loadeddata', 'video:canplay'].includes(name))).toEqual([])
|
||||
expect(state.error).toEqual(ending === 'timeout' ? { code: 4, message: 'Load timeout' } : null)
|
||||
if (ending === 'source')
|
||||
await expect.poll(() => page.evaluate(() => window.mbReplacement.locked)).toBe(true)
|
||||
await page.evaluate(() => window.mbHost.emit('destroy'))
|
||||
await expect.poll(() => page.evaluate(() => window.mbCancelled)).toEqual(ending === 'source' ? ['old', 'new'] : ['old'])
|
||||
await testInfo.attach('mediabunny-load-cancellation', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), ending, state, cancellations: await page.evaluate(() => window.mbCancelled), scope: 'Real browser ReadableStream and actual SDK Input disposal before first media bytes; real timeout; controlled ArtPlayer host; does not assert decoder, late frame or audio-node cleanup.' }) })
|
||||
}
|
||||
finally {
|
||||
await page.evaluate(() => window.mbHost.emit('destroy'))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { mbHistorical } from '../helpers/mediabunny.js'
|
||||
import { mbBrowserImplementations } from '../helpers/mediabunny.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementations = (await mbHistorical()).filter(item => item.name.startsWith('published'))
|
||||
const implementations = await mbBrowserImplementations()
|
||||
|
||||
for (const implementation of implementations) {
|
||||
test(`MediaBunny ${implementation.name}: native MP4 playback or exact Windows WebKit capability failure`, async ({ page, browserName }, testInfo) => {
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import vm from 'node:vm'
|
||||
import { verifyMbContract } from '../../refactor/scripts/mb-contract.mjs'
|
||||
import { readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { compilePackage } from './load.js'
|
||||
|
||||
export async function mbCandidate() {
|
||||
if (process.env.ARTPLAYER_MB_BASELINE === '1')
|
||||
return (await mbHistorical()).find(item => item.name === 'frozen-workspace')
|
||||
if (process.env.ARTPLAYER_MB_ARTIFACT)
|
||||
return { name: 'candidate-artifact', code: fs.readFileSync(process.env.ARTPLAYER_MB_ARTIFACT, 'utf8') }
|
||||
return { name: 'candidate-source', code: await compilePackage('artplayer-proxy-mediabunny', 'umd') }
|
||||
}
|
||||
|
||||
export async function mbBrowserImplementations() {
|
||||
const candidate = await mbCandidate()
|
||||
return process.env.ARTPLAYER_MB_BROWSER_CANDIDATE === '1'
|
||||
? [candidate]
|
||||
: [...(await mbHistorical()).filter(item => item.name.startsWith('published')), candidate]
|
||||
}
|
||||
|
||||
export async function mbHistorical() {
|
||||
const { baseline, archives, sources } = await verifyMbContract()
|
||||
@@ -63,6 +81,8 @@ export function mbEnvironment({ code }, option = {}) {
|
||||
Blob,
|
||||
Event,
|
||||
URL,
|
||||
AbortController,
|
||||
queueMicrotask,
|
||||
console,
|
||||
performance,
|
||||
setTimeout: option.setTimeout || setTimeout,
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Use actual SDK input parsing and explicit track selection contracts.
|
||||
import test from 'node:test'
|
||||
import { loadModules } from './helpers/load.js'
|
||||
|
||||
const base = 'packages/artplayer-proxy-mediabunny/src/'
|
||||
const { isHlsSource, normalizeSource, createInput, resolveDuration, selectPlaybackTracks, getHlsState } = await loadModules({
|
||||
isHlsSource: { file: `${base}input`, name: 'isHlsSource' },
|
||||
normalizeSource: { file: `${base}input`, name: 'normalizeSource' },
|
||||
createInput: { file: `${base}input`, name: 'createInput' },
|
||||
resolveDuration: { file: `${base}tracks`, name: 'resolveDuration' },
|
||||
selectPlaybackTracks: { file: `${base}tracks`, name: 'selectPlaybackTracks' },
|
||||
getHlsState: { file: `${base}hls-state`, name: 'getHlsState' },
|
||||
})
|
||||
|
||||
test('MediaBunny input detection preserves URL suffix, query/hash, casing and non-string boundaries', () => {
|
||||
for (const source of ['live.m3u8', 'LIVE.M3U8?token=x', 'live.m3u8#segment']) assert.equal(isHlsSource(source), true)
|
||||
for (const source of ['live.m3u8.mp4', 'file.mp4?name=live.m3u8&x=1', new URL('https://example.com/live.m3u8'), null, new Blob()]) assert.equal(isHlsSource(source), false)
|
||||
})
|
||||
|
||||
test('MediaBunny inputs preserve falsy source handling and SDK validation ownership', () => {
|
||||
for (const source of [null, undefined, false, 0]) assert.equal(createInput(source), null)
|
||||
for (const source of [{ invalid: true }, 42, true]) {
|
||||
assert.equal(normalizeSource(source), source)
|
||||
assert.throws(() => createInput(source), { message: 'options.source must be a Source or SourceRef.' })
|
||||
}
|
||||
const source = normalizeSource('https://example.com/movie.mp4')
|
||||
assert.equal(normalizeSource(source), source)
|
||||
const input = createInput(source)
|
||||
input.dispose()
|
||||
})
|
||||
|
||||
for (const sourceType of ['blob', 'stream']) {
|
||||
test(`MediaBunny ${sourceType} uses one actual SDK input for metadata, duration and track selection`, async () => {
|
||||
const bytes = fs.readFileSync(new URL('./browser/media/pattern.mp4', import.meta.url))
|
||||
const source = sourceType === 'blob'
|
||||
? new Blob([bytes])
|
||||
: new ReadableStream({ start(controller) {
|
||||
for (let offset = 0; offset < bytes.length; offset += 1024) controller.enqueue(bytes.subarray(offset, offset + 1024))
|
||||
controller.close()
|
||||
} })
|
||||
const input = createInput(source)
|
||||
try {
|
||||
const media = await selectPlaybackTracks(input, source)
|
||||
assert.equal(media.input, input)
|
||||
assert.equal(media.videoTrack.displayWidth, 320)
|
||||
assert.equal(media.videoTrack.displayHeight, 180)
|
||||
assert.equal(media.audioTrack, null)
|
||||
assert.equal(media.duration, 8)
|
||||
assert.equal(media.isLive, false)
|
||||
assert.equal(media.videoMode, 'manual')
|
||||
assert.equal(media.audioMode, 'manual')
|
||||
assert.equal(await getHlsState(media), null)
|
||||
}
|
||||
finally { input.dispose() }
|
||||
})
|
||||
}
|
||||
|
||||
for (const scenario of ['metadata', 'computed', 'live', 'unknown', 'trackless']) {
|
||||
test(`MediaBunny duration preserves ${scenario} handling without scanning live or trackless inputs`, async () => {
|
||||
const calls = []
|
||||
const track = scenario === 'trackless' ? null : { isLive: async () => scenario === 'live' }
|
||||
const duration = await resolveDuration({
|
||||
input: {
|
||||
getDurationFromMetadata: async (tracks, option) => {
|
||||
assert.deepEqual(tracks, [track])
|
||||
assert.deepEqual(option, { skipLiveWait: true })
|
||||
calls.push('metadata')
|
||||
return scenario === 'metadata' ? 12 : null
|
||||
},
|
||||
computeDuration: async (tracks, option) => {
|
||||
assert.deepEqual(tracks, [track])
|
||||
assert.deepEqual(option, { skipLiveWait: true })
|
||||
calls.push('compute')
|
||||
return scenario === 'unknown' ? null : 18
|
||||
},
|
||||
},
|
||||
videoTrack: track,
|
||||
audioTrack: null,
|
||||
})
|
||||
assert.equal(duration, scenario === 'metadata' ? 12 : scenario === 'computed' ? 18 : scenario === 'live' ? Infinity : Number.NaN)
|
||||
assert.deepEqual(calls, scenario === 'trackless' ? [] : ['computed', 'unknown'].includes(scenario) ? ['metadata', 'compute'] : ['metadata'])
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny chooses pairable audio from primary video instead of an unrelated primary audio track', async () => {
|
||||
const paired = { id: 2 }
|
||||
const videoTrack = { isLive: async () => false, getPrimaryPairableAudioTrack: async () => paired }
|
||||
const input = {
|
||||
getPrimaryVideoTrack: async () => videoTrack,
|
||||
getPrimaryAudioTrack: async () => { throw new Error('Unrelated audio must not be selected') },
|
||||
getDurationFromMetadata: async (tracks) => {
|
||||
assert.deepEqual(tracks, [videoTrack, paired])
|
||||
return 5
|
||||
},
|
||||
}
|
||||
const media = await selectPlaybackTracks(input, 'live.m3u8')
|
||||
assert.equal(media.audioTrack, paired)
|
||||
assert.equal(media.videoMode, 'auto')
|
||||
assert.equal(media.audioMode, 'auto')
|
||||
assert.equal(media.isHls, true)
|
||||
})
|
||||
|
||||
test('MediaBunny audio-only input selects primary audio and retains live detection', async () => {
|
||||
const audioTrack = { isLive: async () => true }
|
||||
const media = await selectPlaybackTracks({ getPrimaryVideoTrack: async () => null, getPrimaryAudioTrack: async () => audioTrack, getDurationFromMetadata: async () => null }, 'live.m3u8')
|
||||
assert.equal(media.videoTrack, null)
|
||||
assert.equal(media.audioTrack, audioTrack)
|
||||
assert.equal(media.duration, Infinity)
|
||||
assert.equal(media.isLive, true)
|
||||
})
|
||||
|
||||
test('MediaBunny HLS state keeps actual track identity, pairability, bitrate fallback and language aliases', async () => {
|
||||
const audio = { id: 7, getName: async () => null, getLanguageCode: async () => 'fr', getAverageBitrate: async () => null, getBitrate: async () => 128000 }
|
||||
const video = {
|
||||
id: 3,
|
||||
getName: async () => 'Main',
|
||||
getDisplayHeight: async () => 180,
|
||||
getAverageBitrate: async () => 0,
|
||||
getBitrate: async () => {
|
||||
throw new Error('Zero is a valid average bitrate')
|
||||
},
|
||||
getPairableAudioTracks: async () => [audio],
|
||||
}
|
||||
const media = { input: { getVideoTracks: async () => [video] }, isHls: true, videoTrack: video, audioTrack: audio, videoMode: 'manual', audioMode: 'auto' }
|
||||
const state = await getHlsState(media)
|
||||
assert.equal(state.currentLevel, state.levels[0])
|
||||
assert.equal(state.currentAudio, state.audios[0])
|
||||
assert.equal(state.currentLevel.track, video)
|
||||
assert.equal(state.currentLevel.bitrate, 0)
|
||||
assert.equal(state.currentAudio.track, audio)
|
||||
assert.equal(state.currentAudio.name, null)
|
||||
assert.equal(state.currentAudio.lang, 'fr')
|
||||
assert.equal(state.currentAudio.language, 'fr')
|
||||
assert.equal(state.currentAudio.bitrate, 128000)
|
||||
assert.equal(state.videoMode, 'manual')
|
||||
assert.equal(state.audioMode, 'auto')
|
||||
assert.equal(await getHlsState(null), null)
|
||||
})
|
||||
@@ -0,0 +1,272 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Compare candidate cancellation with the frozen implementation.
|
||||
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 environment(option = {}) {
|
||||
const timers = new Map()
|
||||
let sequence = 0
|
||||
const env = mbEnvironment(implementation, {
|
||||
setTimeout(fn, delay) {
|
||||
const id = sequence++
|
||||
timers.set(id, { fn, delay })
|
||||
return id
|
||||
},
|
||||
clearTimeout(id) { timers.delete(id) },
|
||||
})
|
||||
env.eventNames.push('waiting', 'loadstart', 'loadeddata', 'canplay')
|
||||
const canvas = env.factory(option)(env.art)
|
||||
const errors = []
|
||||
const warnings = []
|
||||
env.globals.console = { error: (...args) => errors.push(args), warn: (...args) => warnings.push(args), log() {}, debug() {} }
|
||||
function fire(delay) {
|
||||
for (const [id, timer] of [...timers]) {
|
||||
if (timer.delay === delay) {
|
||||
timers.delete(id)
|
||||
timer.fn()
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ...env, canvas, engine: canvas.engine, timers, fire, errors, warnings }
|
||||
}
|
||||
|
||||
test('MediaBunny candidate destroy settles pending load, drops deferred events and ignores late failure', async () => {
|
||||
const env = environment({ loadTimeout: 100 })
|
||||
const pending = deferred()
|
||||
env.engine.performLoad = () => pending.promise
|
||||
let settled = false
|
||||
const loading = env.engine.load('movie.mp4').then(() => settled = true)
|
||||
env.art.emit('destroy')
|
||||
await flush()
|
||||
assert.equal(settled, true, 'Cancellation must not wait for the underlying operation')
|
||||
assert.equal(env.timers.size, 0)
|
||||
const cursor = env.emitted.length
|
||||
pending.reject(new Error('late failure'))
|
||||
await loading
|
||||
await flush()
|
||||
assert.equal(env.engine.error, null)
|
||||
assert.equal(env.emitted.length, cursor)
|
||||
})
|
||||
|
||||
test('MediaBunny candidate source replacement settles old load without waiting for its result', async () => {
|
||||
const env = environment({ loadTimeout: 100 })
|
||||
const pending = deferred()
|
||||
const sources = []
|
||||
env.engine.performLoad = (src) => {
|
||||
sources.push(src)
|
||||
return src === 'old' ? pending.promise : Promise.resolve()
|
||||
}
|
||||
let settled = false
|
||||
const old = env.engine.load('old').then(() => settled = true)
|
||||
await env.engine.load('new')
|
||||
await flush()
|
||||
assert.equal(settled, true)
|
||||
assert.deepEqual(sources, ['old', 'new'])
|
||||
assert.equal([...env.timers.values()].filter(timer => timer.delay === 100).length, 0)
|
||||
env.fire(0)
|
||||
assert.deepEqual(env.emitted.map(event => event.name), ['video:waiting', 'video:loadstart'])
|
||||
pending.reject(new Error('obsolete'))
|
||||
await old
|
||||
await flush()
|
||||
assert.equal(env.engine.error, null)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny candidate clears successful load deadline while keeping its normal deferred notifications', async () => {
|
||||
const env = environment({ loadTimeout: 100 })
|
||||
env.engine.performLoad = async () => {}
|
||||
await env.engine.load('movie.mp4')
|
||||
assert.deepEqual([...env.timers.values()].map(timer => timer.delay), [0, 0])
|
||||
env.fire(0)
|
||||
assert.deepEqual(env.emitted.map(event => event.name), ['video:waiting', 'video:loadstart'])
|
||||
env.art.emit('destroy')
|
||||
assert.equal(env.timers.size, 0)
|
||||
})
|
||||
|
||||
test('MediaBunny candidate keeps active timeout error and suppresses a late operation failure', async () => {
|
||||
const env = environment({ loadTimeout: 100 })
|
||||
const pending = deferred()
|
||||
env.engine.performLoad = () => pending.promise
|
||||
const loading = env.engine.load('movie.mp4')
|
||||
env.fire(100)
|
||||
await loading
|
||||
assert.deepEqual({ ...env.engine.error }, { code: 4, message: 'Load timeout' })
|
||||
assert.equal(env.engine.networkState, 3)
|
||||
assert.equal(env.timers.size, 0)
|
||||
pending.reject(new Error('late failure'))
|
||||
await flush()
|
||||
assert.deepEqual(env.emitted.map(event => event.name), ['video:error'])
|
||||
assert.equal(env.engine.error.message, 'Load timeout')
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
for (const ending of ['destroy', 'source', 'timeout']) {
|
||||
test(`MediaBunny candidate ${ending} aborts pending HEAD and suppresses stale Range errors`, async () => {
|
||||
const env = environment({ preflightRange: true, loadTimeout: 100 })
|
||||
const response = deferred()
|
||||
let request
|
||||
env.globals.fetch = (url, init) => {
|
||||
request = { url, init }
|
||||
return response.promise
|
||||
}
|
||||
const loading = env.engine.load('movie.mp4')
|
||||
assert.equal(request.init.method, 'HEAD')
|
||||
if (ending === 'destroy') {
|
||||
env.art.emit('destroy')
|
||||
}
|
||||
else if (ending === 'source') {
|
||||
env.engine.audio.handleNoAudioTrack = () => {}
|
||||
await env.engine.load(null)
|
||||
}
|
||||
else {
|
||||
env.fire(100)
|
||||
}
|
||||
await flush()
|
||||
assert.equal(request.init.signal?.aborted, true)
|
||||
const cursor = env.emitted.length
|
||||
response.resolve({ headers: { get: () => 'none' } })
|
||||
await loading
|
||||
await flush()
|
||||
assert.equal(env.emitted.length, cursor)
|
||||
assert.equal(env.warnings.length, 0)
|
||||
if (ending === 'timeout')
|
||||
assert.equal(env.engine.error.message, 'Load timeout')
|
||||
else assert.equal(env.engine.error, null)
|
||||
env.art.emit('destroy')
|
||||
assert.equal(env.timers.size, 0)
|
||||
})
|
||||
}
|
||||
|
||||
for (const ending of ['destroy', 'source', 'timeout']) {
|
||||
test(`MediaBunny candidate ${ending} disposes the real SDK Input while its first stream read is pending`, async () => {
|
||||
const env = environment({ loadTimeout: 100 })
|
||||
let cancellations = 0
|
||||
const stream = new ReadableStream({ cancel() {
|
||||
cancellations++
|
||||
} })
|
||||
let settled = false
|
||||
const loading = env.engine.load(stream).then(() => settled = true)
|
||||
await flush()
|
||||
assert.equal(stream.locked, true, 'The SDK must have started reading the real stream')
|
||||
if (ending === 'destroy')
|
||||
env.art.emit('destroy')
|
||||
else if (ending === 'source')
|
||||
await env.engine.load(null)
|
||||
else env.fire(100)
|
||||
await flush()
|
||||
assert.equal(settled, true)
|
||||
assert.equal(cancellations, 1)
|
||||
assert.equal(env.engine.input, null)
|
||||
assert.equal(env.engine.media, null)
|
||||
await loading
|
||||
env.art.emit('destroy')
|
||||
assert.equal(cancellations, 1)
|
||||
assert.equal(env.timers.size, 0)
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny candidate Range retains supported, missing, network-failure and HLS bypass behavior', async () => {
|
||||
const env = environment({ preflightRange: true })
|
||||
const requests = []
|
||||
env.globals.fetch = async (url, init) => {
|
||||
requests.push({ url, init })
|
||||
if (url === 'network')
|
||||
throw new Error('network failure')
|
||||
return { headers: { get: () => url === 'supported' ? 'bytes' : null } }
|
||||
}
|
||||
assert.equal(await env.engine.video.preflight('supported'), true)
|
||||
assert.equal(await env.engine.video.preflight('missing'), false)
|
||||
assert.equal(env.emitted.at(-1).args[0].detail.type, 'RangeNotSupported')
|
||||
assert.equal(env.engine.error, null)
|
||||
assert.equal(await env.engine.video.preflight('network'), true)
|
||||
assert.equal(env.warnings.length, 1)
|
||||
assert.equal(await env.engine.video.preflight('LIVE.M3U8?x=1'), true)
|
||||
assert.equal(await env.engine.video.preflight(new Blob()), true)
|
||||
assert.deepEqual(requests.map(({ url, init }) => ({ url, init: { ...init } })), ['supported', 'missing', 'network'].map(url => ({ url, init: { method: 'HEAD' } })))
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny candidate has no load side effects after destruction', async () => {
|
||||
const env = environment()
|
||||
let calls = 0
|
||||
env.engine.performLoad = async () => calls++
|
||||
env.art.emit('destroy')
|
||||
await env.engine.load('later.mp4')
|
||||
assert.equal(calls, 0)
|
||||
assert.equal(env.timers.size, 0)
|
||||
})
|
||||
|
||||
for (const failure of [false, true]) {
|
||||
test(`MediaBunny candidate suppresses stale HEAD ${failure ? 'rejection' : 'response'} without AbortController`, async () => {
|
||||
const env = environment({ preflightRange: true })
|
||||
env.globals.AbortController = undefined
|
||||
const response = deferred()
|
||||
env.globals.fetch = () => response.promise
|
||||
let settled = false
|
||||
const loading = env.engine.load('movie.mp4').then(() => settled = true)
|
||||
env.art.emit('destroy')
|
||||
await flush()
|
||||
assert.equal(settled, true)
|
||||
const cursor = env.emitted.length
|
||||
if (failure)
|
||||
response.reject(new Error('obsolete'))
|
||||
else response.resolve({ headers: { get: () => null } })
|
||||
await loading
|
||||
await flush()
|
||||
assert.equal(env.emitted.length, cursor)
|
||||
assert.equal(env.warnings.length, 0)
|
||||
})
|
||||
}
|
||||
|
||||
for (const timeout of [0, -1, Number.NaN, Infinity, '100']) {
|
||||
test(`MediaBunny candidate keeps ${String(timeout)} timeout disabled`, async () => {
|
||||
const env = environment({ loadTimeout: timeout })
|
||||
env.engine.performLoad = async () => {}
|
||||
await env.engine.load('movie.mp4')
|
||||
assert.deepEqual([...env.timers.values()].map(timer => timer.delay), [0, 0])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny candidate releases the real SDK input after decoder setup failure and ignores late metadata', async () => {
|
||||
const env = environment()
|
||||
const pendingAudio = deferred()
|
||||
let metadata
|
||||
let disposals = 0
|
||||
env.engine.video.load = async (media) => {
|
||||
const dispose = media.input.dispose.bind(media.input)
|
||||
media.input.dispose = () => {
|
||||
disposals++
|
||||
dispose()
|
||||
}
|
||||
throw new Error('decoder setup failed')
|
||||
}
|
||||
env.engine.audio.load = (media, onMetadata) => {
|
||||
metadata = onMetadata
|
||||
return pendingAudio.promise
|
||||
}
|
||||
await env.engine.load(new Blob([fs.readFileSync(new URL('./browser/media/pattern.mp4', import.meta.url))]))
|
||||
assert.equal(disposals, 1)
|
||||
assert.equal(env.engine.input, null)
|
||||
assert.equal(env.engine.error.message, 'decoder setup failed')
|
||||
const cursor = env.emitted.length
|
||||
metadata()
|
||||
pendingAudio.resolve()
|
||||
await flush()
|
||||
assert.equal(env.emitted.length, cursor)
|
||||
assert.equal(env.engine.readyState, 0)
|
||||
env.art.emit('destroy')
|
||||
assert.equal(disposals, 1)
|
||||
})
|
||||
@@ -1717,15 +1717,10 @@
|
||||
dependencies:
|
||||
"@types/dom-webcodecs" "*"
|
||||
|
||||
"@types/dom-webcodecs@*":
|
||||
version "0.1.18"
|
||||
resolved "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.18.tgz"
|
||||
integrity sha512-vAvE8C9DGWR+tkb19xyjk1TSUlJ7RUzzp4a9Anu7mwBT+fpyePWK1UxmH14tMO5zHmrnrRIMg5NutnnDztLxgg==
|
||||
|
||||
"@types/dom-webcodecs@0.1.13":
|
||||
version "0.1.13"
|
||||
resolved "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz"
|
||||
integrity sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==
|
||||
"@types/dom-webcodecs@*", "@types/dom-webcodecs@0.1.13", "@types/dom-webcodecs@0.1.19":
|
||||
version "0.1.19"
|
||||
resolved "https://registry.yarnpkg.com/@types/dom-webcodecs/-/dom-webcodecs-0.1.19.tgz#46479c9f47fb0024b762aa38987b38e3c31ac650"
|
||||
integrity sha512-+yFUIDUaASFz0vQpWBErPxun/Lb+F0TdrlTsj50gm7K797YzEx2TmV6fEiwciHyFNghlZYPgQXH5qRcwMHcJFQ==
|
||||
|
||||
"@types/estree@1.0.8", "@types/estree@^1.0.6", "@types/estree@^1.0.8":
|
||||
version "1.0.8"
|
||||
|
||||
Reference in new issue
Block a user