mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
refactor(mediabunny): [PKG-MB-04] isolate typed playback and readiness coordination
This commit is contained in:
1 parent
9af253a5a9
commit
5d0b128483
29 files changed
+2841
-726
No files matched your search
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t=e=>{throw TypeError(e)},i=(e,i,r)=>i.has(e)||t("Cannot "+r),r=(e,t,r)=>(i(e,t,"read from private field"),r?r.call(e):t.get(e));function a(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 s{constructor(){var i,r,a;i=this,a=!1,(r=e).has(i)?t("Cannot add the same private member more than once"):r instanceof WeakSet?r.add(i):r.set(i,a),this.listeners=new Map}addEventListener(t,i){r(this,e)||(this.listeners.has(t)||this.listeners.set(t,[]),this.listeners.get(t)?.push(i))}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(t,i){if(r(this,e))return;const a=Object.assign(new Event(t),{detail:i});this.listeners.get(t)?.forEach(t=>{r(this,e)||t(a)})}destroy(){var t,r,a,s;a=!0,i(t=this,r=e,"write to private field"),s?s.call(t,a):r.set(t,a),this.listeners.clear()}}function n(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const o=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},c=e=>e&&e[e.length-1],l=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)},u=(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)},d=e=>{const t=l(e);return 0===(1&t)?-(t>>1):t+1>>1},h=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),f=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),m=new TextDecoder,p=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),g={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},k=p(g),y={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},b=p(y),w={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},T=p(w),S=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,v={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},P=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class I{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 x=/^[0-9a-fA-F]+$/,C=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),B=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,E=(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},_=(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},R=(e,t,i)=>{const r=_(e,i(t),i);e.splice(r+1,0,t)},F=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},A=(e,t)=>{const i=e.indexOf(t);-1!==i&&e.splice(i,1)},D=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},M=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},N=e=>{throw new Error(`Unexpected value: ${e}`)},O=(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},L=(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)},z=(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}}),U=(e,t,i)=>Math.max(t,Math.min(i,e)),q="und",W=e=>{const t=Math.round(e);return Math.abs(e/t-1)<10*Number.EPSILON?t:e},H=(e,t)=>Math.round(e/t)*t,V=(e,t)=>Math.round(e*t)/t,j=(e,t)=>Math.floor(e/t)*t,K=e=>{let t=0;for(;0!==e;)e&=e-1,t++;return t},X=/^[a-z]{3}$/,$=e=>X.test(e),G=1e6*(1+Number.EPSILON),Q=(e,t)=>{const i={...e,...t};if(e.headers||t.headers){const r=e.headers?Y(e.headers):{},a=t.headers?Y(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},Y=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},J=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(ve._error("Retrying failed fetch. Error:",n),!Number.isFinite(e)||e<0)throw new TypeError("Retry delay must be aLine truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i=e=>{throw TypeError(e)},r=(e,t,r)=>t.has(e)||i("Cannot "+r),a=(e,t,i)=>(r(e,t,"read from private field"),i?i.call(e):t.get(e)),s=(e,t,r)=>t.has(e)?i("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,r),n=(e,t,i,a)=>(r(e,t,"write to private field"),a?a.call(e,i):t.set(e,i),i);function o(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 c{constructor(){s(this,e,!1),this.listeners=new Map}addEventListener(t,i){a(this,e)||(this.listeners.has(t)||this.listeners.set(t,[]),this.listeners.get(t)?.push(i))}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(t,i){if(a(this,e))return;const r=Object.assign(new Event(t),{detail:i});this.listeners.get(t)?.forEach(t=>{a(this,e)||t(r)})}destroy(){n(this,e,!0),this.listeners.clear()}}function l(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const u=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},d=e=>e&&e[e.length-1],h=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)},f=(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)},m=e=>{const t=h(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),g=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),k=new TextDecoder,y=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),b={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},w=y(b),T={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},S=y(T),v={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},P=y(v),I=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,x={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},C=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 E=/^[0-9a-fA-F]+$/,_=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),R=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,F=(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},A=(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},D=(e,t,i)=>{const r=A(e,i(t),i);e.splice(r+1,0,t)},M=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},N=(e,t)=>{const i=e.indexOf(t);-1!==i&&e.splice(i,1)},O=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},L=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},z=e=>{throw new Error(`Unexpected value: ${e}`)},U=(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},q=(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)},W=(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}}),H=(e,t,i)=>Math.max(t,Math.min(i,e)),V="und",j=e=>{const t=Math.round(e);return Math.abs(e/t-1)<10*Number.EPSILON?t:e},K=(e,t)=>Math.round(e/t)*t,X=(e,t)=>Math.round(e*t)/t,$=(e,t)=>Math.floor(e/t)*t,G=e=>{let t=0;for(;0!==e;)e&=e-1,t++;return t},Q=/^[a-z]{3}$/,Y=e=>Q.test(e),J=1e6*(1+Number.EPSILON),Z=(e,t)=>{const i={...e,...t};if(e.headers||t.headers){const r=e.headers?ee(e.headers):{},a=t.headers?ee(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},ee=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},te=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(xe._error("Retrying failed fetch. Error:",n),!Number.isFinite(e)||e<0)throw new TypeError("Retry delayLine truncated
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t=Object.defineProperty,i=Object.defineProperties,r=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,s=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,o=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),l=e=>{throw TypeError(e)},c=Math.pow,u=(e,i,r)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[i]=r,d=(e,t)=>{for(var i in t||(t={}))s.call(t,i)&&u(e,i,t[i]);if(n)for(var i of n(t))a.call(t,i)&&u(e,i,t[i]);return e},h=(e,t)=>i(e,r(t)),f=(e,t,i)=>t.has(e)||l("Cannot "+i),m=(e,t,i)=>(f(e,t,"read from private field"),i?i.call(e):t.get(e)),p=(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())}),g=function(e,t){this[0]=e,this[1]=t},k=(e,t,i)=>{var r,n=(e,t,r,s)=>{try{var a=i[e](t),o=(t=a.value)instanceof g,l=a.done;Promise.resolve(o?t[0]:t).then(i=>o?n("return"===e?e:"next",t[1]?{done:i.done,value:i.value}:i,r,s):r({value:i,done:l})).catch(e=>n("throw",e,r,s))}catch(c){s(c)}},s=(e,t,i,s)=>a[e]=a=>(t=new Promise((t,i,s)=>(s=()=>n(e,a,t,i),r?r.then(s):s())),s=()=>r===i&&(r=0),r=i=t.then(s,s),t),a={};return i=i.apply(e,t),a[o("asyncIterator")]=()=>a,s("next"),s("throw"),s("return"),a},y=e=>{var t,i=e[o("asyncIterator")],r=!1,n={};return null==i?(i=e[o("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 g(new Promise(r=>{var n=i[e](t);n instanceof Object||l("Object expected"),r(n)}),1)}}),n[o("iterator")]=()=>n,t("next"),"throw"in i?t("throw"):n.throw=e=>{throw e},"return"in i&&t("return"),n},b=(e,t,i)=>(t=e[o("asyncIterator")])?t.call(e):(e=e[o("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 T(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 v{constructor(){var t,i,r;t=this,r=!1,(i=e).has(t)?l("Cannot add the same private member more than once"):i instanceof WeakSet?i.add(t):i.set(t,r),this.listeners=new Map}addEventListener(t,i){var r;m(this,e)||(this.listeners.has(t)||this.listeners.set(t,[]),null==(r=this.listeners.get(t))||r.push(i))}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(t,i){var r;if(m(this,e))return;const n=Object.assign(new Event(t),{detail:i});null==(r=this.listeners.get(t))||r.forEach(t=>{m(this,e)||t(n)})}destroy(){var t,i,r,n;r=!0,f(t=this,i=e,"write to private field"),n?n.call(t,r):i.set(t,r),this.listeners.clear()}}function S(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const w=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},P=e=>e&&e[e.length-1],I=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)},x=(e,t)=>{const i=t+1,r=Math.floor(Math.log2(i));e.writeBits(r,0),e.writeBits(1,1),e.writeBits(r,i-c(2,r))},C=e=>{const t=I(e);return 0===(1&t)?-(t>>1):t+1>>1},B=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),E=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),_=new TextDecoder,R=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),F={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},A=R(F),D={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},M=R(D),N={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},O=R(N),L=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,z={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},U=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class q{constructor(){this.currentPromise=Promise.resolve(),this.pending=0}acquire(){return p(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 W=/^[0-9a-fA-F]+$/,H=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),V=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,j=(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},Line truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i=Object.defineProperty,r=Object.defineProperties,n=Object.getOwnPropertyDescriptors,s=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,l=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),c=e=>{throw TypeError(e)},u=Math.pow,d=(e,t,r)=>t in e?i(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,h=(e,t)=>{for(var i in t||(t={}))a.call(t,i)&&d(e,i,t[i]);if(s)for(var i of s(t))o.call(t,i)&&d(e,i,t[i]);return e},f=(e,t)=>r(e,n(t)),m=(e,t,i)=>t.has(e)||c("Cannot "+i),p=(e,t,i)=>(m(e,t,"read from private field"),i?i.call(e):t.get(e)),g=(e,t,i)=>t.has(e)?c("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,i),k=(e,t,i,r)=>(m(e,t,"write to private field"),r?r.call(e,i):t.set(e,i),i),y=(e,t,i)=>new Promise((r,n)=>{var s=e=>{try{o(i.next(e))}catch(t){n(t)}},a=e=>{try{o(i.throw(e))}catch(t){n(t)}},o=e=>e.done?r(e.value):Promise.resolve(e.value).then(s,a);o((i=i.apply(e,t)).next())}),b=function(e,t){this[0]=e,this[1]=t},T=(e,t,i)=>{var r,n=(e,t,r,s)=>{try{var a=i[e](t),o=(t=a.value)instanceof b,l=a.done;Promise.resolve(o?t[0]:t).then(i=>o?n("return"===e?e:"next",t[1]?{done:i.done,value:i.value}:i,r,s):r({value:i,done:l})).catch(e=>n("throw",e,r,s))}catch(c){s(c)}},s=(e,t,i,s)=>a[e]=a=>(t=new Promise((t,i,s)=>(s=()=>n(e,a,t,i),r?r.then(s):s())),s=()=>r===i&&(r=0),r=i=t.then(s,s),t),a={};return i=i.apply(e,t),a[l("asyncIterator")]=()=>a,s("next"),s("throw"),s("return"),a},v=e=>{var t,i=e[l("asyncIterator")],r=!1,n={};return null==i?(i=e[l("iterator")](),t=e=>n[e]=t=>i[e](t)):(i=i.call(e),t=e=>n[e]=t=>{if(r){if(r=!1,"throw"===e)throw t;return t}return r=!0,{done:!1,value:new b(new Promise(r=>{var n=i[e](t);n instanceof Object||c("Object expected"),r(n)}),1)}}),n[l("iterator")]=()=>n,t("next"),"throw"in i?t("throw"):n.throw=e=>{throw e},"return"in i&&t("return"),n},S=(e,t,i)=>(t=e[l("asyncIterator")])?t.call(e):(e=e[l("iterator")](),t={},(i=(i,r)=>(r=e[i])&&(t[i]=t=>new Promise((i,n,s)=>(t=r.call(e,t),s=t.done,Promise.resolve(t.value).then(e=>i({value:e,done:s}),n)))))("next"),i("return"),t);function w(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 P{constructor(){g(this,e,!1),this.listeners=new Map}addEventListener(t,i){var r;p(this,e)||(this.listeners.has(t)||this.listeners.set(t,[]),null==(r=this.listeners.get(t))||r.push(i))}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(t,i){var r;if(p(this,e))return;const n=Object.assign(new Event(t),{detail:i});null==(r=this.listeners.get(t))||r.forEach(t=>{p(this,e)||t(n)})}destroy(){k(this,e,!0),this.listeners.clear()}}function I(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const x=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},C=e=>e&&e[e.length-1],B=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)},E=(e,t)=>{const i=t+1,r=Math.floor(Math.log2(i));e.writeBits(r,0),e.writeBits(1,1),e.writeBits(r,i-u(2,r))},_=e=>{const t=B(e);return 0===(1&t)?-(t>>1):t+1>>1},R=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),F=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),A=new TextDecoder,D=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),M={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},N=D(M),O={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},L=D(O),z={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},U=D(z),q=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,W={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},H=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class V{constructor(){this.currentPromise=Promise.resolve(),this.pending=0}acquire(){return y(this,null,function*(){let e;const t=new Promise(t=>{let i=!1;e=()=>{i||(t(),this.pending--,i=!0)}}),i=this.currentPromise;return this.currentPromise=t,this.pending++,yield i,e})}}const j=/^[0-9a-fA-F]+$/,K=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),X=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,$=(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}retuLine truncated
|
||||
@@ -11,7 +11,7 @@ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot
|
||||
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
||||
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
||||
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
||||
var _closed;
|
||||
var _closed, _playback;
|
||||
const $audio = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>';
|
||||
const $quality = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
|
||||
function uniqBy(array, property) {
|
||||
@@ -24271,6 +24271,62 @@ class AudioEngine {
|
||||
this.audioSink = null;
|
||||
}
|
||||
}
|
||||
async function selectQuality(host, value) {
|
||||
const { input, media, loadSeq } = host;
|
||||
if (!media?.isHls || !input || host.destroyed)
|
||||
return;
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && host.media === media;
|
||||
let selection;
|
||||
try {
|
||||
const tracks = await input.getVideoTracks();
|
||||
if (!current())
|
||||
return;
|
||||
const videoTrack = value === "auto" ? await input.getPrimaryVideoTrack() : tracks.find((track) => track.id === value) ?? media.videoTrack;
|
||||
if (!current() || !videoTrack)
|
||||
return;
|
||||
let audioTrack = media.audioTrack;
|
||||
let audioMode = media.audioMode;
|
||||
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
||||
audioTrack = await videoTrack.getPrimaryPairableAudioTrack();
|
||||
audioMode = "auto";
|
||||
}
|
||||
selection = { videoTrack, audioTrack, videoMode: value === "auto" ? "auto" : "manual", audioMode };
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
throw error;
|
||||
}
|
||||
if (current())
|
||||
await host.replaceTracks(selection);
|
||||
}
|
||||
async function selectAudio(host, value) {
|
||||
const { input, media, loadSeq } = host;
|
||||
if (!media?.isHls || !input || host.destroyed)
|
||||
return;
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && host.media === media;
|
||||
let selection;
|
||||
try {
|
||||
const tracks = await input.getAudioTracks();
|
||||
if (!current())
|
||||
return;
|
||||
const audioTrack = value === "auto" ? media.videoTrack ? await media.videoTrack.getPrimaryPairableAudioTrack() : await input.getPrimaryAudioTrack() : tracks.find((track) => track.id === value) ?? media.audioTrack;
|
||||
if (!current() || !audioTrack)
|
||||
return;
|
||||
let videoTrack = media.videoTrack;
|
||||
let videoMode = media.videoMode;
|
||||
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
||||
videoTrack = await audioTrack.getPrimaryPairableVideoTrack();
|
||||
videoMode = "auto";
|
||||
}
|
||||
selection = { videoTrack, audioTrack, videoMode, audioMode: value === "auto" ? "auto" : "manual" };
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
throw error;
|
||||
}
|
||||
if (current())
|
||||
await host.replaceTracks(selection);
|
||||
}
|
||||
async function getTrackBitrate(track) {
|
||||
return await track.getAverageBitrate() ?? await track.getBitrate() ?? 0;
|
||||
}
|
||||
@@ -24404,6 +24460,30 @@ class LoadSession {
|
||||
this.disposeInput();
|
||||
}
|
||||
}
|
||||
function publish(events, current, names) {
|
||||
for (const name of names) {
|
||||
if (!current())
|
||||
return;
|
||||
events.emit(name);
|
||||
}
|
||||
}
|
||||
function metadataBarrier(ready, current) {
|
||||
let video = false;
|
||||
let audio = false;
|
||||
let published = false;
|
||||
function complete(part) {
|
||||
if (!current() || published)
|
||||
return;
|
||||
if (part === "video")
|
||||
video = true;
|
||||
else audio = true;
|
||||
if (video && audio) {
|
||||
published = true;
|
||||
ready();
|
||||
}
|
||||
}
|
||||
return { video: () => complete("video"), audio: () => complete("audio") };
|
||||
}
|
||||
async function resolveDuration({ input, videoTrack, audioTrack }) {
|
||||
const tracks = [videoTrack, audioTrack].filter((track) => track !== null);
|
||||
const referenceTrack = videoTrack || audioTrack;
|
||||
@@ -24433,6 +24513,212 @@ async function selectPlaybackTracks(input, src) {
|
||||
audioMode: isHls ? "auto" : "manual"
|
||||
};
|
||||
}
|
||||
function completion() {
|
||||
let resolve;
|
||||
let reject;
|
||||
const promise = new Promise((yes, no) => {
|
||||
resolve = yes;
|
||||
reject = no;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
class Playback {
|
||||
constructor(host) {
|
||||
this.host = host;
|
||||
this.wanted = false;
|
||||
this.playing = null;
|
||||
this.seeking = null;
|
||||
this.audioStart = null;
|
||||
}
|
||||
operation() {
|
||||
return { ...completion(), source: this.host.loadSeq, complete: completion() };
|
||||
}
|
||||
current(operation, kind) {
|
||||
return !this.host.destroyed && operation.source === this.host.loadSeq && this[kind] === operation;
|
||||
}
|
||||
cancelPlay() {
|
||||
const previous = this.playing;
|
||||
this.playing = null;
|
||||
previous?.resolve();
|
||||
}
|
||||
invalidate() {
|
||||
this.cancelPlay();
|
||||
this.wanted = false;
|
||||
const previous = this.seeking;
|
||||
this.seeking = null;
|
||||
previous?.complete.resolve();
|
||||
previous?.resolve();
|
||||
this.host.seeking = false;
|
||||
}
|
||||
ended() {
|
||||
this.wanted = false;
|
||||
this.cancelPlay();
|
||||
}
|
||||
stop() {
|
||||
if (this.host.paused)
|
||||
return;
|
||||
this.host.paused = true;
|
||||
this.stopEngines();
|
||||
this.host.events.emit("pause");
|
||||
}
|
||||
stopEngines() {
|
||||
const failures = [];
|
||||
for (const stop of [() => this.host.audio.pause(), () => this.host.video.stop()]) {
|
||||
try {
|
||||
stop();
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0];
|
||||
}
|
||||
pause() {
|
||||
this.wanted = false;
|
||||
this.cancelPlay();
|
||||
this.stop();
|
||||
}
|
||||
play() {
|
||||
if (this.host.destroyed)
|
||||
return Promise.resolve();
|
||||
this.wanted = true;
|
||||
if (this.playing)
|
||||
return this.playing.promise;
|
||||
if (this.host.ended)
|
||||
return this.seek(0, true);
|
||||
if (!this.host.paused && !this.seeking)
|
||||
return Promise.resolve();
|
||||
const operation = this.operation();
|
||||
this.playing = operation;
|
||||
this.runPlay(operation).then(operation.resolve, operation.reject);
|
||||
return operation.promise;
|
||||
}
|
||||
async startAudio() {
|
||||
if (!this.audioStart)
|
||||
this.audioStart = this.host.audio.play();
|
||||
const pending = this.audioStart;
|
||||
try {
|
||||
await pending;
|
||||
} finally {
|
||||
if (this.audioStart === pending)
|
||||
this.audioStart = null;
|
||||
}
|
||||
}
|
||||
async runPlay(operation) {
|
||||
const current = () => this.current(operation, "playing") && this.wanted;
|
||||
try {
|
||||
while (this.seeking) {
|
||||
await this.seeking.complete.promise;
|
||||
if (!current())
|
||||
return;
|
||||
}
|
||||
if (!current())
|
||||
return;
|
||||
this.host.paused = false;
|
||||
await this.startAudio();
|
||||
if (!current()) {
|
||||
if (!this.wanted || this.host.destroyed)
|
||||
this.host.audio.pause();
|
||||
return;
|
||||
}
|
||||
this.host.video.start(this.host.audio);
|
||||
publish(this.host.events, current, ["play", "playing"]);
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
this.wanted = false;
|
||||
this.host.paused = true;
|
||||
try {
|
||||
this.stopEngines();
|
||||
} catch (failure) {
|
||||
console.warn("MediaBunny play cleanup error:", failure);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (this.playing === operation)
|
||||
this.playing = null;
|
||||
}
|
||||
}
|
||||
seek(time, resume) {
|
||||
return this.position(async (current) => {
|
||||
const audio = this.host.audio.seek(time);
|
||||
const video = current() ? this.host.video.seek(time) : Promise.resolve();
|
||||
await Promise.all([audio, video]);
|
||||
}, ["seeked"], false, resume);
|
||||
}
|
||||
replace(media, time) {
|
||||
return this.position(async (current) => {
|
||||
media.duration = await resolveDuration(media);
|
||||
if (!current())
|
||||
return;
|
||||
this.host.media = media;
|
||||
await Promise.all([this.host.video.load(media), this.host.audio.load(media)]);
|
||||
if (!current())
|
||||
return;
|
||||
await Promise.all([this.host.video.seek(time), this.host.audio.seek(time)]);
|
||||
if (current()) {
|
||||
this.host.readyState = 4;
|
||||
this.host.networkState = 1;
|
||||
}
|
||||
}, ["loadedmetadata", "durationchange", "progress", "loadeddata", "canplay", "canplaythrough", "seeked"], true);
|
||||
}
|
||||
position(work, events, pauseFirst, resume) {
|
||||
if (this.host.destroyed)
|
||||
return Promise.resolve();
|
||||
this.wanted = resume ?? (this.seeking ? this.wanted : !this.host.paused);
|
||||
this.cancelPlay();
|
||||
const previous = this.seeking;
|
||||
const operation = this.operation();
|
||||
this.seeking = operation;
|
||||
previous?.complete.resolve();
|
||||
previous?.resolve();
|
||||
this.runPosition(operation, work, events, pauseFirst).then(operation.resolve, operation.reject);
|
||||
return operation.promise;
|
||||
}
|
||||
async runPosition(operation, work, events, pauseFirst) {
|
||||
const current = () => this.current(operation, "seeking");
|
||||
let resume = false;
|
||||
try {
|
||||
if (pauseFirst)
|
||||
this.stop();
|
||||
if (!current())
|
||||
return;
|
||||
this.host.ended = false;
|
||||
this.host.seeking = true;
|
||||
publish(this.host.events, current, ["seeking", "waiting"]);
|
||||
if (!current())
|
||||
return;
|
||||
if (!pauseFirst)
|
||||
this.stop();
|
||||
if (!current())
|
||||
return;
|
||||
await work(current);
|
||||
if (!current())
|
||||
return;
|
||||
this.host.seeking = false;
|
||||
operation.complete.resolve();
|
||||
publish(this.host.events, current, events);
|
||||
if (!current())
|
||||
return;
|
||||
this.seeking = null;
|
||||
resume = this.wanted;
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
this.host.seeking = false;
|
||||
this.wanted = false;
|
||||
this.cancelPlay();
|
||||
this.host.reportError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
operation.complete.resolve();
|
||||
if (this.seeking === operation)
|
||||
this.seeking = null;
|
||||
}
|
||||
if (resume)
|
||||
await this.play();
|
||||
}
|
||||
}
|
||||
async function preflightRange(url, enabled, events, signal, isCurrent = () => true) {
|
||||
if (signal?.aborted || !isCurrent())
|
||||
return false;
|
||||
@@ -24661,6 +24947,7 @@ class VideoEngine {
|
||||
}
|
||||
class MediaBunnyEngine {
|
||||
constructor({ canvas, ctx, events, option = {} }) {
|
||||
__privateAdd(this, _playback);
|
||||
this.events = events;
|
||||
this.option = option;
|
||||
this.audio = new AudioEngine(events);
|
||||
@@ -24685,15 +24972,20 @@ class MediaBunnyEngine {
|
||||
this.media = null;
|
||||
this.loadSession = null;
|
||||
this.destroyed = false;
|
||||
events.addEventListener?.("ended", () => {
|
||||
__privateSet(this, _playback, new Playback(this));
|
||||
events.addEventListener("ended", () => {
|
||||
if (this.destroyed)
|
||||
return;
|
||||
this.ended = true;
|
||||
this.paused = true;
|
||||
__privateGet(this, _playback).ended();
|
||||
});
|
||||
}
|
||||
async load(src) {
|
||||
if (this.destroyed)
|
||||
return;
|
||||
const id = ++this.loadSeq;
|
||||
__privateGet(this, _playback).invalidate();
|
||||
this.loadSession?.cancel();
|
||||
if (id !== this.loadSeq || this.destroyed)
|
||||
return;
|
||||
@@ -24709,75 +25001,62 @@ class MediaBunnyEngine {
|
||||
this.readyState = 0;
|
||||
session.defer(() => this.events.emit("waiting"));
|
||||
session.defer(() => this.events.emit("loadstart"));
|
||||
const loadTimeout = Number.isFinite(this.option.loadTimeout) ? this.option.loadTimeout : 0;
|
||||
const timeout = Number.isFinite(this.option.loadTimeout) ? this.option.loadTimeout ?? 0 : 0;
|
||||
try {
|
||||
await session.wait(this.performLoad(src, id, session), loadTimeout);
|
||||
} catch (err) {
|
||||
await session.wait(this.performLoad(src, id, session), timeout);
|
||||
} catch (error) {
|
||||
if (id !== this.loadSeq)
|
||||
return;
|
||||
const failed = ++this.loadSeq;
|
||||
__privateGet(this, _playback).invalidate();
|
||||
session.cancel();
|
||||
if (failed !== this.loadSeq || this.destroyed)
|
||||
return;
|
||||
this.disposeInput();
|
||||
this.error = { code: 4, message: err.message };
|
||||
this.networkState = 3;
|
||||
this.events.emit("error");
|
||||
this.reportError(error);
|
||||
}
|
||||
}
|
||||
async performLoad(src, id, session = this.loadSession) {
|
||||
if (!await this.video.preflight(src, session?.signal, () => id === this.loadSeq) || id !== this.loadSeq)
|
||||
const current = () => !this.destroyed && id === this.loadSeq;
|
||||
if (!session || !await this.video.preflight(src, session.signal, current) || !current())
|
||||
return;
|
||||
const input = createInput(src);
|
||||
if (!input) {
|
||||
this.video.handleNoVideoTrack();
|
||||
this.audio.handleNoAudioTrack();
|
||||
if (current())
|
||||
this.audio.handleNoAudioTrack();
|
||||
return;
|
||||
}
|
||||
session.own(input);
|
||||
const media = await selectPlaybackTracks(input, src);
|
||||
if (id !== this.loadSeq)
|
||||
if (!current())
|
||||
return;
|
||||
if (!media.videoTrack && !media.audioTrack) {
|
||||
this.video.handleNoVideoTrack();
|
||||
this.video.duration = Number.NaN;
|
||||
this.audio.duration = Number.NaN;
|
||||
throw new Error("Input has no audio or video tracks.");
|
||||
}
|
||||
this.input = input;
|
||||
this.media = media;
|
||||
let videoMetadataLoaded = !media.videoTrack;
|
||||
let audioMetadataLoaded = !media.audioTrack;
|
||||
const checkMetadata = () => {
|
||||
if (videoMetadataLoaded && audioMetadataLoaded) {
|
||||
this.readyState = 1;
|
||||
this.events.emit("loadedmetadata");
|
||||
this.events.emit("durationchange");
|
||||
this.events.emit("progress");
|
||||
}
|
||||
};
|
||||
const metadata = metadataBarrier(() => {
|
||||
this.readyState = 1;
|
||||
publish(this.events, current, ["loadedmetadata", "durationchange", "progress"]);
|
||||
}, current);
|
||||
try {
|
||||
await Promise.all([
|
||||
this.video.load(media, () => {
|
||||
if (id !== this.loadSeq)
|
||||
return;
|
||||
videoMetadataLoaded = true;
|
||||
checkMetadata();
|
||||
}),
|
||||
this.audio.load(media, () => {
|
||||
if (id !== this.loadSeq)
|
||||
return;
|
||||
audioMetadataLoaded = true;
|
||||
checkMetadata();
|
||||
})
|
||||
]);
|
||||
if (id !== this.loadSeq)
|
||||
const video = this.video.load(media, metadata.video);
|
||||
const audio = current() ? this.audio.load(media, metadata.audio) : Promise.resolve();
|
||||
await Promise.all([video, audio]);
|
||||
if (!current())
|
||||
return;
|
||||
this.readyState = 4;
|
||||
this.networkState = 1;
|
||||
this.events.emit("loadeddata");
|
||||
this.events.emit("canplay");
|
||||
this.events.emit("canplaythrough");
|
||||
this.events.emit("progress");
|
||||
} catch (err) {
|
||||
if (id !== this.loadSeq)
|
||||
publish(this.events, current, ["loadeddata", "canplay", "canplaythrough", "progress"]);
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
console.error("MediaBunny load error:", err);
|
||||
throw err;
|
||||
console.error("MediaBunny load error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
createTimeout(ms) {
|
||||
@@ -24790,127 +25069,38 @@ class MediaBunnyEngine {
|
||||
this.input = null;
|
||||
this.media = null;
|
||||
}
|
||||
async replaceTracks({ videoTrack, audioTrack, videoMode, audioMode }) {
|
||||
if (!this.media)
|
||||
async replaceTracks(tracks) {
|
||||
if (!this.media || this.destroyed)
|
||||
return;
|
||||
const nextMedia = {
|
||||
...this.media,
|
||||
videoTrack,
|
||||
audioTrack,
|
||||
videoMode,
|
||||
audioMode
|
||||
};
|
||||
const shouldResume = !this.paused;
|
||||
const currentTime = this.currentTime;
|
||||
this.pause();
|
||||
this.ended = false;
|
||||
this.seeking = true;
|
||||
this.events.emit("seeking");
|
||||
this.events.emit("waiting");
|
||||
nextMedia.duration = await resolveDuration(nextMedia);
|
||||
this.media = nextMedia;
|
||||
await Promise.all([
|
||||
this.video.load(nextMedia),
|
||||
this.audio.load(nextMedia)
|
||||
]);
|
||||
await Promise.all([
|
||||
this.video.seek(currentTime),
|
||||
this.audio.seek(currentTime)
|
||||
]);
|
||||
this.readyState = 4;
|
||||
this.networkState = 1;
|
||||
this.seeking = false;
|
||||
this.events.emit("loadedmetadata");
|
||||
this.events.emit("durationchange");
|
||||
this.events.emit("progress");
|
||||
this.events.emit("loadeddata");
|
||||
this.events.emit("canplay");
|
||||
this.events.emit("canplaythrough");
|
||||
this.events.emit("seeked");
|
||||
if (shouldResume) {
|
||||
await this.play();
|
||||
}
|
||||
return __privateGet(this, _playback).replace({ ...this.media, ...tracks }, this.currentTime);
|
||||
}
|
||||
async selectHlsQuality(value) {
|
||||
if (!this.media?.isHls || !this.input)
|
||||
return;
|
||||
const videoTracks = await this.input.getVideoTracks();
|
||||
const videoTrack = value === "auto" ? await this.input.getPrimaryVideoTrack() : videoTracks.find((track) => track.id === value) ?? this.media.videoTrack;
|
||||
if (!videoTrack)
|
||||
return;
|
||||
let audioTrack = this.media.audioTrack;
|
||||
let audioMode = this.media.audioMode;
|
||||
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
||||
audioTrack = await videoTrack.getPrimaryPairableAudioTrack();
|
||||
audioMode = "auto";
|
||||
}
|
||||
await this.replaceTracks({
|
||||
videoTrack,
|
||||
audioTrack,
|
||||
videoMode: value === "auto" ? "auto" : "manual",
|
||||
audioMode
|
||||
});
|
||||
return selectQuality(this, value);
|
||||
}
|
||||
async selectHlsAudio(value) {
|
||||
if (!this.media?.isHls || !this.input)
|
||||
return;
|
||||
const audioTracks = await this.input.getAudioTracks();
|
||||
const audioTrack = value === "auto" ? this.media.videoTrack ? await this.media.videoTrack.getPrimaryPairableAudioTrack() : await this.input.getPrimaryAudioTrack() : audioTracks.find((track) => track.id === value) ?? this.media.audioTrack;
|
||||
if (!audioTrack)
|
||||
return;
|
||||
let videoTrack = this.media.videoTrack;
|
||||
let videoMode = this.media.videoMode;
|
||||
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
||||
videoTrack = await audioTrack.getPrimaryPairableVideoTrack();
|
||||
videoMode = "auto";
|
||||
}
|
||||
await this.replaceTracks({
|
||||
videoTrack,
|
||||
audioTrack,
|
||||
videoMode,
|
||||
audioMode: value === "auto" ? "auto" : "manual"
|
||||
});
|
||||
return selectAudio(this, value);
|
||||
}
|
||||
getHlsState() {
|
||||
return getHlsState(this.media);
|
||||
async getHlsState() {
|
||||
const media = this.media;
|
||||
if (this.destroyed)
|
||||
return null;
|
||||
try {
|
||||
const state2 = await getHlsState(media);
|
||||
return !this.destroyed && this.media === media ? state2 : null;
|
||||
} catch (error) {
|
||||
if (this.destroyed || this.media !== media)
|
||||
return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async play() {
|
||||
if (!this.paused)
|
||||
return;
|
||||
if (this.ended) {
|
||||
this.ended = false;
|
||||
await this.seek(0);
|
||||
}
|
||||
this.paused = false;
|
||||
await this.audio.play();
|
||||
this.video.start(this.audio);
|
||||
this.events.emit("play");
|
||||
this.events.emit("playing");
|
||||
return __privateGet(this, _playback).play();
|
||||
}
|
||||
pause() {
|
||||
if (this.paused)
|
||||
return;
|
||||
this.paused = true;
|
||||
this.audio.pause();
|
||||
this.video.stop();
|
||||
this.events.emit("pause");
|
||||
__privateGet(this, _playback).pause();
|
||||
}
|
||||
async seek(time) {
|
||||
const shouldResume = !this.paused;
|
||||
this.ended = false;
|
||||
this.seeking = true;
|
||||
this.events.emit("seeking");
|
||||
this.events.emit("waiting");
|
||||
this.pause();
|
||||
await Promise.all([
|
||||
this.audio.seek(time),
|
||||
this.video.seek(time)
|
||||
]);
|
||||
this.seeking = false;
|
||||
this.events.emit("seeked");
|
||||
if (shouldResume && !this.ended) {
|
||||
await this.play();
|
||||
}
|
||||
return __privateGet(this, _playback).seek(time);
|
||||
}
|
||||
setVolume(volume, muted) {
|
||||
this.audio.setVolume(volume, muted);
|
||||
@@ -24919,18 +25109,31 @@ class MediaBunnyEngine {
|
||||
this.audio.setPlaybackRate(rate);
|
||||
this.video.setPlaybackRate(rate);
|
||||
}
|
||||
reportError(error) {
|
||||
if (this.destroyed)
|
||||
return;
|
||||
const message = error && typeof error === "object" ? Reflect.get(error, "message") : void 0;
|
||||
this.error = { code: 4, message: typeof message === "string" ? message : String(error) };
|
||||
this.networkState = 3;
|
||||
this.events.emit("error");
|
||||
}
|
||||
destroy() {
|
||||
if (this.destroyed)
|
||||
return;
|
||||
this.destroyed = true;
|
||||
this.loadSeq++;
|
||||
this.loadSession?.cancel();
|
||||
this.pause();
|
||||
this.disposeInput();
|
||||
this.audio.destroy();
|
||||
this.video.destroy();
|
||||
__privateGet(this, _playback).invalidate();
|
||||
const failures = [];
|
||||
for (const release of [() => this.loadSession?.cancel(), () => this.pause(), () => this.disposeInput(), () => this.audio.destroy(), () => this.video.destroy()]) {
|
||||
try {
|
||||
release();
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0];
|
||||
}
|
||||
// Getters
|
||||
get currentTime() {
|
||||
return this.audio.currentTime;
|
||||
}
|
||||
@@ -24944,6 +25147,7 @@ class MediaBunnyEngine {
|
||||
return this.video.height;
|
||||
}
|
||||
}
|
||||
_playback = new WeakMap();
|
||||
const states = /* @__PURE__ */ new WeakMap();
|
||||
function state(owner) {
|
||||
let value = states.get(owner);
|
||||
@@ -25049,7 +25253,8 @@ class VideoShim {
|
||||
return this.engine.currentTime;
|
||||
}
|
||||
set currentTime(t) {
|
||||
this.engine.seek(Number(t) || 0);
|
||||
void Promise.resolve(this.engine.seek(Number(t) || 0)).catch(() => {
|
||||
});
|
||||
}
|
||||
get duration() {
|
||||
return this.engine.duration;
|
||||
|
||||
+2
-2
@@ -51,7 +51,7 @@
|
||||
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
"test:unit": "node --test test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
|
||||
"test:unit": "node --test test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
|
||||
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
|
||||
"test": "yarn test:node && yarn test:baseline",
|
||||
"test:browser": "playwright test",
|
||||
@@ -76,7 +76,7 @@
|
||||
"test:canvas-types-package": "node refactor/scripts/canvas-package-types.mjs",
|
||||
"test:dpip": "node --test test/dpip.test.js test/dpip-lifecycle.test.js",
|
||||
"test:dpip-types-package": "node refactor/scripts/dpip-package-types.mjs",
|
||||
"test:mediabunny": "node --test test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js"
|
||||
"test:mediabunny": "node --test test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -15,19 +15,22 @@ declarations, the factory signature, canvas forwarding, or package entrypoints.
|
||||
| `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 |
|
||||
| `MediaBunnyEngine.ts` | Load/readiness coordinator, source identity, errors and terminal teardown |
|
||||
| `playback.ts` | Playback intent, cancellable play/seek/replacement operations and shared pending audio resume |
|
||||
| `readiness.ts` | One metadata publication after both participants, guarded event sequences |
|
||||
| `hls-selection.ts` | Source-bound track queries, pairing and active replacement errors |
|
||||
| `VideoEngine.js` / `AudioEngine.js` | Current native decoding, rendering, seeking, Web Audio clock and scheduling |
|
||||
| `VideoShim.ts` / `EventTarget.ts` | Video-like descriptors, synchronous/Promise forwarding and owned event lifecycle |
|
||||
| `shim-values.ts` | Existing volume coercion and synthetic TimeRanges values |
|
||||
| `shim-frames.ts` | Synthetic frame callbacks, per-instance RAF ownership and terminal cleanup |
|
||||
| `engine-types.ts` / `MediaBunnyEngine.d.ts` | Explicit temporary coordinator boundary while its JavaScript implementation is migrated in MB-04 |
|
||||
| `engine-types.ts` / `engine-ports.ts` | Internal shim, coordinator and decoder interfaces |
|
||||
| `AudioEngine.d.ts` / `VideoEngine.d.ts` | Temporary boundaries for JS decoders, removed in MB-06/05 |
|
||||
| `m3u8.js` | Current ArtPlayer control/setting integration |
|
||||
|
||||
The eleven TypeScript modules use strict checking with `skipLibCheck: false` and no
|
||||
ambient Node types. The adjacent coordinator declaration is a transitional boundary,
|
||||
not proof that the JavaScript coordinator has been checked. Replace it with the actual
|
||||
strict implementation before completing MB-04. Remaining JavaScript is explicitly
|
||||
scheduled in PKG-MB-04/05/06/07/08.
|
||||
The sixteen production TypeScript modules use strict checking with `skipLibCheck: false`
|
||||
and no ambient Node types. The coordinator is an actual checked implementation. The two
|
||||
adjacent decoder declarations are temporary boundaries, not checking of their JavaScript
|
||||
implementations. Remaining JavaScript is scheduled in PKG-MB-05/06/07/08.
|
||||
Do not describe the whole package as migrated yet.
|
||||
|
||||
The shim keeps its existing own-property order and direct prototype surface because
|
||||
@@ -40,7 +43,8 @@ EventTarget retains duplicate listeners, first-match removal, live-array `forEac
|
||||
mutation semantics, detail identity and listener exceptions during normal dispatch.
|
||||
Its terminal state stops callbacks later in a dispatch when a listener destroys the
|
||||
shim. Shim teardown closes events in `finally`, even if engine cleanup throws, and
|
||||
subsequent teardown is inert. Engine playback/readiness race fixes remain in progress.
|
||||
subsequent teardown is inert. Engine teardown attempts input, audio and video cleanup
|
||||
even when an earlier release throws, then preserves the first failure.
|
||||
|
||||
## Input and cancellation flow
|
||||
|
||||
@@ -69,6 +73,32 @@ 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.
|
||||
|
||||
## Playback and readiness ownership
|
||||
|
||||
Playback intent is separate from the temporary paused state during seek. Explicit pause
|
||||
cancels pending play and prevents automatic resume; a newer seek settles the old operation
|
||||
without allowing its completion or rejection to overwrite current state. Pending play waits
|
||||
for the seek completion signal, resolved before optional resume to avoid a cycle.
|
||||
A pending AudioContext resume is shared across play-pause-play; only the current operation
|
||||
starts video and emits play/playing. Source change and destroy invalidate both operations.
|
||||
Cancellation settles the public operation while observing the underlying late result;
|
||||
actual late decoder work still belongs to MB-05/06.
|
||||
|
||||
Normal play, pause, seek/resume and HLS replacement event order is retained. Before each
|
||||
event the coordinator rechecks ownership, since a listener may synchronously pause, seek,
|
||||
load another source or destroy. Active play errors reject their Promise and restore paused
|
||||
state; active seek errors additionally publish one media error. The synchronous currentTime
|
||||
setter observes its otherwise unreturnable rejection; direct engine.seek still rejects.
|
||||
Active replacement errors remain visible even after the media object changes. Stale HLS query
|
||||
errors are ignored only when their captured source no longer owns the result.
|
||||
|
||||
Metadata publishes once after both audio/video participants report; repeated or obsolete
|
||||
callbacks cannot ready the current source. A parsed container with neither track fails with
|
||||
code 4 and message `Input has no audio or video tracks.` before decoder/AudioContext setup.
|
||||
This corrects the old false-ready state; normal valid input event order is preserved.
|
||||
Existing-track unsupported-codec handling remains a decoder capability task, not evidence
|
||||
that every capability failure has been fixed.
|
||||
|
||||
## Types and dependencies
|
||||
|
||||
The locked runtime SDK remains MediaBunny 1.56.1. Its pinned WebCodecs declaration
|
||||
@@ -101,7 +131,7 @@ The source of historical truth is `refactor/baselines/mb-release.json` and its G
|
||||
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
|
||||
PKG-MB-04 covers 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
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t=e=>{throw TypeError(e)},i=(e,i,r)=>i.has(e)||t("Cannot "+r),r=(e,t,r)=>(i(e,t,"read from private field"),r?r.call(e):t.get(e));function a(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 s{constructor(){var i,r,a;i=this,a=!1,(r=e).has(i)?t("Cannot add the same private member more than once"):r instanceof WeakSet?r.add(i):r.set(i,a),this.listeners=new Map}addEventListener(t,i){r(this,e)||(this.listeners.has(t)||this.listeners.set(t,[]),this.listeners.get(t)?.push(i))}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(t,i){if(r(this,e))return;const a=Object.assign(new Event(t),{detail:i});this.listeners.get(t)?.forEach(t=>{r(this,e)||t(a)})}destroy(){var t,r,a,s;a=!0,i(t=this,r=e,"write to private field"),s?s.call(t,a):r.set(t,a),this.listeners.clear()}}function n(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const o=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},c=e=>e&&e[e.length-1],l=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)},u=(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)},d=e=>{const t=l(e);return 0===(1&t)?-(t>>1):t+1>>1},h=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),f=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),m=new TextDecoder,p=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),g={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},k=p(g),y={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},b=p(y),w={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},T=p(w),S=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,v={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},P=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class I{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 x=/^[0-9a-fA-F]+$/,C=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),B=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,E=(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},_=(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},R=(e,t,i)=>{const r=_(e,i(t),i);e.splice(r+1,0,t)},F=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},A=(e,t)=>{const i=e.indexOf(t);-1!==i&&e.splice(i,1)},D=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},M=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},N=e=>{throw new Error(`Unexpected value: ${e}`)},O=(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},L=(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)},z=(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}}),U=(e,t,i)=>Math.max(t,Math.min(i,e)),q="und",W=e=>{const t=Math.round(e);return Math.abs(e/t-1)<10*Number.EPSILON?t:e},H=(e,t)=>Math.round(e/t)*t,V=(e,t)=>Math.round(e*t)/t,j=(e,t)=>Math.floor(e/t)*t,K=e=>{let t=0;for(;0!==e;)e&=e-1,t++;return t},X=/^[a-z]{3}$/,$=e=>X.test(e),G=1e6*(1+Number.EPSILON),Q=(e,t)=>{const i={...e,...t};if(e.headers||t.headers){const r=e.headers?Y(e.headers):{},a=t.headers?Y(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},Y=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},J=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(ve._error("Retrying failed fetch. Error:",n),!Number.isFinite(e)||e<0)throw new TypeError("Retry delay must be aLine truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i=e=>{throw TypeError(e)},r=(e,t,r)=>t.has(e)||i("Cannot "+r),a=(e,t,i)=>(r(e,t,"read from private field"),i?i.call(e):t.get(e)),s=(e,t,r)=>t.has(e)?i("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,r),n=(e,t,i,a)=>(r(e,t,"write to private field"),a?a.call(e,i):t.set(e,i),i);function o(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 c{constructor(){s(this,e,!1),this.listeners=new Map}addEventListener(t,i){a(this,e)||(this.listeners.has(t)||this.listeners.set(t,[]),this.listeners.get(t)?.push(i))}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(t,i){if(a(this,e))return;const r=Object.assign(new Event(t),{detail:i});this.listeners.get(t)?.forEach(t=>{a(this,e)||t(r)})}destroy(){n(this,e,!0),this.listeners.clear()}}function l(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const u=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},d=e=>e&&e[e.length-1],h=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)},f=(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)},m=e=>{const t=h(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),g=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),k=new TextDecoder,y=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),b={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},w=y(b),T={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},S=y(T),v={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},P=y(v),I=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,x={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},C=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 E=/^[0-9a-fA-F]+$/,_=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),R=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,F=(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},A=(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},D=(e,t,i)=>{const r=A(e,i(t),i);e.splice(r+1,0,t)},M=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},N=(e,t)=>{const i=e.indexOf(t);-1!==i&&e.splice(i,1)},O=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},L=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},z=e=>{throw new Error(`Unexpected value: ${e}`)},U=(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},q=(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)},W=(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}}),H=(e,t,i)=>Math.max(t,Math.min(i,e)),V="und",j=e=>{const t=Math.round(e);return Math.abs(e/t-1)<10*Number.EPSILON?t:e},K=(e,t)=>Math.round(e/t)*t,X=(e,t)=>Math.round(e*t)/t,$=(e,t)=>Math.floor(e/t)*t,G=e=>{let t=0;for(;0!==e;)e&=e-1,t++;return t},Q=/^[a-z]{3}$/,Y=e=>Q.test(e),J=1e6*(1+Number.EPSILON),Z=(e,t)=>{const i={...e,...t};if(e.headers||t.headers){const r=e.headers?ee(e.headers):{},a=t.headers?ee(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},ee=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},te=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(xe._error("Retrying failed fetch. Error:",n),!Number.isFinite(e)||e<0)throw new TypeError("Retry delayLine truncated
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t=Object.defineProperty,i=Object.defineProperties,r=Object.getOwnPropertyDescriptors,n=Object.getOwnPropertySymbols,s=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,o=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),l=e=>{throw TypeError(e)},c=Math.pow,u=(e,i,r)=>i in e?t(e,i,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[i]=r,d=(e,t)=>{for(var i in t||(t={}))s.call(t,i)&&u(e,i,t[i]);if(n)for(var i of n(t))a.call(t,i)&&u(e,i,t[i]);return e},h=(e,t)=>i(e,r(t)),f=(e,t,i)=>t.has(e)||l("Cannot "+i),m=(e,t,i)=>(f(e,t,"read from private field"),i?i.call(e):t.get(e)),p=(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())}),g=function(e,t){this[0]=e,this[1]=t},k=(e,t,i)=>{var r,n=(e,t,r,s)=>{try{var a=i[e](t),o=(t=a.value)instanceof g,l=a.done;Promise.resolve(o?t[0]:t).then(i=>o?n("return"===e?e:"next",t[1]?{done:i.done,value:i.value}:i,r,s):r({value:i,done:l})).catch(e=>n("throw",e,r,s))}catch(c){s(c)}},s=(e,t,i,s)=>a[e]=a=>(t=new Promise((t,i,s)=>(s=()=>n(e,a,t,i),r?r.then(s):s())),s=()=>r===i&&(r=0),r=i=t.then(s,s),t),a={};return i=i.apply(e,t),a[o("asyncIterator")]=()=>a,s("next"),s("throw"),s("return"),a},y=e=>{var t,i=e[o("asyncIterator")],r=!1,n={};return null==i?(i=e[o("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 g(new Promise(r=>{var n=i[e](t);n instanceof Object||l("Object expected"),r(n)}),1)}}),n[o("iterator")]=()=>n,t("next"),"throw"in i?t("throw"):n.throw=e=>{throw e},"return"in i&&t("return"),n},b=(e,t,i)=>(t=e[o("asyncIterator")])?t.call(e):(e=e[o("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 T(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 v{constructor(){var t,i,r;t=this,r=!1,(i=e).has(t)?l("Cannot add the same private member more than once"):i instanceof WeakSet?i.add(t):i.set(t,r),this.listeners=new Map}addEventListener(t,i){var r;m(this,e)||(this.listeners.has(t)||this.listeners.set(t,[]),null==(r=this.listeners.get(t))||r.push(i))}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(t,i){var r;if(m(this,e))return;const n=Object.assign(new Event(t),{detail:i});null==(r=this.listeners.get(t))||r.forEach(t=>{m(this,e)||t(n)})}destroy(){var t,i,r,n;r=!0,f(t=this,i=e,"write to private field"),n?n.call(t,r):i.set(t,r),this.listeners.clear()}}function S(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const w=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},P=e=>e&&e[e.length-1],I=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)},x=(e,t)=>{const i=t+1,r=Math.floor(Math.log2(i));e.writeBits(r,0),e.writeBits(1,1),e.writeBits(r,i-c(2,r))},C=e=>{const t=I(e);return 0===(1&t)?-(t>>1):t+1>>1},B=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),E=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),_=new TextDecoder,R=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),F={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},A=R(F),D={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},M=R(D),N={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},O=R(N),L=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,z={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},U=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class q{constructor(){this.currentPromise=Promise.resolve(),this.pending=0}acquire(){return p(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 W=/^[0-9a-fA-F]+$/,H=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),V=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,j=(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},Line truncated
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i=Object.defineProperty,r=Object.defineProperties,n=Object.getOwnPropertyDescriptors,s=Object.getOwnPropertySymbols,a=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,l=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),c=e=>{throw TypeError(e)},u=Math.pow,d=(e,t,r)=>t in e?i(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,h=(e,t)=>{for(var i in t||(t={}))a.call(t,i)&&d(e,i,t[i]);if(s)for(var i of s(t))o.call(t,i)&&d(e,i,t[i]);return e},f=(e,t)=>r(e,n(t)),m=(e,t,i)=>t.has(e)||c("Cannot "+i),p=(e,t,i)=>(m(e,t,"read from private field"),i?i.call(e):t.get(e)),g=(e,t,i)=>t.has(e)?c("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,i),k=(e,t,i,r)=>(m(e,t,"write to private field"),r?r.call(e,i):t.set(e,i),i),y=(e,t,i)=>new Promise((r,n)=>{var s=e=>{try{o(i.next(e))}catch(t){n(t)}},a=e=>{try{o(i.throw(e))}catch(t){n(t)}},o=e=>e.done?r(e.value):Promise.resolve(e.value).then(s,a);o((i=i.apply(e,t)).next())}),b=function(e,t){this[0]=e,this[1]=t},T=(e,t,i)=>{var r,n=(e,t,r,s)=>{try{var a=i[e](t),o=(t=a.value)instanceof b,l=a.done;Promise.resolve(o?t[0]:t).then(i=>o?n("return"===e?e:"next",t[1]?{done:i.done,value:i.value}:i,r,s):r({value:i,done:l})).catch(e=>n("throw",e,r,s))}catch(c){s(c)}},s=(e,t,i,s)=>a[e]=a=>(t=new Promise((t,i,s)=>(s=()=>n(e,a,t,i),r?r.then(s):s())),s=()=>r===i&&(r=0),r=i=t.then(s,s),t),a={};return i=i.apply(e,t),a[l("asyncIterator")]=()=>a,s("next"),s("throw"),s("return"),a},v=e=>{var t,i=e[l("asyncIterator")],r=!1,n={};return null==i?(i=e[l("iterator")](),t=e=>n[e]=t=>i[e](t)):(i=i.call(e),t=e=>n[e]=t=>{if(r){if(r=!1,"throw"===e)throw t;return t}return r=!0,{done:!1,value:new b(new Promise(r=>{var n=i[e](t);n instanceof Object||c("Object expected"),r(n)}),1)}}),n[l("iterator")]=()=>n,t("next"),"throw"in i?t("throw"):n.throw=e=>{throw e},"return"in i&&t("return"),n},S=(e,t,i)=>(t=e[l("asyncIterator")])?t.call(e):(e=e[l("iterator")](),t={},(i=(i,r)=>(r=e[i])&&(t[i]=t=>new Promise((i,n,s)=>(t=r.call(e,t),s=t.done,Promise.resolve(t.value).then(e=>i({value:e,done:s}),n)))))("next"),i("return"),t);function w(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 P{constructor(){g(this,e,!1),this.listeners=new Map}addEventListener(t,i){var r;p(this,e)||(this.listeners.has(t)||this.listeners.set(t,[]),null==(r=this.listeners.get(t))||r.push(i))}removeEventListener(e,t){const i=this.listeners.get(e);if(!i)return;const r=i.indexOf(t);r>=0&&i.splice(r,1)}emit(t,i){var r;if(p(this,e))return;const n=Object.assign(new Event(t),{detail:i});null==(r=this.listeners.get(t))||r.forEach(t=>{p(this,e)||t(n)})}destroy(){k(this,e,!0),this.listeners.clear()}}function I(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const x=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},C=e=>e&&e[e.length-1],B=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)},E=(e,t)=>{const i=t+1,r=Math.floor(Math.log2(i));e.writeBits(r,0),e.writeBits(1,1),e.writeBits(r,i-u(2,r))},_=e=>{const t=B(e);return 0===(1&t)?-(t>>1):t+1>>1},R=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),F=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),A=new TextDecoder,D=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),M={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},N=D(M),O={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},L=D(O),z={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},U=D(z),q=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,W={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},H=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class V{constructor(){this.currentPromise=Promise.resolve(),this.pending=0}acquire(){return y(this,null,function*(){let e;const t=new Promise(t=>{let i=!1;e=()=>{i||(t(),this.pending--,i=!0)}}),i=this.currentPromise;return this.currentPromise=t,this.pending++,yield i,e})}}const j=/^[0-9a-fA-F]+$/,K=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),X=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,$=(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}retuLine truncated
|
||||
+365
-160
@@ -11,7 +11,7 @@ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot
|
||||
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
||||
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
||||
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
||||
var _closed;
|
||||
var _closed, _playback;
|
||||
const $audio = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>';
|
||||
const $quality = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
|
||||
function uniqBy(array, property) {
|
||||
@@ -24271,6 +24271,62 @@ class AudioEngine {
|
||||
this.audioSink = null;
|
||||
}
|
||||
}
|
||||
async function selectQuality(host, value) {
|
||||
const { input, media, loadSeq } = host;
|
||||
if (!media?.isHls || !input || host.destroyed)
|
||||
return;
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && host.media === media;
|
||||
let selection;
|
||||
try {
|
||||
const tracks = await input.getVideoTracks();
|
||||
if (!current())
|
||||
return;
|
||||
const videoTrack = value === "auto" ? await input.getPrimaryVideoTrack() : tracks.find((track) => track.id === value) ?? media.videoTrack;
|
||||
if (!current() || !videoTrack)
|
||||
return;
|
||||
let audioTrack = media.audioTrack;
|
||||
let audioMode = media.audioMode;
|
||||
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
||||
audioTrack = await videoTrack.getPrimaryPairableAudioTrack();
|
||||
audioMode = "auto";
|
||||
}
|
||||
selection = { videoTrack, audioTrack, videoMode: value === "auto" ? "auto" : "manual", audioMode };
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
throw error;
|
||||
}
|
||||
if (current())
|
||||
await host.replaceTracks(selection);
|
||||
}
|
||||
async function selectAudio(host, value) {
|
||||
const { input, media, loadSeq } = host;
|
||||
if (!media?.isHls || !input || host.destroyed)
|
||||
return;
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && host.media === media;
|
||||
let selection;
|
||||
try {
|
||||
const tracks = await input.getAudioTracks();
|
||||
if (!current())
|
||||
return;
|
||||
const audioTrack = value === "auto" ? media.videoTrack ? await media.videoTrack.getPrimaryPairableAudioTrack() : await input.getPrimaryAudioTrack() : tracks.find((track) => track.id === value) ?? media.audioTrack;
|
||||
if (!current() || !audioTrack)
|
||||
return;
|
||||
let videoTrack = media.videoTrack;
|
||||
let videoMode = media.videoMode;
|
||||
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
||||
videoTrack = await audioTrack.getPrimaryPairableVideoTrack();
|
||||
videoMode = "auto";
|
||||
}
|
||||
selection = { videoTrack, audioTrack, videoMode, audioMode: value === "auto" ? "auto" : "manual" };
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
throw error;
|
||||
}
|
||||
if (current())
|
||||
await host.replaceTracks(selection);
|
||||
}
|
||||
async function getTrackBitrate(track) {
|
||||
return await track.getAverageBitrate() ?? await track.getBitrate() ?? 0;
|
||||
}
|
||||
@@ -24404,6 +24460,30 @@ class LoadSession {
|
||||
this.disposeInput();
|
||||
}
|
||||
}
|
||||
function publish(events, current, names) {
|
||||
for (const name of names) {
|
||||
if (!current())
|
||||
return;
|
||||
events.emit(name);
|
||||
}
|
||||
}
|
||||
function metadataBarrier(ready, current) {
|
||||
let video = false;
|
||||
let audio = false;
|
||||
let published = false;
|
||||
function complete(part) {
|
||||
if (!current() || published)
|
||||
return;
|
||||
if (part === "video")
|
||||
video = true;
|
||||
else audio = true;
|
||||
if (video && audio) {
|
||||
published = true;
|
||||
ready();
|
||||
}
|
||||
}
|
||||
return { video: () => complete("video"), audio: () => complete("audio") };
|
||||
}
|
||||
async function resolveDuration({ input, videoTrack, audioTrack }) {
|
||||
const tracks = [videoTrack, audioTrack].filter((track) => track !== null);
|
||||
const referenceTrack = videoTrack || audioTrack;
|
||||
@@ -24433,6 +24513,212 @@ async function selectPlaybackTracks(input, src) {
|
||||
audioMode: isHls ? "auto" : "manual"
|
||||
};
|
||||
}
|
||||
function completion() {
|
||||
let resolve;
|
||||
let reject;
|
||||
const promise = new Promise((yes, no) => {
|
||||
resolve = yes;
|
||||
reject = no;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
class Playback {
|
||||
constructor(host) {
|
||||
this.host = host;
|
||||
this.wanted = false;
|
||||
this.playing = null;
|
||||
this.seeking = null;
|
||||
this.audioStart = null;
|
||||
}
|
||||
operation() {
|
||||
return { ...completion(), source: this.host.loadSeq, complete: completion() };
|
||||
}
|
||||
current(operation, kind) {
|
||||
return !this.host.destroyed && operation.source === this.host.loadSeq && this[kind] === operation;
|
||||
}
|
||||
cancelPlay() {
|
||||
const previous = this.playing;
|
||||
this.playing = null;
|
||||
previous?.resolve();
|
||||
}
|
||||
invalidate() {
|
||||
this.cancelPlay();
|
||||
this.wanted = false;
|
||||
const previous = this.seeking;
|
||||
this.seeking = null;
|
||||
previous?.complete.resolve();
|
||||
previous?.resolve();
|
||||
this.host.seeking = false;
|
||||
}
|
||||
ended() {
|
||||
this.wanted = false;
|
||||
this.cancelPlay();
|
||||
}
|
||||
stop() {
|
||||
if (this.host.paused)
|
||||
return;
|
||||
this.host.paused = true;
|
||||
this.stopEngines();
|
||||
this.host.events.emit("pause");
|
||||
}
|
||||
stopEngines() {
|
||||
const failures = [];
|
||||
for (const stop of [() => this.host.audio.pause(), () => this.host.video.stop()]) {
|
||||
try {
|
||||
stop();
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0];
|
||||
}
|
||||
pause() {
|
||||
this.wanted = false;
|
||||
this.cancelPlay();
|
||||
this.stop();
|
||||
}
|
||||
play() {
|
||||
if (this.host.destroyed)
|
||||
return Promise.resolve();
|
||||
this.wanted = true;
|
||||
if (this.playing)
|
||||
return this.playing.promise;
|
||||
if (this.host.ended)
|
||||
return this.seek(0, true);
|
||||
if (!this.host.paused && !this.seeking)
|
||||
return Promise.resolve();
|
||||
const operation = this.operation();
|
||||
this.playing = operation;
|
||||
this.runPlay(operation).then(operation.resolve, operation.reject);
|
||||
return operation.promise;
|
||||
}
|
||||
async startAudio() {
|
||||
if (!this.audioStart)
|
||||
this.audioStart = this.host.audio.play();
|
||||
const pending = this.audioStart;
|
||||
try {
|
||||
await pending;
|
||||
} finally {
|
||||
if (this.audioStart === pending)
|
||||
this.audioStart = null;
|
||||
}
|
||||
}
|
||||
async runPlay(operation) {
|
||||
const current = () => this.current(operation, "playing") && this.wanted;
|
||||
try {
|
||||
while (this.seeking) {
|
||||
await this.seeking.complete.promise;
|
||||
if (!current())
|
||||
return;
|
||||
}
|
||||
if (!current())
|
||||
return;
|
||||
this.host.paused = false;
|
||||
await this.startAudio();
|
||||
if (!current()) {
|
||||
if (!this.wanted || this.host.destroyed)
|
||||
this.host.audio.pause();
|
||||
return;
|
||||
}
|
||||
this.host.video.start(this.host.audio);
|
||||
publish(this.host.events, current, ["play", "playing"]);
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
this.wanted = false;
|
||||
this.host.paused = true;
|
||||
try {
|
||||
this.stopEngines();
|
||||
} catch (failure) {
|
||||
console.warn("MediaBunny play cleanup error:", failure);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (this.playing === operation)
|
||||
this.playing = null;
|
||||
}
|
||||
}
|
||||
seek(time, resume) {
|
||||
return this.position(async (current) => {
|
||||
const audio = this.host.audio.seek(time);
|
||||
const video = current() ? this.host.video.seek(time) : Promise.resolve();
|
||||
await Promise.all([audio, video]);
|
||||
}, ["seeked"], false, resume);
|
||||
}
|
||||
replace(media, time) {
|
||||
return this.position(async (current) => {
|
||||
media.duration = await resolveDuration(media);
|
||||
if (!current())
|
||||
return;
|
||||
this.host.media = media;
|
||||
await Promise.all([this.host.video.load(media), this.host.audio.load(media)]);
|
||||
if (!current())
|
||||
return;
|
||||
await Promise.all([this.host.video.seek(time), this.host.audio.seek(time)]);
|
||||
if (current()) {
|
||||
this.host.readyState = 4;
|
||||
this.host.networkState = 1;
|
||||
}
|
||||
}, ["loadedmetadata", "durationchange", "progress", "loadeddata", "canplay", "canplaythrough", "seeked"], true);
|
||||
}
|
||||
position(work, events, pauseFirst, resume) {
|
||||
if (this.host.destroyed)
|
||||
return Promise.resolve();
|
||||
this.wanted = resume ?? (this.seeking ? this.wanted : !this.host.paused);
|
||||
this.cancelPlay();
|
||||
const previous = this.seeking;
|
||||
const operation = this.operation();
|
||||
this.seeking = operation;
|
||||
previous?.complete.resolve();
|
||||
previous?.resolve();
|
||||
this.runPosition(operation, work, events, pauseFirst).then(operation.resolve, operation.reject);
|
||||
return operation.promise;
|
||||
}
|
||||
async runPosition(operation, work, events, pauseFirst) {
|
||||
const current = () => this.current(operation, "seeking");
|
||||
let resume = false;
|
||||
try {
|
||||
if (pauseFirst)
|
||||
this.stop();
|
||||
if (!current())
|
||||
return;
|
||||
this.host.ended = false;
|
||||
this.host.seeking = true;
|
||||
publish(this.host.events, current, ["seeking", "waiting"]);
|
||||
if (!current())
|
||||
return;
|
||||
if (!pauseFirst)
|
||||
this.stop();
|
||||
if (!current())
|
||||
return;
|
||||
await work(current);
|
||||
if (!current())
|
||||
return;
|
||||
this.host.seeking = false;
|
||||
operation.complete.resolve();
|
||||
publish(this.host.events, current, events);
|
||||
if (!current())
|
||||
return;
|
||||
this.seeking = null;
|
||||
resume = this.wanted;
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
this.host.seeking = false;
|
||||
this.wanted = false;
|
||||
this.cancelPlay();
|
||||
this.host.reportError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
operation.complete.resolve();
|
||||
if (this.seeking === operation)
|
||||
this.seeking = null;
|
||||
}
|
||||
if (resume)
|
||||
await this.play();
|
||||
}
|
||||
}
|
||||
async function preflightRange(url, enabled, events, signal, isCurrent = () => true) {
|
||||
if (signal?.aborted || !isCurrent())
|
||||
return false;
|
||||
@@ -24661,6 +24947,7 @@ class VideoEngine {
|
||||
}
|
||||
class MediaBunnyEngine {
|
||||
constructor({ canvas, ctx, events, option = {} }) {
|
||||
__privateAdd(this, _playback);
|
||||
this.events = events;
|
||||
this.option = option;
|
||||
this.audio = new AudioEngine(events);
|
||||
@@ -24685,15 +24972,20 @@ class MediaBunnyEngine {
|
||||
this.media = null;
|
||||
this.loadSession = null;
|
||||
this.destroyed = false;
|
||||
events.addEventListener?.("ended", () => {
|
||||
__privateSet(this, _playback, new Playback(this));
|
||||
events.addEventListener("ended", () => {
|
||||
if (this.destroyed)
|
||||
return;
|
||||
this.ended = true;
|
||||
this.paused = true;
|
||||
__privateGet(this, _playback).ended();
|
||||
});
|
||||
}
|
||||
async load(src) {
|
||||
if (this.destroyed)
|
||||
return;
|
||||
const id = ++this.loadSeq;
|
||||
__privateGet(this, _playback).invalidate();
|
||||
this.loadSession?.cancel();
|
||||
if (id !== this.loadSeq || this.destroyed)
|
||||
return;
|
||||
@@ -24709,75 +25001,62 @@ class MediaBunnyEngine {
|
||||
this.readyState = 0;
|
||||
session.defer(() => this.events.emit("waiting"));
|
||||
session.defer(() => this.events.emit("loadstart"));
|
||||
const loadTimeout = Number.isFinite(this.option.loadTimeout) ? this.option.loadTimeout : 0;
|
||||
const timeout = Number.isFinite(this.option.loadTimeout) ? this.option.loadTimeout ?? 0 : 0;
|
||||
try {
|
||||
await session.wait(this.performLoad(src, id, session), loadTimeout);
|
||||
} catch (err) {
|
||||
await session.wait(this.performLoad(src, id, session), timeout);
|
||||
} catch (error) {
|
||||
if (id !== this.loadSeq)
|
||||
return;
|
||||
const failed = ++this.loadSeq;
|
||||
__privateGet(this, _playback).invalidate();
|
||||
session.cancel();
|
||||
if (failed !== this.loadSeq || this.destroyed)
|
||||
return;
|
||||
this.disposeInput();
|
||||
this.error = { code: 4, message: err.message };
|
||||
this.networkState = 3;
|
||||
this.events.emit("error");
|
||||
this.reportError(error);
|
||||
}
|
||||
}
|
||||
async performLoad(src, id, session = this.loadSession) {
|
||||
if (!await this.video.preflight(src, session?.signal, () => id === this.loadSeq) || id !== this.loadSeq)
|
||||
const current = () => !this.destroyed && id === this.loadSeq;
|
||||
if (!session || !await this.video.preflight(src, session.signal, current) || !current())
|
||||
return;
|
||||
const input = createInput(src);
|
||||
if (!input) {
|
||||
this.video.handleNoVideoTrack();
|
||||
this.audio.handleNoAudioTrack();
|
||||
if (current())
|
||||
this.audio.handleNoAudioTrack();
|
||||
return;
|
||||
}
|
||||
session.own(input);
|
||||
const media = await selectPlaybackTracks(input, src);
|
||||
if (id !== this.loadSeq)
|
||||
if (!current())
|
||||
return;
|
||||
if (!media.videoTrack && !media.audioTrack) {
|
||||
this.video.handleNoVideoTrack();
|
||||
this.video.duration = Number.NaN;
|
||||
this.audio.duration = Number.NaN;
|
||||
throw new Error("Input has no audio or video tracks.");
|
||||
}
|
||||
this.input = input;
|
||||
this.media = media;
|
||||
let videoMetadataLoaded = !media.videoTrack;
|
||||
let audioMetadataLoaded = !media.audioTrack;
|
||||
const checkMetadata = () => {
|
||||
if (videoMetadataLoaded && audioMetadataLoaded) {
|
||||
this.readyState = 1;
|
||||
this.events.emit("loadedmetadata");
|
||||
this.events.emit("durationchange");
|
||||
this.events.emit("progress");
|
||||
}
|
||||
};
|
||||
const metadata = metadataBarrier(() => {
|
||||
this.readyState = 1;
|
||||
publish(this.events, current, ["loadedmetadata", "durationchange", "progress"]);
|
||||
}, current);
|
||||
try {
|
||||
await Promise.all([
|
||||
this.video.load(media, () => {
|
||||
if (id !== this.loadSeq)
|
||||
return;
|
||||
videoMetadataLoaded = true;
|
||||
checkMetadata();
|
||||
}),
|
||||
this.audio.load(media, () => {
|
||||
if (id !== this.loadSeq)
|
||||
return;
|
||||
audioMetadataLoaded = true;
|
||||
checkMetadata();
|
||||
})
|
||||
]);
|
||||
if (id !== this.loadSeq)
|
||||
const video = this.video.load(media, metadata.video);
|
||||
const audio = current() ? this.audio.load(media, metadata.audio) : Promise.resolve();
|
||||
await Promise.all([video, audio]);
|
||||
if (!current())
|
||||
return;
|
||||
this.readyState = 4;
|
||||
this.networkState = 1;
|
||||
this.events.emit("loadeddata");
|
||||
this.events.emit("canplay");
|
||||
this.events.emit("canplaythrough");
|
||||
this.events.emit("progress");
|
||||
} catch (err) {
|
||||
if (id !== this.loadSeq)
|
||||
publish(this.events, current, ["loadeddata", "canplay", "canplaythrough", "progress"]);
|
||||
} catch (error) {
|
||||
if (!current())
|
||||
return;
|
||||
console.error("MediaBunny load error:", err);
|
||||
throw err;
|
||||
console.error("MediaBunny load error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
createTimeout(ms) {
|
||||
@@ -24790,127 +25069,38 @@ class MediaBunnyEngine {
|
||||
this.input = null;
|
||||
this.media = null;
|
||||
}
|
||||
async replaceTracks({ videoTrack, audioTrack, videoMode, audioMode }) {
|
||||
if (!this.media)
|
||||
async replaceTracks(tracks) {
|
||||
if (!this.media || this.destroyed)
|
||||
return;
|
||||
const nextMedia = {
|
||||
...this.media,
|
||||
videoTrack,
|
||||
audioTrack,
|
||||
videoMode,
|
||||
audioMode
|
||||
};
|
||||
const shouldResume = !this.paused;
|
||||
const currentTime = this.currentTime;
|
||||
this.pause();
|
||||
this.ended = false;
|
||||
this.seeking = true;
|
||||
this.events.emit("seeking");
|
||||
this.events.emit("waiting");
|
||||
nextMedia.duration = await resolveDuration(nextMedia);
|
||||
this.media = nextMedia;
|
||||
await Promise.all([
|
||||
this.video.load(nextMedia),
|
||||
this.audio.load(nextMedia)
|
||||
]);
|
||||
await Promise.all([
|
||||
this.video.seek(currentTime),
|
||||
this.audio.seek(currentTime)
|
||||
]);
|
||||
this.readyState = 4;
|
||||
this.networkState = 1;
|
||||
this.seeking = false;
|
||||
this.events.emit("loadedmetadata");
|
||||
this.events.emit("durationchange");
|
||||
this.events.emit("progress");
|
||||
this.events.emit("loadeddata");
|
||||
this.events.emit("canplay");
|
||||
this.events.emit("canplaythrough");
|
||||
this.events.emit("seeked");
|
||||
if (shouldResume) {
|
||||
await this.play();
|
||||
}
|
||||
return __privateGet(this, _playback).replace({ ...this.media, ...tracks }, this.currentTime);
|
||||
}
|
||||
async selectHlsQuality(value) {
|
||||
if (!this.media?.isHls || !this.input)
|
||||
return;
|
||||
const videoTracks = await this.input.getVideoTracks();
|
||||
const videoTrack = value === "auto" ? await this.input.getPrimaryVideoTrack() : videoTracks.find((track) => track.id === value) ?? this.media.videoTrack;
|
||||
if (!videoTrack)
|
||||
return;
|
||||
let audioTrack = this.media.audioTrack;
|
||||
let audioMode = this.media.audioMode;
|
||||
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
||||
audioTrack = await videoTrack.getPrimaryPairableAudioTrack();
|
||||
audioMode = "auto";
|
||||
}
|
||||
await this.replaceTracks({
|
||||
videoTrack,
|
||||
audioTrack,
|
||||
videoMode: value === "auto" ? "auto" : "manual",
|
||||
audioMode
|
||||
});
|
||||
return selectQuality(this, value);
|
||||
}
|
||||
async selectHlsAudio(value) {
|
||||
if (!this.media?.isHls || !this.input)
|
||||
return;
|
||||
const audioTracks = await this.input.getAudioTracks();
|
||||
const audioTrack = value === "auto" ? this.media.videoTrack ? await this.media.videoTrack.getPrimaryPairableAudioTrack() : await this.input.getPrimaryAudioTrack() : audioTracks.find((track) => track.id === value) ?? this.media.audioTrack;
|
||||
if (!audioTrack)
|
||||
return;
|
||||
let videoTrack = this.media.videoTrack;
|
||||
let videoMode = this.media.videoMode;
|
||||
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
||||
videoTrack = await audioTrack.getPrimaryPairableVideoTrack();
|
||||
videoMode = "auto";
|
||||
}
|
||||
await this.replaceTracks({
|
||||
videoTrack,
|
||||
audioTrack,
|
||||
videoMode,
|
||||
audioMode: value === "auto" ? "auto" : "manual"
|
||||
});
|
||||
return selectAudio(this, value);
|
||||
}
|
||||
getHlsState() {
|
||||
return getHlsState(this.media);
|
||||
async getHlsState() {
|
||||
const media = this.media;
|
||||
if (this.destroyed)
|
||||
return null;
|
||||
try {
|
||||
const state2 = await getHlsState(media);
|
||||
return !this.destroyed && this.media === media ? state2 : null;
|
||||
} catch (error) {
|
||||
if (this.destroyed || this.media !== media)
|
||||
return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async play() {
|
||||
if (!this.paused)
|
||||
return;
|
||||
if (this.ended) {
|
||||
this.ended = false;
|
||||
await this.seek(0);
|
||||
}
|
||||
this.paused = false;
|
||||
await this.audio.play();
|
||||
this.video.start(this.audio);
|
||||
this.events.emit("play");
|
||||
this.events.emit("playing");
|
||||
return __privateGet(this, _playback).play();
|
||||
}
|
||||
pause() {
|
||||
if (this.paused)
|
||||
return;
|
||||
this.paused = true;
|
||||
this.audio.pause();
|
||||
this.video.stop();
|
||||
this.events.emit("pause");
|
||||
__privateGet(this, _playback).pause();
|
||||
}
|
||||
async seek(time) {
|
||||
const shouldResume = !this.paused;
|
||||
this.ended = false;
|
||||
this.seeking = true;
|
||||
this.events.emit("seeking");
|
||||
this.events.emit("waiting");
|
||||
this.pause();
|
||||
await Promise.all([
|
||||
this.audio.seek(time),
|
||||
this.video.seek(time)
|
||||
]);
|
||||
this.seeking = false;
|
||||
this.events.emit("seeked");
|
||||
if (shouldResume && !this.ended) {
|
||||
await this.play();
|
||||
}
|
||||
return __privateGet(this, _playback).seek(time);
|
||||
}
|
||||
setVolume(volume, muted) {
|
||||
this.audio.setVolume(volume, muted);
|
||||
@@ -24919,18 +25109,31 @@ class MediaBunnyEngine {
|
||||
this.audio.setPlaybackRate(rate);
|
||||
this.video.setPlaybackRate(rate);
|
||||
}
|
||||
reportError(error) {
|
||||
if (this.destroyed)
|
||||
return;
|
||||
const message = error && typeof error === "object" ? Reflect.get(error, "message") : void 0;
|
||||
this.error = { code: 4, message: typeof message === "string" ? message : String(error) };
|
||||
this.networkState = 3;
|
||||
this.events.emit("error");
|
||||
}
|
||||
destroy() {
|
||||
if (this.destroyed)
|
||||
return;
|
||||
this.destroyed = true;
|
||||
this.loadSeq++;
|
||||
this.loadSession?.cancel();
|
||||
this.pause();
|
||||
this.disposeInput();
|
||||
this.audio.destroy();
|
||||
this.video.destroy();
|
||||
__privateGet(this, _playback).invalidate();
|
||||
const failures = [];
|
||||
for (const release of [() => this.loadSession?.cancel(), () => this.pause(), () => this.disposeInput(), () => this.audio.destroy(), () => this.video.destroy()]) {
|
||||
try {
|
||||
release();
|
||||
} catch (error) {
|
||||
failures.push(error);
|
||||
}
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0];
|
||||
}
|
||||
// Getters
|
||||
get currentTime() {
|
||||
return this.audio.currentTime;
|
||||
}
|
||||
@@ -24944,6 +25147,7 @@ class MediaBunnyEngine {
|
||||
return this.video.height;
|
||||
}
|
||||
}
|
||||
_playback = new WeakMap();
|
||||
const states = /* @__PURE__ */ new WeakMap();
|
||||
function state(owner) {
|
||||
let value = states.get(owner);
|
||||
@@ -25049,7 +25253,8 @@ class VideoShim {
|
||||
return this.engine.currentTime;
|
||||
}
|
||||
set currentTime(t) {
|
||||
this.engine.seek(Number(t) || 0);
|
||||
void Promise.resolve(this.engine.seek(Number(t) || 0)).catch(() => {
|
||||
});
|
||||
}
|
||||
get duration() {
|
||||
return this.engine.duration;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { AudioPort } from './engine-ports'
|
||||
import type EventTarget from './EventTarget'
|
||||
|
||||
// Exact coordinator-facing port; implementation migration and decoder ownership are MB-06.
|
||||
declare const AudioEngine: new (events: EventTarget) => AudioPort
|
||||
export default AudioEngine
|
||||
@@ -1,4 +0,0 @@
|
||||
import type { EngineOptions, EnginePort } from './engine-types'
|
||||
|
||||
declare const MediaBunnyEngine: new (options: EngineOptions) => EnginePort
|
||||
export default MediaBunnyEngine
|
||||
@@ -1,372 +0,0 @@
|
||||
/**
|
||||
* Main MediaBunny Engine
|
||||
* Coordinates audio and video playback
|
||||
*/
|
||||
import AudioEngine from './AudioEngine.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 {
|
||||
constructor({ canvas, ctx, events, option = {} }) {
|
||||
this.events = events
|
||||
this.option = option
|
||||
|
||||
// Create audio and video engines
|
||||
this.audio = new AudioEngine(events)
|
||||
this.video = new VideoEngine({
|
||||
canvas,
|
||||
ctx,
|
||||
events,
|
||||
timeupdateInterval: option.timeupdateInterval ?? 250,
|
||||
avSyncTolerance: option.avSyncTolerance ?? 0.12,
|
||||
dropLateFrames: option.dropLateFrames ?? false,
|
||||
poster: option.poster ?? '',
|
||||
preflightRange: option.preflightRange ?? false,
|
||||
})
|
||||
|
||||
// Playback state
|
||||
this.paused = true
|
||||
this.ended = false
|
||||
this.readyState = 0
|
||||
this.networkState = 0
|
||||
this.error = null
|
||||
this.seeking = false
|
||||
this.loadSeq = 0
|
||||
this.input = null
|
||||
this.media = null
|
||||
this.loadSession = null
|
||||
this.destroyed = false
|
||||
|
||||
// Listen to ended event
|
||||
events.addEventListener?.('ended', () => {
|
||||
this.ended = true
|
||||
this.paused = true
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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 session.wait(this.performLoad(src, id, session), loadTimeout)
|
||||
}
|
||||
catch (err) {
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
|
||||
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, session = this.loadSession) {
|
||||
if (!(await this.video.preflight(src, session?.signal, () => id === this.loadSeq)) || id !== this.loadSeq)
|
||||
return
|
||||
|
||||
const input = createInput(src)
|
||||
if (!input) {
|
||||
this.video.handleNoVideoTrack()
|
||||
this.audio.handleNoAudioTrack()
|
||||
return
|
||||
}
|
||||
session.own(input)
|
||||
const media = await selectPlaybackTracks(input, src)
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
this.input = input
|
||||
this.media = media
|
||||
|
||||
let videoMetadataLoaded = !media.videoTrack
|
||||
let audioMetadataLoaded = !media.audioTrack
|
||||
|
||||
const checkMetadata = () => {
|
||||
if (videoMetadataLoaded && audioMetadataLoaded) {
|
||||
this.readyState = 1 // HAVE_METADATA
|
||||
this.events.emit('loadedmetadata')
|
||||
this.events.emit('durationchange')
|
||||
this.events.emit('progress')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
this.video.load(media, () => {
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
videoMetadataLoaded = true
|
||||
checkMetadata()
|
||||
}),
|
||||
this.audio.load(media, () => {
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
audioMetadataLoaded = true
|
||||
checkMetadata()
|
||||
}),
|
||||
])
|
||||
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
|
||||
this.readyState = 4 // HAVE_ENOUGH_DATA
|
||||
this.networkState = 1 // NETWORK_IDLE
|
||||
this.events.emit('loadeddata')
|
||||
this.events.emit('canplay')
|
||||
this.events.emit('canplaythrough')
|
||||
this.events.emit('progress')
|
||||
}
|
||||
catch (err) {
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
|
||||
console.error('MediaBunny load error:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
createTimeout(ms) {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Load timeout')), ms)
|
||||
})
|
||||
}
|
||||
|
||||
disposeInput() {
|
||||
this.loadSession?.disposeInput()
|
||||
this.input = null
|
||||
this.media = null
|
||||
}
|
||||
|
||||
async replaceTracks({ videoTrack, audioTrack, videoMode, audioMode }) {
|
||||
if (!this.media)
|
||||
return
|
||||
|
||||
const nextMedia = {
|
||||
...this.media,
|
||||
videoTrack,
|
||||
audioTrack,
|
||||
videoMode,
|
||||
audioMode,
|
||||
}
|
||||
|
||||
const shouldResume = !this.paused
|
||||
const currentTime = this.currentTime
|
||||
|
||||
this.pause()
|
||||
this.ended = false
|
||||
this.seeking = true
|
||||
this.events.emit('seeking')
|
||||
this.events.emit('waiting')
|
||||
|
||||
nextMedia.duration = await resolveDuration(nextMedia)
|
||||
this.media = nextMedia
|
||||
|
||||
await Promise.all([
|
||||
this.video.load(nextMedia),
|
||||
this.audio.load(nextMedia),
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
this.video.seek(currentTime),
|
||||
this.audio.seek(currentTime),
|
||||
])
|
||||
|
||||
this.readyState = 4
|
||||
this.networkState = 1
|
||||
this.seeking = false
|
||||
this.events.emit('loadedmetadata')
|
||||
this.events.emit('durationchange')
|
||||
this.events.emit('progress')
|
||||
this.events.emit('loadeddata')
|
||||
this.events.emit('canplay')
|
||||
this.events.emit('canplaythrough')
|
||||
this.events.emit('seeked')
|
||||
|
||||
if (shouldResume) {
|
||||
await this.play()
|
||||
}
|
||||
}
|
||||
|
||||
async selectHlsQuality(value) {
|
||||
if (!this.media?.isHls || !this.input)
|
||||
return
|
||||
|
||||
const videoTracks = await this.input.getVideoTracks()
|
||||
const videoTrack = value === 'auto'
|
||||
? await this.input.getPrimaryVideoTrack()
|
||||
: videoTracks.find(track => track.id === value) ?? this.media.videoTrack
|
||||
|
||||
if (!videoTrack)
|
||||
return
|
||||
|
||||
let audioTrack = this.media.audioTrack
|
||||
let audioMode = this.media.audioMode
|
||||
|
||||
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
||||
audioTrack = await videoTrack.getPrimaryPairableAudioTrack()
|
||||
audioMode = 'auto'
|
||||
}
|
||||
|
||||
await this.replaceTracks({
|
||||
videoTrack,
|
||||
audioTrack,
|
||||
videoMode: value === 'auto' ? 'auto' : 'manual',
|
||||
audioMode,
|
||||
})
|
||||
}
|
||||
|
||||
async selectHlsAudio(value) {
|
||||
if (!this.media?.isHls || !this.input)
|
||||
return
|
||||
|
||||
const audioTracks = await this.input.getAudioTracks()
|
||||
const audioTrack = value === 'auto'
|
||||
? this.media.videoTrack
|
||||
? await this.media.videoTrack.getPrimaryPairableAudioTrack()
|
||||
: await this.input.getPrimaryAudioTrack()
|
||||
: audioTracks.find(track => track.id === value) ?? this.media.audioTrack
|
||||
|
||||
if (!audioTrack)
|
||||
return
|
||||
|
||||
let videoTrack = this.media.videoTrack
|
||||
let videoMode = this.media.videoMode
|
||||
|
||||
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
||||
videoTrack = await audioTrack.getPrimaryPairableVideoTrack()
|
||||
videoMode = 'auto'
|
||||
}
|
||||
|
||||
await this.replaceTracks({
|
||||
videoTrack,
|
||||
audioTrack,
|
||||
videoMode,
|
||||
audioMode: value === 'auto' ? 'auto' : 'manual',
|
||||
})
|
||||
}
|
||||
|
||||
getHlsState() {
|
||||
return getHlsState(this.media)
|
||||
}
|
||||
|
||||
async play() {
|
||||
if (!this.paused)
|
||||
return
|
||||
|
||||
if (this.ended) {
|
||||
this.ended = false
|
||||
await this.seek(0)
|
||||
}
|
||||
|
||||
this.paused = false
|
||||
|
||||
await this.audio.play()
|
||||
this.video.start(this.audio)
|
||||
|
||||
this.events.emit('play')
|
||||
this.events.emit('playing')
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (this.paused)
|
||||
return
|
||||
|
||||
this.paused = true
|
||||
|
||||
this.audio.pause()
|
||||
this.video.stop()
|
||||
|
||||
this.events.emit('pause')
|
||||
}
|
||||
|
||||
async seek(time) {
|
||||
const shouldResume = !this.paused
|
||||
|
||||
this.ended = false
|
||||
this.seeking = true
|
||||
|
||||
this.events.emit('seeking')
|
||||
this.events.emit('waiting')
|
||||
|
||||
this.pause()
|
||||
|
||||
await Promise.all([
|
||||
this.audio.seek(time),
|
||||
this.video.seek(time),
|
||||
])
|
||||
|
||||
this.seeking = false
|
||||
this.events.emit('seeked')
|
||||
|
||||
if (shouldResume && !this.ended) {
|
||||
await this.play()
|
||||
}
|
||||
}
|
||||
|
||||
setVolume(volume, muted) {
|
||||
this.audio.setVolume(volume, muted)
|
||||
}
|
||||
|
||||
setPlaybackRate(rate) {
|
||||
this.audio.setPlaybackRate(rate)
|
||||
this.video.setPlaybackRate(rate)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.destroyed)
|
||||
return
|
||||
this.destroyed = true
|
||||
this.loadSeq++
|
||||
this.loadSession?.cancel()
|
||||
this.pause()
|
||||
this.disposeInput()
|
||||
this.audio.destroy()
|
||||
this.video.destroy()
|
||||
}
|
||||
|
||||
// Getters
|
||||
get currentTime() {
|
||||
return this.audio.currentTime
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return this.media?.duration ?? this.audio.duration ?? this.video.duration
|
||||
}
|
||||
|
||||
get videoWidth() {
|
||||
return this.video.width
|
||||
}
|
||||
|
||||
get videoHeight() {
|
||||
return this.video.height
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import type { Input } from 'mediabunny'
|
||||
import type { AudioPort, VideoPort } from './engine-ports'
|
||||
import type { EngineOptions, EnginePort, ProxyOptions } from './engine-types'
|
||||
import type EventTarget from './EventTarget'
|
||||
import type { SelectedTracks } from './hls-selection'
|
||||
import type { PlaybackMedia } from './media'
|
||||
import AudioEngine from './AudioEngine.js'
|
||||
import { selectAudio, selectQuality } from './hls-selection'
|
||||
import { getHlsState } from './hls-state'
|
||||
import { createInput } from './input'
|
||||
import LoadSession from './load-session'
|
||||
import Playback from './playback'
|
||||
import { metadataBarrier, publish } from './readiness'
|
||||
import { selectPlaybackTracks } from './tracks'
|
||||
import VideoEngine from './VideoEngine.js'
|
||||
|
||||
export default class MediaBunnyEngine implements EnginePort {
|
||||
declare events: EventTarget
|
||||
declare option: ProxyOptions
|
||||
declare audio: AudioPort
|
||||
declare video: VideoPort
|
||||
declare paused: boolean
|
||||
declare ended: boolean
|
||||
declare readyState: number
|
||||
declare networkState: number
|
||||
declare error: { code: number, message: string } | null
|
||||
declare seeking: boolean
|
||||
declare loadSeq: number
|
||||
declare input: Input | null
|
||||
declare media: PlaybackMedia | null
|
||||
declare loadSession: LoadSession | null
|
||||
declare destroyed: boolean
|
||||
#playback: Playback
|
||||
|
||||
constructor({ canvas, ctx, events, option = {} }: EngineOptions) {
|
||||
this.events = events
|
||||
this.option = option
|
||||
this.audio = new AudioEngine(events)
|
||||
this.video = new VideoEngine({
|
||||
canvas,
|
||||
ctx,
|
||||
events,
|
||||
timeupdateInterval: option.timeupdateInterval ?? 250,
|
||||
avSyncTolerance: option.avSyncTolerance ?? 0.12,
|
||||
dropLateFrames: option.dropLateFrames ?? false,
|
||||
poster: option.poster ?? '',
|
||||
preflightRange: option.preflightRange ?? false,
|
||||
})
|
||||
this.paused = true
|
||||
this.ended = false
|
||||
this.readyState = 0
|
||||
this.networkState = 0
|
||||
this.error = null
|
||||
this.seeking = false
|
||||
this.loadSeq = 0
|
||||
this.input = null
|
||||
this.media = null
|
||||
this.loadSession = null
|
||||
this.destroyed = false
|
||||
this.#playback = new Playback(this)
|
||||
events.addEventListener('ended', () => {
|
||||
if (this.destroyed)
|
||||
return
|
||||
this.ended = true
|
||||
this.paused = true
|
||||
this.#playback.ended()
|
||||
})
|
||||
}
|
||||
|
||||
async load(src: unknown): Promise<void> {
|
||||
if (this.destroyed)
|
||||
return
|
||||
const id = ++this.loadSeq
|
||||
this.#playback.invalidate()
|
||||
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
|
||||
this.readyState = 0
|
||||
session.defer(() => this.events.emit('waiting'))
|
||||
session.defer(() => this.events.emit('loadstart'))
|
||||
const timeout = Number.isFinite(this.option.loadTimeout) ? this.option.loadTimeout ?? 0 : 0
|
||||
try {
|
||||
await session.wait(this.performLoad(src, id, session), timeout)
|
||||
}
|
||||
catch (error) {
|
||||
if (id !== this.loadSeq)
|
||||
return
|
||||
const failed = ++this.loadSeq
|
||||
this.#playback.invalidate()
|
||||
session.cancel()
|
||||
if (failed !== this.loadSeq || this.destroyed)
|
||||
return
|
||||
this.disposeInput()
|
||||
this.reportError(error)
|
||||
}
|
||||
}
|
||||
|
||||
async performLoad(src: unknown, id: number, session = this.loadSession): Promise<void> {
|
||||
const current = () => !this.destroyed && id === this.loadSeq
|
||||
if (!session || !(await this.video.preflight(src, session.signal, current)) || !current())
|
||||
return
|
||||
const input = createInput(src)
|
||||
if (!input) {
|
||||
this.video.handleNoVideoTrack()
|
||||
if (current())
|
||||
this.audio.handleNoAudioTrack()
|
||||
return
|
||||
}
|
||||
session.own(input)
|
||||
const media = await selectPlaybackTracks(input, src)
|
||||
if (!current())
|
||||
return
|
||||
if (!media.videoTrack && !media.audioTrack) {
|
||||
this.video.handleNoVideoTrack()
|
||||
this.video.duration = Number.NaN
|
||||
this.audio.duration = Number.NaN
|
||||
throw new Error('Input has no audio or video tracks.')
|
||||
}
|
||||
this.input = input
|
||||
this.media = media
|
||||
const metadata = metadataBarrier(() => {
|
||||
this.readyState = 1
|
||||
publish(this.events, current, ['loadedmetadata', 'durationchange', 'progress'])
|
||||
}, current)
|
||||
try {
|
||||
const video = this.video.load(media, metadata.video)
|
||||
const audio = current() ? this.audio.load(media, metadata.audio) : Promise.resolve()
|
||||
await Promise.all([video, audio])
|
||||
if (!current())
|
||||
return
|
||||
this.readyState = 4
|
||||
this.networkState = 1
|
||||
publish(this.events, current, ['loadeddata', 'canplay', 'canplaythrough', 'progress'])
|
||||
}
|
||||
catch (error) {
|
||||
if (!current())
|
||||
return
|
||||
console.error('MediaBunny load error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
createTimeout(ms: number): Promise<never> {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Load timeout')), ms)
|
||||
})
|
||||
}
|
||||
|
||||
disposeInput(): void {
|
||||
this.loadSession?.disposeInput()
|
||||
this.input = null
|
||||
this.media = null
|
||||
}
|
||||
|
||||
async replaceTracks(tracks: SelectedTracks): Promise<void> {
|
||||
if (!this.media || this.destroyed)
|
||||
return
|
||||
return this.#playback.replace({ ...this.media, ...tracks }, this.currentTime)
|
||||
}
|
||||
|
||||
async selectHlsQuality(value: unknown): Promise<void> {
|
||||
return selectQuality(this, value)
|
||||
}
|
||||
|
||||
async selectHlsAudio(value: unknown): Promise<void> {
|
||||
return selectAudio(this, value)
|
||||
}
|
||||
|
||||
async getHlsState() {
|
||||
const media = this.media
|
||||
if (this.destroyed)
|
||||
return null
|
||||
try {
|
||||
const state = await getHlsState(media)
|
||||
return !this.destroyed && this.media === media ? state : null
|
||||
}
|
||||
catch (error) {
|
||||
if (this.destroyed || this.media !== media)
|
||||
return null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async play(): Promise<void> {
|
||||
return this.#playback.play()
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
this.#playback.pause()
|
||||
}
|
||||
|
||||
async seek(time: number): Promise<void> {
|
||||
return this.#playback.seek(time)
|
||||
}
|
||||
|
||||
setVolume(volume: number, muted: boolean): void {
|
||||
this.audio.setVolume(volume, muted)
|
||||
}
|
||||
|
||||
setPlaybackRate(rate: number): void {
|
||||
this.audio.setPlaybackRate(rate)
|
||||
this.video.setPlaybackRate(rate)
|
||||
}
|
||||
|
||||
reportError(error: unknown): void {
|
||||
if (this.destroyed)
|
||||
return
|
||||
const message: unknown = error && typeof error === 'object' ? Reflect.get(error, 'message') : undefined
|
||||
this.error = { code: 4, message: typeof message === 'string' ? message : String(error) }
|
||||
this.networkState = 3
|
||||
this.events.emit('error')
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.destroyed)
|
||||
return
|
||||
this.destroyed = true
|
||||
this.loadSeq++
|
||||
this.#playback.invalidate()
|
||||
const failures: unknown[] = []
|
||||
for (const release of [() => this.loadSession?.cancel(), () => this.pause(), () => this.disposeInput(), () => this.audio.destroy(), () => this.video.destroy()]) {
|
||||
try {
|
||||
release()
|
||||
}
|
||||
catch (error) { failures.push(error) }
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0]
|
||||
}
|
||||
|
||||
get currentTime(): number {
|
||||
return this.audio.currentTime
|
||||
}
|
||||
|
||||
get duration(): number {
|
||||
return this.media?.duration ?? this.audio.duration ?? this.video.duration
|
||||
}
|
||||
|
||||
get videoWidth(): number {
|
||||
return this.video.width
|
||||
}
|
||||
|
||||
get videoHeight(): number {
|
||||
return this.video.height
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { VideoOptions, VideoPort } from './engine-ports'
|
||||
|
||||
// Exact coordinator-facing port; implementation migration and frame ownership are MB-05.
|
||||
declare const VideoEngine: new (options: VideoOptions) => VideoPort
|
||||
export default VideoEngine
|
||||
@@ -87,7 +87,8 @@ export default class VideoShim {
|
||||
}
|
||||
|
||||
set currentTime(t: unknown) {
|
||||
this.engine.seek(Number(t) || 0)
|
||||
// The coordinator reports active failures; the synchronous setter must observe rejection.
|
||||
void Promise.resolve(this.engine.seek(Number(t) || 0)).catch(() => {})
|
||||
}
|
||||
|
||||
get duration() {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { Input } from 'mediabunny'
|
||||
import type { ProxyOptions } from './engine-types'
|
||||
import type EventTarget from './EventTarget'
|
||||
import type { PlaybackMedia } from './media'
|
||||
|
||||
export interface AudioPort {
|
||||
readonly currentTime: number
|
||||
duration: number
|
||||
paused: boolean
|
||||
load: (media: PlaybackMedia, metadata?: () => void) => Promise<void>
|
||||
play: () => Promise<void>
|
||||
pause: () => void
|
||||
seek: (time: number) => Promise<void>
|
||||
setVolume: (volume: number, muted: boolean) => void
|
||||
setPlaybackRate: (rate: number) => void
|
||||
handleNoAudioTrack: () => void
|
||||
destroy: () => void
|
||||
}
|
||||
|
||||
export interface VideoPort {
|
||||
width: number
|
||||
height: number
|
||||
duration: number
|
||||
preflight: (source: unknown, signal?: AbortSignal, current?: () => boolean) => Promise<boolean>
|
||||
load: (media: PlaybackMedia, metadata?: () => void) => Promise<void>
|
||||
start: (clock: AudioPort) => void
|
||||
stop: () => void
|
||||
seek: (time: number) => Promise<void>
|
||||
setPlaybackRate: (rate: number) => void
|
||||
handleNoVideoTrack: () => void
|
||||
destroy: () => void
|
||||
}
|
||||
|
||||
export interface VideoOptions {
|
||||
canvas: HTMLCanvasElement
|
||||
ctx: CanvasRenderingContext2D | null
|
||||
events: EventTarget
|
||||
timeupdateInterval: number
|
||||
avSyncTolerance: number
|
||||
dropLateFrames: boolean
|
||||
poster: string
|
||||
preflightRange: boolean
|
||||
}
|
||||
|
||||
export interface PlaybackHost {
|
||||
audio: AudioPort
|
||||
video: VideoPort
|
||||
events: EventTarget
|
||||
readonly destroyed: boolean
|
||||
readonly loadSeq: number
|
||||
paused: boolean
|
||||
ended: boolean
|
||||
seeking: boolean
|
||||
readyState: number
|
||||
networkState: number
|
||||
media: PlaybackMedia | null
|
||||
input: Input | null
|
||||
option: ProxyOptions
|
||||
reportError: (error: unknown) => void
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export interface EngineOptions {
|
||||
option?: ProxyOptions
|
||||
}
|
||||
|
||||
// Transitional boundary for the coordinator; replaced by its strict implementation in MB-04.
|
||||
// Stable coordinator-facing surface used by the public shim.
|
||||
export interface EnginePort {
|
||||
readonly currentTime: number
|
||||
readonly duration: number
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { Input } from 'mediabunny'
|
||||
import type { PlaybackMedia } from './media'
|
||||
|
||||
export type SelectedTracks = Pick<PlaybackMedia, 'videoTrack' | 'audioTrack' | 'videoMode' | 'audioMode'>
|
||||
|
||||
interface SelectionHost {
|
||||
input: Input | null
|
||||
media: PlaybackMedia | null
|
||||
destroyed: boolean
|
||||
loadSeq: number
|
||||
replaceTracks: (tracks: SelectedTracks) => Promise<void>
|
||||
}
|
||||
|
||||
export async function selectQuality(host: SelectionHost, value: unknown): Promise<void> {
|
||||
const { input, media, loadSeq } = host
|
||||
if (!media?.isHls || !input || host.destroyed)
|
||||
return
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && host.media === media
|
||||
let selection: SelectedTracks
|
||||
try {
|
||||
const tracks = await input.getVideoTracks()
|
||||
if (!current())
|
||||
return
|
||||
const videoTrack = value === 'auto' ? await input.getPrimaryVideoTrack() : tracks.find(track => track.id === value) ?? media.videoTrack
|
||||
if (!current() || !videoTrack)
|
||||
return
|
||||
let audioTrack = media.audioTrack
|
||||
let audioMode = media.audioMode
|
||||
if (!audioTrack || !videoTrack.canBePairedWith(audioTrack)) {
|
||||
audioTrack = await videoTrack.getPrimaryPairableAudioTrack()
|
||||
audioMode = 'auto'
|
||||
}
|
||||
selection = { videoTrack, audioTrack, videoMode: value === 'auto' ? 'auto' : 'manual', audioMode }
|
||||
}
|
||||
catch (error) {
|
||||
if (!current())
|
||||
return
|
||||
throw error
|
||||
}
|
||||
if (current())
|
||||
await host.replaceTracks(selection)
|
||||
}
|
||||
|
||||
export async function selectAudio(host: SelectionHost, value: unknown): Promise<void> {
|
||||
const { input, media, loadSeq } = host
|
||||
if (!media?.isHls || !input || host.destroyed)
|
||||
return
|
||||
const current = () => !host.destroyed && host.loadSeq === loadSeq && host.input === input && host.media === media
|
||||
let selection: SelectedTracks
|
||||
try {
|
||||
const tracks = await input.getAudioTracks()
|
||||
if (!current())
|
||||
return
|
||||
const audioTrack = value === 'auto'
|
||||
? media.videoTrack ? await media.videoTrack.getPrimaryPairableAudioTrack() : await input.getPrimaryAudioTrack()
|
||||
: tracks.find(track => track.id === value) ?? media.audioTrack
|
||||
if (!current() || !audioTrack)
|
||||
return
|
||||
let videoTrack = media.videoTrack
|
||||
let videoMode = media.videoMode
|
||||
if (!videoTrack || !audioTrack.canBePairedWith(videoTrack)) {
|
||||
videoTrack = await audioTrack.getPrimaryPairableVideoTrack()
|
||||
videoMode = 'auto'
|
||||
}
|
||||
selection = { videoTrack, audioTrack, videoMode, audioMode: value === 'auto' ? 'auto' : 'manual' }
|
||||
}
|
||||
catch (error) {
|
||||
if (!current())
|
||||
return
|
||||
throw error
|
||||
}
|
||||
if (current())
|
||||
await host.replaceTracks(selection)
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import type { PlaybackHost } from './engine-ports'
|
||||
import type { PlaybackMedia } from './media'
|
||||
import { publish } from './readiness'
|
||||
import { resolveDuration } from './tracks'
|
||||
|
||||
interface Completion {
|
||||
promise: Promise<void>
|
||||
resolve: () => void
|
||||
reject: (error: unknown) => void
|
||||
}
|
||||
|
||||
interface Operation extends Completion {
|
||||
source: number
|
||||
complete: Completion
|
||||
}
|
||||
|
||||
function completion(): Completion {
|
||||
let resolve!: () => void
|
||||
let reject!: (error: unknown) => void
|
||||
const promise = new Promise<void>((yes, no) => {
|
||||
resolve = yes
|
||||
reject = no
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
export default class Playback {
|
||||
private wanted = false
|
||||
private playing: Operation | null = null
|
||||
private seeking: Operation | null = null
|
||||
private audioStart: Promise<void> | null = null
|
||||
|
||||
constructor(private host: PlaybackHost) {}
|
||||
|
||||
private operation(): Operation {
|
||||
return { ...completion(), source: this.host.loadSeq, complete: completion() }
|
||||
}
|
||||
|
||||
private current(operation: Operation, kind: 'playing' | 'seeking'): boolean {
|
||||
return !this.host.destroyed && operation.source === this.host.loadSeq && this[kind] === operation
|
||||
}
|
||||
|
||||
private cancelPlay(): void {
|
||||
const previous = this.playing
|
||||
this.playing = null
|
||||
previous?.resolve()
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
this.cancelPlay()
|
||||
this.wanted = false
|
||||
const previous = this.seeking
|
||||
this.seeking = null
|
||||
previous?.complete.resolve()
|
||||
previous?.resolve()
|
||||
this.host.seeking = false
|
||||
}
|
||||
|
||||
ended(): void {
|
||||
this.wanted = false
|
||||
this.cancelPlay()
|
||||
}
|
||||
|
||||
private stop(): void {
|
||||
if (this.host.paused)
|
||||
return
|
||||
this.host.paused = true
|
||||
this.stopEngines()
|
||||
this.host.events.emit('pause')
|
||||
}
|
||||
|
||||
private stopEngines(): void {
|
||||
const failures: unknown[] = []
|
||||
for (const stop of [() => this.host.audio.pause(), () => this.host.video.stop()]) {
|
||||
try {
|
||||
stop()
|
||||
}
|
||||
catch (error) { failures.push(error) }
|
||||
}
|
||||
if (failures.length)
|
||||
throw failures[0]
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
this.wanted = false
|
||||
this.cancelPlay()
|
||||
this.stop()
|
||||
}
|
||||
|
||||
play(): Promise<void> {
|
||||
if (this.host.destroyed)
|
||||
return Promise.resolve()
|
||||
this.wanted = true
|
||||
if (this.playing)
|
||||
return this.playing.promise
|
||||
if (this.host.ended)
|
||||
return this.seek(0, true)
|
||||
if (!this.host.paused && !this.seeking)
|
||||
return Promise.resolve()
|
||||
const operation = this.operation()
|
||||
this.playing = operation
|
||||
this.runPlay(operation).then(operation.resolve, operation.reject)
|
||||
return operation.promise
|
||||
}
|
||||
|
||||
private async startAudio(): Promise<void> {
|
||||
if (!this.audioStart)
|
||||
this.audioStart = this.host.audio.play()
|
||||
const pending = this.audioStart
|
||||
try {
|
||||
await pending
|
||||
}
|
||||
finally {
|
||||
if (this.audioStart === pending)
|
||||
this.audioStart = null
|
||||
}
|
||||
}
|
||||
|
||||
private async runPlay(operation: Operation): Promise<void> {
|
||||
const current = () => this.current(operation, 'playing') && this.wanted
|
||||
try {
|
||||
while (this.seeking) {
|
||||
await this.seeking.complete.promise
|
||||
if (!current())
|
||||
return
|
||||
}
|
||||
if (!current())
|
||||
return
|
||||
this.host.paused = false
|
||||
await this.startAudio()
|
||||
if (!current()) {
|
||||
if (!this.wanted || this.host.destroyed)
|
||||
this.host.audio.pause()
|
||||
return
|
||||
}
|
||||
this.host.video.start(this.host.audio)
|
||||
publish(this.host.events, current, ['play', 'playing'])
|
||||
}
|
||||
catch (error) {
|
||||
if (!current())
|
||||
return
|
||||
this.wanted = false
|
||||
this.host.paused = true
|
||||
try {
|
||||
this.stopEngines()
|
||||
}
|
||||
catch (failure) { console.warn('MediaBunny play cleanup error:', failure) }
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
if (this.playing === operation)
|
||||
this.playing = null
|
||||
}
|
||||
}
|
||||
|
||||
seek(time: number, resume?: boolean): Promise<void> {
|
||||
return this.position(async (current) => {
|
||||
const audio = this.host.audio.seek(time)
|
||||
const video = current() ? this.host.video.seek(time) : Promise.resolve()
|
||||
await Promise.all([audio, video])
|
||||
}, ['seeked'], false, resume)
|
||||
}
|
||||
|
||||
replace(media: PlaybackMedia, time: number): Promise<void> {
|
||||
return this.position(async (current) => {
|
||||
media.duration = await resolveDuration(media)
|
||||
if (!current())
|
||||
return
|
||||
this.host.media = media
|
||||
await Promise.all([this.host.video.load(media), this.host.audio.load(media)])
|
||||
if (!current())
|
||||
return
|
||||
await Promise.all([this.host.video.seek(time), this.host.audio.seek(time)])
|
||||
if (current()) {
|
||||
this.host.readyState = 4
|
||||
this.host.networkState = 1
|
||||
}
|
||||
}, ['loadedmetadata', 'durationchange', 'progress', 'loadeddata', 'canplay', 'canplaythrough', 'seeked'], true)
|
||||
}
|
||||
|
||||
private position(work: (current: () => boolean) => Promise<void>, events: readonly string[], pauseFirst: boolean, resume?: boolean): Promise<void> {
|
||||
if (this.host.destroyed)
|
||||
return Promise.resolve()
|
||||
this.wanted = resume ?? (this.seeking ? this.wanted : !this.host.paused)
|
||||
this.cancelPlay()
|
||||
const previous = this.seeking
|
||||
const operation = this.operation()
|
||||
this.seeking = operation
|
||||
previous?.complete.resolve()
|
||||
previous?.resolve()
|
||||
this.runPosition(operation, work, events, pauseFirst).then(operation.resolve, operation.reject)
|
||||
return operation.promise
|
||||
}
|
||||
|
||||
private async runPosition(operation: Operation, work: (current: () => boolean) => Promise<void>, events: readonly string[], pauseFirst: boolean): Promise<void> {
|
||||
const current = () => this.current(operation, 'seeking')
|
||||
let resume = false
|
||||
try {
|
||||
if (pauseFirst)
|
||||
this.stop()
|
||||
if (!current())
|
||||
return
|
||||
this.host.ended = false
|
||||
this.host.seeking = true
|
||||
publish(this.host.events, current, ['seeking', 'waiting'])
|
||||
if (!current())
|
||||
return
|
||||
if (!pauseFirst)
|
||||
this.stop()
|
||||
if (!current())
|
||||
return
|
||||
await work(current)
|
||||
if (!current())
|
||||
return
|
||||
this.host.seeking = false
|
||||
operation.complete.resolve()
|
||||
publish(this.host.events, current, events)
|
||||
if (!current())
|
||||
return
|
||||
this.seeking = null
|
||||
resume = this.wanted
|
||||
}
|
||||
catch (error) {
|
||||
if (!current())
|
||||
return
|
||||
this.host.seeking = false
|
||||
this.wanted = false
|
||||
this.cancelPlay()
|
||||
this.host.reportError(error)
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
operation.complete.resolve()
|
||||
if (this.seeking === operation)
|
||||
this.seeking = null
|
||||
}
|
||||
if (resume)
|
||||
await this.play()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type EventTarget from './EventTarget'
|
||||
|
||||
export function publish(events: EventTarget, current: () => boolean, names: readonly string[]): void {
|
||||
for (const name of names) {
|
||||
if (!current())
|
||||
return
|
||||
events.emit(name)
|
||||
}
|
||||
}
|
||||
|
||||
export function metadataBarrier(ready: () => void, current: () => boolean) {
|
||||
let video = false
|
||||
let audio = false
|
||||
let published = false
|
||||
function complete(part: 'audio' | 'video'): void {
|
||||
if (!current() || published)
|
||||
return
|
||||
if (part === 'video')
|
||||
video = true
|
||||
else audio = true
|
||||
if (video && audio) {
|
||||
published = true
|
||||
ready()
|
||||
}
|
||||
}
|
||||
return { video: () => complete('video'), audio: () => complete('audio') }
|
||||
}
|
||||
@@ -0,0 +1,728 @@
|
||||
{
|
||||
"task": "PKG-MB-04",
|
||||
"status": "done",
|
||||
"parentCommit": "9af253a5a9db9c10062f7552f4e084582d5f6e94",
|
||||
"sourceSha256LF": {
|
||||
"packages/artplayer-proxy-mediabunny/src/AudioEngine.d.ts": "2d59216f234173253b87e2b3ab8710efd25666f6cadbb2d1052b5c1cb829635a",
|
||||
"packages/artplayer-proxy-mediabunny/src/AudioEngine.js": "910d6831372e4895d55dec78010f281a199bc88f99735630a5a3b621f192ac35",
|
||||
"packages/artplayer-proxy-mediabunny/src/engine-ports.ts": "43a5b9277f2b52a537c522f1cf2a2c92ce4cfb1d2fb6c91e73519371a92940a2",
|
||||
"packages/artplayer-proxy-mediabunny/src/engine-types.ts": "857a61f1fa3531bd737626658981eaab1b83eb369266fb45528676b9f57a293a",
|
||||
"packages/artplayer-proxy-mediabunny/src/EventTarget.ts": "4cea57e59b0a1118744a55b0a87c1df28cb0d69c26c50f9275039fe7540c9720",
|
||||
"packages/artplayer-proxy-mediabunny/src/hls-selection.ts": "a8632c0ea7d0c59434de005eee81bbf8d1dea8ae73c690673b603e5d6fa3cfd6",
|
||||
"packages/artplayer-proxy-mediabunny/src/hls-state.ts": "ff9ffd742f9763349df1f50eac6b6bd488a80d05aaab8ca198f387f0d17d5e25",
|
||||
"packages/artplayer-proxy-mediabunny/src/index.js": "4e1be8937908ff6e6ef8a4e863e22b4468312cf31d66ac299322e475757773bb",
|
||||
"packages/artplayer-proxy-mediabunny/src/input.ts": "d25b1c4ee4aee358c86fd8c554b8b36bfc23fdbd3c27fa21d2cd7e812c98bf9c",
|
||||
"packages/artplayer-proxy-mediabunny/src/load-session.ts": "560fbd5935fc787bde5f413fcaa41b8829506179c5328ce3cb51e734bbba8cec",
|
||||
"packages/artplayer-proxy-mediabunny/src/m3u8.js": "4639f09e0c743dc2b24078f48ca1e58eaaedaa3f5e6b863583cbd4bc4a7d3142",
|
||||
"packages/artplayer-proxy-mediabunny/src/media.ts": "e745cd8e750e05d0fcfa4bc588f7eb5a5576f3be20f5e67b65b704f57d539ae9",
|
||||
"packages/artplayer-proxy-mediabunny/src/MediaBunnyEngine.ts": "d13bdd700399b57a4ff90a0fa9bdedf10206609e994ff07326a37c6838be135c",
|
||||
"packages/artplayer-proxy-mediabunny/src/playback.ts": "0512f6a1b253d754b6644266327e368e0e18cce1cf9a3af62b6747ed55d691f7",
|
||||
"packages/artplayer-proxy-mediabunny/src/preflight.ts": "d296234312f23b97a1e0a054f1d7dbbc988d5240c5fd5089c36df1677c7b7348",
|
||||
"packages/artplayer-proxy-mediabunny/src/readiness.ts": "9aa24b5e994006b45bda9eafad602785c3e0e334cef54ff45a4939e386c4aceb",
|
||||
"packages/artplayer-proxy-mediabunny/src/shim-frames.ts": "172b222f997a711c2cc7741f189f05127a4ac5099c1cbe2729e903b36b4ce24a",
|
||||
"packages/artplayer-proxy-mediabunny/src/shim-values.ts": "a93ee17e87cc7db24f4b02d866af8af206d3007d01def446906fb769c20de17d",
|
||||
"packages/artplayer-proxy-mediabunny/src/tracks.ts": "6da68530c1fc7105ab17ed742d87d93e84447e822e84386591f9c3e6d0bec733",
|
||||
"packages/artplayer-proxy-mediabunny/src/VideoEngine.d.ts": "172fd4d30fa98a3eaa540ac8e5b653ae321c4ad8f376f3954550e2298bff5a9d",
|
||||
"packages/artplayer-proxy-mediabunny/src/VideoEngine.js": "5ac862b0fd9c196dc6656f8908a214c1b3a0a53a7e85ff845d9068888831986b",
|
||||
"packages/artplayer-proxy-mediabunny/src/VideoShim.ts": "1fef3028a3937182c31503129655efdb88118ea0f6d3be96e9be99dcbcc5fdc7",
|
||||
"packages/artplayer-proxy-mediabunny/dist/artplayer-proxy-mediabunny.js": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"packages/artplayer-proxy-mediabunny/dist/artplayer-proxy-mediabunny.legacy.js": "7c39173fdc56ce9089542191f8e0d731d825967244828e4a358d951421889cfc",
|
||||
"packages/artplayer-proxy-mediabunny/dist/artplayer-proxy-mediabunny.mjs": "aed179b85002f5ddab3f26a02b20ad5e0fae203bc865cb0a241a003aa22c8a9e",
|
||||
"packages/artplayer-proxy-mediabunny/ARCHITECTURE.md": "9bd639cab075ac3e5c333b509c016614ad64e14fd58b3f308e0656e325662a43",
|
||||
"package.json": "f91ece4140c07c5bcec06556d088cd98b0d368471df7b129999e3239d5960334",
|
||||
"test/mediabunny-coordination.test.js": "1ce97e02492c0b85e041f3bf33b75ec9915af160d7fd7550bef84408e034cba9",
|
||||
"test/browser/mediabunny-inputs.spec.js": "561fbb9316691210c239831aa9f87ec5f2b6adaa5a7cde7b5830b721bd9d56de",
|
||||
"test/browser/mediabunny.spec.js": "59491aa2007e1816fb8561f3feaf4d149c74667994ed2632ea3d37cf10891364"
|
||||
},
|
||||
"node": {
|
||||
"total": 108,
|
||||
"coordinator": 40,
|
||||
"sameCoordinatorAssertionsOnPreviousMain": {
|
||||
"passed": 8,
|
||||
"failed": 32
|
||||
},
|
||||
"main": 72,
|
||||
"legacy": 72,
|
||||
"artifactScope": "Each 72-case command includes 68 candidate assertions and four frozen normal-event controls."
|
||||
},
|
||||
"browser": {
|
||||
"total": 39,
|
||||
"outcomes": {
|
||||
"native-input-playback-and-seek": 10,
|
||||
"trackless-rejected-before-decoder-setup": 3,
|
||||
"unsupported-capability-control": 5,
|
||||
"native-pending-stream-cancellation": 9,
|
||||
"native-synthetic-raf": 9,
|
||||
"native-mp4-playback-observed": 2,
|
||||
"verified-unsupported-capability-control": 1
|
||||
},
|
||||
"records": [
|
||||
{
|
||||
"project": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "webm",
|
||||
"outcome": "native-input-playback-and-seek",
|
||||
"cleanup": {
|
||||
"audioContext": "closed",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"reentrantPlay": {
|
||||
"paused": true,
|
||||
"events": [
|
||||
"video:play",
|
||||
"video:pause"
|
||||
]
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "blob",
|
||||
"outcome": "native-input-playback-and-seek",
|
||||
"cleanup": {
|
||||
"audioContext": "closed",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "stream",
|
||||
"outcome": "native-input-playback-and-seek",
|
||||
"cleanup": {
|
||||
"audioContext": "closed",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": true
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "audio",
|
||||
"outcome": "native-input-playback-and-seek",
|
||||
"cleanup": {
|
||||
"audioContext": "closed",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "hls",
|
||||
"outcome": "native-input-playback-and-seek",
|
||||
"cleanup": {
|
||||
"audioContext": "closed",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "trackless",
|
||||
"outcome": "trackless-rejected-before-decoder-setup",
|
||||
"cleanup": {
|
||||
"audioContext": "not-created",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "webm",
|
||||
"outcome": "native-input-playback-and-seek",
|
||||
"cleanup": {
|
||||
"audioContext": "closed",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"reentrantPlay": {
|
||||
"paused": true,
|
||||
"events": [
|
||||
"video:play",
|
||||
"video:pause"
|
||||
]
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "blob",
|
||||
"outcome": "native-input-playback-and-seek",
|
||||
"cleanup": {
|
||||
"audioContext": "closed",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "stream",
|
||||
"outcome": "native-input-playback-and-seek",
|
||||
"cleanup": {
|
||||
"audioContext": "closed",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": true
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "audio",
|
||||
"outcome": "native-input-playback-and-seek",
|
||||
"cleanup": {
|
||||
"audioContext": "closed",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "hls",
|
||||
"outcome": "native-input-playback-and-seek",
|
||||
"cleanup": {
|
||||
"audioContext": "closed",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "trackless",
|
||||
"outcome": "trackless-rejected-before-decoder-setup",
|
||||
"cleanup": {
|
||||
"audioContext": "not-created",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "webm",
|
||||
"outcome": "unsupported-capability-control",
|
||||
"cleanup": {
|
||||
"audioContext": "not-created",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "blob",
|
||||
"outcome": "unsupported-capability-control",
|
||||
"cleanup": {
|
||||
"audioContext": "not-created",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "stream",
|
||||
"outcome": "unsupported-capability-control",
|
||||
"cleanup": {
|
||||
"audioContext": "not-created",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": true
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "audio",
|
||||
"outcome": "unsupported-capability-control",
|
||||
"cleanup": {
|
||||
"audioContext": "not-created",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "hls",
|
||||
"outcome": "unsupported-capability-control",
|
||||
"cleanup": {
|
||||
"audioContext": "not-created",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"input": "trackless",
|
||||
"outcome": "trackless-rejected-before-decoder-setup",
|
||||
"cleanup": {
|
||||
"audioContext": "not-created",
|
||||
"pendingFrames": 0,
|
||||
"streamCancelled": false
|
||||
},
|
||||
"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 rejection is checked separately before decoder setup; this is not full release acceptance."
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"ending": "destroy",
|
||||
"outcome": "native-pending-stream-cancellation",
|
||||
"cancellations": [
|
||||
"old"
|
||||
],
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"ending": "source",
|
||||
"outcome": "native-pending-stream-cancellation",
|
||||
"cancellations": [
|
||||
"old",
|
||||
"new"
|
||||
],
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"ending": "timeout",
|
||||
"outcome": "native-pending-stream-cancellation",
|
||||
"cancellations": [
|
||||
"old"
|
||||
],
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"ending": "destroy",
|
||||
"outcome": "native-pending-stream-cancellation",
|
||||
"cancellations": [
|
||||
"old"
|
||||
],
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"ending": "source",
|
||||
"outcome": "native-pending-stream-cancellation",
|
||||
"cancellations": [
|
||||
"old",
|
||||
"new"
|
||||
],
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"ending": "timeout",
|
||||
"outcome": "native-pending-stream-cancellation",
|
||||
"cancellations": [
|
||||
"old"
|
||||
],
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"ending": "destroy",
|
||||
"outcome": "native-pending-stream-cancellation",
|
||||
"cancellations": [
|
||||
"old"
|
||||
],
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"ending": "source",
|
||||
"outcome": "native-pending-stream-cancellation",
|
||||
"cancellations": [
|
||||
"old",
|
||||
"new"
|
||||
],
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"ending": "timeout",
|
||||
"outcome": "native-pending-stream-cancellation",
|
||||
"cancellations": [
|
||||
"old"
|
||||
],
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"scenario": "callback",
|
||||
"outcome": "native-synthetic-raf",
|
||||
"result": {
|
||||
"idType": "number",
|
||||
"calls": [
|
||||
{
|
||||
"owner": "first",
|
||||
"time": "number",
|
||||
"mediaTime": 0,
|
||||
"presentedFrames": 0
|
||||
}
|
||||
],
|
||||
"listeners": 0
|
||||
},
|
||||
"scope": "Real browser requestAnimationFrame and actual proxy instances; synthetic metadata is preserved, no decoding or video frame accuracy is claimed."
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"scenario": "destroy",
|
||||
"outcome": "native-synthetic-raf",
|
||||
"result": {
|
||||
"idType": "number",
|
||||
"calls": [],
|
||||
"listeners": 0
|
||||
},
|
||||
"scope": "Real browser requestAnimationFrame and actual proxy instances; synthetic metadata is preserved, no decoding or video frame accuracy is claimed."
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"scenario": "independent",
|
||||
"outcome": "native-synthetic-raf",
|
||||
"result": {
|
||||
"idType": "number",
|
||||
"calls": [
|
||||
{
|
||||
"owner": "second"
|
||||
}
|
||||
],
|
||||
"listeners": 0
|
||||
},
|
||||
"scope": "Real browser requestAnimationFrame and actual proxy instances; synthetic metadata is preserved, no decoding or video frame accuracy is claimed."
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"scenario": "callback",
|
||||
"outcome": "native-synthetic-raf",
|
||||
"result": {
|
||||
"idType": "number",
|
||||
"calls": [
|
||||
{
|
||||
"owner": "first",
|
||||
"time": "number",
|
||||
"mediaTime": 0,
|
||||
"presentedFrames": 0
|
||||
}
|
||||
],
|
||||
"listeners": 0
|
||||
},
|
||||
"scope": "Real browser requestAnimationFrame and actual proxy instances; synthetic metadata is preserved, no decoding or video frame accuracy is claimed."
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"scenario": "destroy",
|
||||
"outcome": "native-synthetic-raf",
|
||||
"result": {
|
||||
"idType": "number",
|
||||
"calls": [],
|
||||
"listeners": 0
|
||||
},
|
||||
"scope": "Real browser requestAnimationFrame and actual proxy instances; synthetic metadata is preserved, no decoding or video frame accuracy is claimed."
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"scenario": "independent",
|
||||
"outcome": "native-synthetic-raf",
|
||||
"result": {
|
||||
"idType": "number",
|
||||
"calls": [
|
||||
{
|
||||
"owner": "second"
|
||||
}
|
||||
],
|
||||
"listeners": 0
|
||||
},
|
||||
"scope": "Real browser requestAnimationFrame and actual proxy instances; synthetic metadata is preserved, no decoding or video frame accuracy is claimed."
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"scenario": "callback",
|
||||
"outcome": "native-synthetic-raf",
|
||||
"result": {
|
||||
"idType": "number",
|
||||
"calls": [
|
||||
{
|
||||
"owner": "first",
|
||||
"time": "number",
|
||||
"mediaTime": 0,
|
||||
"presentedFrames": 0
|
||||
}
|
||||
],
|
||||
"listeners": 0
|
||||
},
|
||||
"scope": "Real browser requestAnimationFrame and actual proxy instances; synthetic metadata is preserved, no decoding or video frame accuracy is claimed."
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"scenario": "destroy",
|
||||
"outcome": "native-synthetic-raf",
|
||||
"result": {
|
||||
"idType": "number",
|
||||
"calls": [],
|
||||
"listeners": 0
|
||||
},
|
||||
"scope": "Real browser requestAnimationFrame and actual proxy instances; synthetic metadata is preserved, no decoding or video frame accuracy is claimed."
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"scenario": "independent",
|
||||
"outcome": "native-synthetic-raf",
|
||||
"result": {
|
||||
"idType": "number",
|
||||
"calls": [
|
||||
{
|
||||
"owner": "second"
|
||||
}
|
||||
],
|
||||
"listeners": 0
|
||||
},
|
||||
"scope": "Real browser requestAnimationFrame and actual proxy instances; synthetic metadata is preserved, no decoding or video frame accuracy is claimed."
|
||||
},
|
||||
{
|
||||
"project": "chromium",
|
||||
"browser": "153.0.8010.12",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"outcome": "native-mp4-playback-observed",
|
||||
"scope": "Identified published or candidate proxy with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No HLS or long-run AV-sync acceptance."
|
||||
},
|
||||
{
|
||||
"project": "firefox",
|
||||
"browser": "155.0",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"outcome": "native-mp4-playback-observed",
|
||||
"scope": "Identified published or candidate proxy with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No HLS or long-run AV-sync acceptance."
|
||||
},
|
||||
{
|
||||
"project": "webkit",
|
||||
"browser": "26.6",
|
||||
"platform": "win32",
|
||||
"implementation": "candidate-artifact",
|
||||
"sha256": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"outcome": "verified-unsupported-capability-control",
|
||||
"scope": "Identified published or candidate proxy with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No HLS or long-run AV-sync acceptance."
|
||||
}
|
||||
],
|
||||
"report": "refactor/.cache/mb04-coordination-results/report.json",
|
||||
"reportSha256": "17dbd77680e414f0b27e94e278b24d46cd6cde443ed1bb96dcc2f3d285a6557f",
|
||||
"results": "refactor/.cache/mb04-coordination-results/results"
|
||||
},
|
||||
"checks": [
|
||||
{
|
||||
"command": "yarn test:mediabunny",
|
||||
"result": "pass",
|
||||
"tests": 108
|
||||
},
|
||||
{
|
||||
"command": "main/legacy shim, load and coordinator tests",
|
||||
"result": "pass",
|
||||
"eachRun": 72
|
||||
},
|
||||
{
|
||||
"command": "same 40 coordinator assertions on adba8a3d main",
|
||||
"result": "expected historical failures",
|
||||
"passed": 8,
|
||||
"failed": 32
|
||||
},
|
||||
{
|
||||
"command": "package strict tsc and focused eslint --no-fix",
|
||||
"result": "pass",
|
||||
"productionModules": 16,
|
||||
"temporaryDecoderDeclarationBridges": 2
|
||||
},
|
||||
{
|
||||
"command": "yarn build artplayer-proxy-mediabunny",
|
||||
"result": "pass",
|
||||
"formats": [
|
||||
"main",
|
||||
"legacy",
|
||||
"esm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"command": "four browser specs with built main",
|
||||
"result": "pass",
|
||||
"tests": 39
|
||||
},
|
||||
{
|
||||
"command": "yarn ci:check",
|
||||
"result": "pass",
|
||||
"tests": {
|
||||
"unit": 1114,
|
||||
"engineering": 14,
|
||||
"baseline": 141,
|
||||
"mainTotal": 1269,
|
||||
"repeatedContractExecutions": 44
|
||||
},
|
||||
"strictProductionTypeScriptFiles": 306,
|
||||
"log": "refactor/.cache/mb04-coordination-ci.log"
|
||||
},
|
||||
{
|
||||
"command": "built ESM default export and source function name",
|
||||
"result": "pass",
|
||||
"log": "refactor/.cache/mb04-coordination-esm.log"
|
||||
}
|
||||
],
|
||||
"limitations": [
|
||||
"AudioEngine and VideoEngine remain JS behind explicit temporary declarations; their late internal work is MB-05/06.",
|
||||
"Six Windows WebKit capability controls are not successful playback.",
|
||||
"Trackless false readiness is corrected; existing-track unsupported codec behavior remains a capability/decoder task.",
|
||||
"Synthetic RAF is not decoded frame accuracy; native sampled frames and short clocks are not long-run AV sync.",
|
||||
"HLS query source ownership is guarded; full concurrent selection/UI topology remains MB-07.",
|
||||
"No npm release or next-major approval is implied."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
# PKG-MB-04:VideoShim、事件与主协调器
|
||||
|
||||
任务通过完整CI后标记done,并立即以专用本地提交交付。前置shim检查点为9af253a5;本轮将
|
||||
MediaBunnyEngine.js实际迁移为严格TS,删除其临时声明桥。包内现在16个生产TS模块,
|
||||
AudioEngine/VideoEngine两个显式临时声明桥不计作实现迁移,分别留给MB-06/05。
|
||||
文件职责、状态归属、错误传播和继续维护入口见包内ARCHITECTURE.md。
|
||||
|
||||
## 行为修复与兼容性
|
||||
|
||||
- playback.ts独立管理播放意图和操作完成信号;pause能够阻止pending play或seek恢复,
|
||||
新操作使旧结果失效。取消可先结束公开Promise,底层晚结果仍被观察。
|
||||
- seek完成信号先于可选恢复播放,避免play等待seek而seek又等待play的死锁。
|
||||
play-pause-play共享尚未结束的audio resume,只有当前操作启动video和发送play/playing。
|
||||
- 每次就绪/播放事件前重新检查归属,允许用户在回调中同步切源、销毁或改变播放状态。
|
||||
正常play/pause、seek/resume和HLS replacement事件顺序由对照测试保持。
|
||||
- 两个metadata参与者均完成才发布一次metadata;无轨道容器提前报code 4,信息为
|
||||
`Input has no audio or video tracks.`,不再错误地readyState 4。有效媒体行为保持。
|
||||
- 活动play失败保留原始Promise拒绝并恢复paused;活动seek失败报告一次error并拒绝。
|
||||
currentTime同步setter观察无法返回的拒绝,直接调用engine.seek仍能捕获错误。
|
||||
- HLS旧source查询失败不污染新source,当前查询失败继续拒绝;replacement改变media对象
|
||||
后的活动错误也不能被误认为旧查询并吞掉。
|
||||
- destroy尽力执行所有input/audio/video释放步骤,之后保留首个异常;shim仍以finally
|
||||
清理事件和synthetic RAF。事件安静不等于底层解码器晚操作已完全停止。
|
||||
|
||||
工厂签名、公开声明、canvas描述符和同步/Promise接口保持。正常事件顺序、方法转发及
|
||||
描述符对照继续纳入验证。非正常行为差异是以上明确的竞态/错误修复,不宣称逐字节行为不变。
|
||||
旧createTimeout帮助方法保持原样,受管理的load路径不使用它;不要称所有手动调用的内部
|
||||
帮助方法也都具备自动取消。
|
||||
|
||||
## 验证
|
||||
|
||||
新增40项协调器测试,使用实际构建代理、受控异步音视频操作和实际SDK解析输入。
|
||||
同一断言对adba8a3d冻结main为8通过/32失败,候选全部通过。包专项108项通过。
|
||||
main与legacy各72项通过,其中68候选产物断言、4冻结正常事件对照。三格式构建通过。
|
||||
|
||||
初轮main产物测试有一条断言把新source合法waiting/loadstart误当作旧replacement事件。
|
||||
已缩小到旧seek/readiness结果并保留完整禁止列表,重新运行main/legacy均72通过;未修改
|
||||
生产代码来消除新来源合法事件。历史同断言仍32失败,修复回归证据没有被放宽掉。
|
||||
|
||||
浏览器报告及整个results归档后记录到mb-coordination-validation.json。39项候选检查区分
|
||||
12真实播放、9原生Stream取消、9synthetic RAF、6Windows WebKit能力对照及3无轨道拒绝。
|
||||
WebM在Chromium/Firefox另验证真实播放的play回调内pause后不会再发playing。
|
||||
此断言附属于现有播放用例,不增加测试数量。长播放、听觉输出和AV同步仍为MB-09。
|
||||
|
||||
## 后续边界
|
||||
|
||||
MB-05/06继续实际视频帧、解码器迭代器和Web Audio调度迁移;两处声明桥必须移除。
|
||||
MB-07继续HLS控件拓扑与并发选轨;MB-08完整公开类型/工厂兼容矩阵;MB-09/10完成组合、
|
||||
长播放、demo、真实安装包与许可通知。MB-LIFE-01/MB-READY-01保持open并更新已修复范围,
|
||||
既有轨道但不支持codec的路径仍需要解码器验证。此任务不构成npm发布验收。
|
||||
|
||||
最终完整CI1269项(1114单元+14工程+141基线)通过,另44重复契约;306生产TS。
|
||||
所有记录的源码/测试/产物及报告哈希已核验,三格式docs/compiled与dist一致。ESM默认工厂
|
||||
及实际源码名称artplayerProxyMediabunny保持;首次临时检查误写大小写,已按实际入口修正。
|
||||
回退本任务可revert专用提交;旧shim检查点9af253a5与更早adba8a3d基线仍可对照。
|
||||
未推送、打tag或发布;下一项PKG-MB-05处理视频迭代器、晚帧与seek释放。
|
||||
@@ -45,3 +45,12 @@ MB-03实施将input分离为六个strict TS模块,接入加载session、Range
|
||||
Chrome连接不可用时可使用内置浏览器;CLI自动化也按现有Playwright三引擎流程执行。
|
||||
保留report.json及整个results目录。受控输入不能替代真实解码,iframe不能替代原生Document PiP。
|
||||
VAST脚本VPN豁免不适用于本包。
|
||||
|
||||
## MB-04 coordinator implementation
|
||||
|
||||
VideoShim/EventTarget与主协调器已拆为严格TS模块,详情见
|
||||
[协调器变更](changes/2026-09-12-PKG-MB-04-coordination.md)及
|
||||
[验证记录](baselines/mb-coordination-validation.json)。包专项命令已包含新的协调器回归用例。
|
||||
无轨道容器由历史readyState 4改为code 4错误;有效媒体的正常顺序保持,重复就绪和
|
||||
失效操作结果被阻止。不要把synthetic RAF、受控竞态或Windows WebKit能力对照计为真实播放。
|
||||
音视频解码器仍通过两个显式声明桥连接,MB-05/06继续实际实现迁移和底层晚操作清理。
|
||||
+3
-3
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 114 / doing 8 / blocked 0 / done 100 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 114 / doing 7 / blocked 0 / done 101 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -332,7 +332,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 | done |
|
||||
| PKG-MB-04 | artplayer-proxy-mediabunny<br>整理 VideoShim/EventTarget/Engine 协调 | PKG-MB-03, CORE-06, CORE-11 | shim 描述符、事件桥、加载状态和操作隔离 | 公开属性/同步异步返回与事件重入顺序保持 | H | doing |
|
||||
| PKG-MB-04 | artplayer-proxy-mediabunny<br>整理 VideoShim/EventTarget/Engine 协调 | PKG-MB-03, CORE-06, CORE-11 | shim 描述符、事件桥、加载状态和操作隔离 | 公开属性/同步异步返回与事件重入顺序保持 | H | done |
|
||||
| PKG-MB-05 | artplayer-proxy-mediabunny<br>整理视频解码、seek 和帧释放 | PKG-MB-04 | VideoEngine 调度、队列、晚帧处理和资源释放 | 暂停/seek/切源无旧帧覆盖,默认丢帧策略不变 | H | todo |
|
||||
| PKG-MB-06 | artplayer-proxy-mediabunny<br>整理音频解码、时钟与同步 | PKG-MB-04 | AudioEngine 时钟、缓冲和 AudioContext 归属 | AV sync、倍速、无音轨、静音/音量和暂停恢复通过 | H | todo |
|
||||
| PKG-MB-07 | artplayer-proxy-mediabunny<br>整理 HLS 配对轨道与 selector | PKG-MB-05, PKG-MB-06, CORE-14 | m3u8 配对、质量/音频选择及拓扑清理 | 实际选择高亮、切到无轨道来源清理、无重复 readiness | H | todo |
|
||||
@@ -523,5 +523,5 @@
|
||||
- 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-MB-04: [记录](baselines/mb-coordination-source.json) [记录](changes/2026-09-12-PKG-MB-04-shim-checkpoint.md) [记录](baselines/mb-shim-checkpoint.json)
|
||||
- PKG-MB-04: [记录](baselines/mb-coordination-source.json) [记录](changes/2026-09-12-PKG-MB-04-shim-checkpoint.md) [记录](baselines/mb-shim-checkpoint.json) [记录](changes/2026-09-12-PKG-MB-04-coordination.md) [记录](baselines/mb-coordination-validation.json)
|
||||
- PKG-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,17 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-MB-04完成:主协调器TS与操作隔离
|
||||
|
||||
MediaBunnyEngine实际迁移TS,并拆分playback/readiness/HLS查询职责;正常接口和事件顺序
|
||||
保持,修复旧play/seek覆盖、错误吞掉、重复就绪及无轨道假就绪。新增40项测试在旧main上
|
||||
8通过/32失败,候选全通过;包专项108通过、main/legacy各72通过。最终39项浏览器
|
||||
包括12真实播放、9Stream取消、9synthetic RAF、6能力对照及3无轨道拒绝;另在已有
|
||||
Chromium/Firefox播放用例内验证play回调同步pause后无playing。完整CI1269通过,
|
||||
另44重复契约,306生产TS;包内16生产TS和2个未迁移解码器声明桥明确区分。
|
||||
见[变更](changes/2026-09-12-PKG-MB-04-coordination.md)及[证据](baselines/mb-coordination-validation.json)。
|
||||
222项:101 done、7 doing、114 todo;立即专用本地提交,下一项MB-05视频帧资源。
|
||||
MB-LIFE-01/MB-READY-01剩余解码器/能力与组合范围仍open;不构成npm发布验收。
|
||||
|
||||
## 当前检查点:PKG-MB-04 VideoShim、事件与RAF资源
|
||||
|
||||
新增五个生产TS模块,保持shim/canvas布局和正常事件/Promise/属性契约;修复销毁后的RAF、
|
||||
|
||||
+8
-4
@@ -3648,7 +3648,7 @@
|
||||
"PKG-MB-07",
|
||||
"PKG-MB-09"
|
||||
],
|
||||
"compatibleResolution": "Reproduce pending operations and destroy/source-switch races before migrating each owner; preserve public event and promise behavior while preventing stale resource use. MB-02 now reproduces late load rejection and uncancelled waiting/loadstart timers after destroy in all three historical implementations using a controlled host. Other interleavings and full-core native behavior are not yet verified. Expanded MB-02 baseline reproduces play-after-pause/destroy, rejected-play false state, seek-after-destroy and retained successful-load timeout timers in all three actual historical implementations; Range and active-timeout normal controls remain separate. MB-03 candidate owns pending Input before track selection and cancels load/HEAD/timers on source replacement, timeout and destroy; source/main/legacy regressions pass. Pending play/seek, late decoder work and full readiness coordination remain MB-04/05/06, so this risk stays open. MB-04 shim checkpoint fixes synthetic RAF retention and terminal event-listener cleanup, including engine cleanup exceptions. Closing the event bridge does not prove pending engine operations stopped; coordination remains in progress.",
|
||||
"compatibleResolution": "Reproduce pending operations and destroy/source-switch races before migrating each owner; preserve public event and promise behavior while preventing stale resource use. MB-02 now reproduces late load rejection and uncancelled waiting/loadstart timers after destroy in all three historical implementations using a controlled host. Other interleavings and full-core native behavior are not yet verified. Expanded MB-02 baseline reproduces play-after-pause/destroy, rejected-play false state, seek-after-destroy and retained successful-load timeout timers in all three actual historical implementations; Range and active-timeout normal controls remain separate. MB-03 candidate owns pending Input before track selection and cancels load/HEAD/timers on source replacement, timeout and destroy; source/main/legacy regressions pass. Pending play/seek, late decoder work and full readiness coordination remain MB-04/05/06, so this risk stays open. MB-04 shim checkpoint fixes synthetic RAF retention and terminal event-listener cleanup, including engine cleanup exceptions. Closing the event bridge does not prove pending engine operations stopped; coordination remains in progress. MB-04 now guards play/seek/replacement completion, preserves explicit pause, observes stale failures and isolates reentrant event sequences. Decoder-internal late frames/audio and HLS UI races remain MB-05/06/07; risk stays open.",
|
||||
"closureCriteria": "Historical failing interleavings and candidate cancellation/cleanup pass with real media and event evidence, including long playback and track topology.",
|
||||
"status": "open",
|
||||
"evidence": [
|
||||
@@ -3660,7 +3660,9 @@
|
||||
"refactor/changes/2026-09-12-PKG-MB-02-baseline.md",
|
||||
"refactor/baselines/mb-behavior-validation.json",
|
||||
"refactor/changes/2026-09-12-PKG-MB-03-input.md",
|
||||
"refactor/changes/2026-09-12-PKG-MB-04-shim-checkpoint.md"
|
||||
"refactor/changes/2026-09-12-PKG-MB-04-shim-checkpoint.md",
|
||||
"refactor/changes/2026-09-12-PKG-MB-04-coordination.md",
|
||||
"refactor/baselines/mb-coordination-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -3707,11 +3709,13 @@
|
||||
"PKG-MB-04",
|
||||
"PKG-MB-09"
|
||||
],
|
||||
"compatibleResolution": "Use explicit coordinated readiness and failure state, preserving valid event order while preventing duplicate or stale callbacks; evaluate trackless behavior separately from normal media.",
|
||||
"compatibleResolution": "Use explicit coordinated readiness and failure state, preserving valid event order while preventing duplicate or stale callbacks; evaluate trackless behavior separately from normal media. MB-04 coordinates metadata once, suppresses stale readiness and rejects trackless input before decoder setup. Existing-track unsupported-codec behavior and full native combinations remain MB-05/06/09; risk stays open.",
|
||||
"closureCriteria": "Candidate metadata/data/canplay ordering and counts pass normal, absent-track and failure interleavings in native browsers and controlled race tests.",
|
||||
"evidence": [
|
||||
"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-04-coordination.md",
|
||||
"refactor/baselines/mb-coordination-validation.json"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
+4
-2
@@ -3460,14 +3460,16 @@
|
||||
"CORE-06",
|
||||
"CORE-11"
|
||||
],
|
||||
"status": "doing",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "shim 描述符、事件桥、加载状态和操作隔离",
|
||||
"acceptance": "公开属性/同步异步返回与事件重入顺序保持",
|
||||
"evidence": [
|
||||
"baselines/mb-coordination-source.json",
|
||||
"changes/2026-09-12-PKG-MB-04-shim-checkpoint.md",
|
||||
"baselines/mb-shim-checkpoint.json"
|
||||
"baselines/mb-shim-checkpoint.json",
|
||||
"changes/2026-09-12-PKG-MB-04-coordination.md",
|
||||
"baselines/mb-coordination-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -95,6 +95,13 @@ for (const implementation of implementations) {
|
||||
expect(loaded.dimensions).toEqual([0, 0])
|
||||
outcome = input === 'hls' ? 'historical-unsupported-hls-control' : 'historical-stream-lock-control'
|
||||
}
|
||||
else if (implementation.name.startsWith('candidate') && input === 'trackless') {
|
||||
expect(loaded.error).toEqual({ code: 4, message: 'Input has no audio or video tracks.' })
|
||||
expect(loaded.readyState).toBe(0)
|
||||
expect(loaded.dimensions).toEqual([0, 0])
|
||||
expect(await page.evaluate(() => window.mbEvents.filter(item => ['video:loadedmetadata', 'video:loadeddata', 'video:canplay'].includes(item.name)))).toEqual([])
|
||||
outcome = 'trackless-rejected-before-decoder-setup'
|
||||
}
|
||||
else if (capabilities.AudioContext === 'undefined' && capabilities.webkitAudioContext === 'undefined') {
|
||||
expect(browserName).toBe('webkit')
|
||||
expect(capabilities).toEqual({ VideoDecoder: 'undefined', AudioDecoder: 'undefined', AudioContext: 'undefined', webkitAudioContext: 'undefined' })
|
||||
@@ -115,7 +122,7 @@ for (const implementation of implementations) {
|
||||
expect(loaded.dimensions).toEqual([0, 0])
|
||||
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 = implementation.name.startsWith('candidate') ? 'candidate-trackless-readiness-gap' : 'historical-trackless-readiness-control'
|
||||
outcome = '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)))
|
||||
@@ -177,6 +184,21 @@ for (const implementation of implementations) {
|
||||
await expect.poll(() => page.evaluate(() => window.mbFrameClockSamples.filter(sample => !sample.paused).length)).toBeGreaterThan(2)
|
||||
await page.evaluate(() => window.mbCanvas.pause())
|
||||
}
|
||||
if (implementation.name.startsWith('candidate') && input === 'webm') {
|
||||
await page.evaluate(() => {
|
||||
const canvas = window.mbCanvas
|
||||
window.mbReentrantStart = window.mbEvents.length
|
||||
const pause = () => {
|
||||
canvas.events.removeEventListener('play', pause)
|
||||
canvas.pause()
|
||||
}
|
||||
canvas.events.addEventListener('play', pause)
|
||||
})
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.mbEvents.slice(window.mbReentrantStart).filter(item => ['video:play', 'video:pause', 'video:playing'].includes(item.name)).map(item => item.name))).toEqual(['video:play', 'video:pause'])
|
||||
expect(await page.evaluate(() => window.mbCanvas.paused)).toBe(true)
|
||||
await page.evaluate(() => window.mbReentrantPlay = { paused: window.mbCanvas.paused, events: window.mbEvents.slice(window.mbReentrantStart).filter(item => ['video:play', 'video:pause', 'video:playing'].includes(item.name)).map(item => item.name) })
|
||||
}
|
||||
if (input === 'hls') {
|
||||
const tracks = await page.evaluate(async () => {
|
||||
const canvas = window.mbCanvas
|
||||
@@ -205,7 +227,7 @@ for (const implementation of implementations) {
|
||||
if (!canvas)
|
||||
return null
|
||||
const hls = canvas.getM3u8State ? await canvas.getM3u8State().catch(error => ({ error: error.message })) : null
|
||||
return { events: window.mbEvents, actions: window.mbActions, dimensions: [canvas.width, canvas.height, canvas.videoWidth, canvas.videoHeight], currentTime: canvas.currentTime, duration: canvas.duration, error: canvas.error, readyState: canvas.readyState, audioState: canvas.engine.audio.audioContext?.state, frameClockSamples: window.mbFrameClockSamples, tracks: window.mbTrackTransitions, hls: hls && { levels: hls.levels?.map(({ id, height }) => ({ id, height })), audios: hls.audios?.map(({ id, lang }) => ({ id, lang })), error: hls.error }, inputBytes: window.mbInputBytes }
|
||||
return { events: window.mbEvents, actions: window.mbActions, dimensions: [canvas.width, canvas.height, canvas.videoWidth, canvas.videoHeight], currentTime: canvas.currentTime, duration: canvas.duration, error: canvas.error, readyState: canvas.readyState, audioState: canvas.engine.audio.audioContext?.state, frameClockSamples: window.mbFrameClockSamples, tracks: window.mbTrackTransitions, reentrantPlay: window.mbReentrantPlay, hls: hls && { levels: hls.levels?.map(({ id, height }) => ({ id, height })), audios: hls.audios?.map(({ id, lang }) => ({ id, lang })), error: hls.error }, inputBytes: window.mbInputBytes }
|
||||
}).catch(error => ({ error: error.message }))
|
||||
await page.evaluate(() => {
|
||||
window.mbContext = window.mbCanvas?.engine.audio.audioContext
|
||||
@@ -214,7 +236,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: '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.' }) })
|
||||
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 rejection is checked separately before decoder setup; this is not full release acceptance.' }) })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ for (const implementation of implementations) {
|
||||
const canvas = window.art.video
|
||||
return { events: window.mbEvents, actions: window.mbActions, ready: window.art.isReady, dimensions: [canvas.width, canvas.height, canvas.videoWidth, canvas.videoHeight], paused: canvas.paused, currentTime: canvas.currentTime, error: canvas.error, readyState: canvas.readyState, audioState: canvas.engine?.audio?.audioContext?.state }
|
||||
})
|
||||
await testInfo.attach('mediabunny-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), capabilities, outcome, state, scope: 'Two actual published proxies with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No candidate, HLS or AV-sync acceptance.' }) })
|
||||
await testInfo.attach('mediabunny-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), capabilities, outcome, state, scope: 'Identified published or candidate proxy with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No HLS or long-run AV-sync acceptance.' }) })
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Verify the actual coordinator with controlled asynchronous operations.
|
||||
import test from 'node:test'
|
||||
import { mbCandidate, mbEnvironment } from './helpers/mediabunny.js'
|
||||
import { tracklessMp4 } from './helpers/trackless-mp4.js'
|
||||
|
||||
const implementation = await mbCandidate()
|
||||
const flush = () => new Promise(resolve => setImmediate(resolve))
|
||||
const media = new Blob([fs.readFileSync(new URL('./browser/media/pattern.mp4', import.meta.url))])
|
||||
function deferred() {
|
||||
let resolve, reject
|
||||
const promise = new Promise((yes, no) => {
|
||||
resolve = yes
|
||||
reject = no
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
function environment() {
|
||||
const env = mbEnvironment(implementation)
|
||||
for (const name of ['playing', 'seeking', 'seeked', 'waiting', 'loadstart', 'loadeddata', 'canplay', 'canplaythrough', 'durationchange', 'progress', 'ended']) {
|
||||
if (!env.eventNames.includes(name))
|
||||
env.eventNames.push(name)
|
||||
}
|
||||
const canvas = env.factory()(env.art)
|
||||
const engine = canvas.engine
|
||||
const calls = []
|
||||
for (const name of env.eventNames) canvas.events.addEventListener(name, () => calls.push(name))
|
||||
engine.audio.play = async () => calls.push('audio:play')
|
||||
engine.audio.pause = () => calls.push('audio:pause')
|
||||
engine.audio.seek = async time => calls.push(`audio:seek:${time}`)
|
||||
engine.video.start = () => calls.push('video:start')
|
||||
engine.video.stop = () => calls.push('video:stop')
|
||||
engine.video.seek = async time => calls.push(`video:seek:${time}`)
|
||||
env.globals.console = { log() {}, warn() {}, error() {}, debug() {} }
|
||||
return { ...env, canvas, engine, calls }
|
||||
}
|
||||
|
||||
test('MediaBunny coordinator preserves ordinary play, seek-resume and synchronous pause ordering', async () => {
|
||||
const env = environment()
|
||||
await env.engine.play()
|
||||
assert.deepEqual(env.calls, ['audio:play', 'video:start', 'play', 'playing'])
|
||||
env.calls.length = 0
|
||||
await env.engine.seek(3)
|
||||
assert.deepEqual(env.calls, ['seeking', 'waiting', 'audio:pause', 'video:stop', 'pause', 'audio:seek:3', 'video:seek:3', 'seeked', 'audio:play', 'video:start', 'play', 'playing'])
|
||||
env.calls.length = 0
|
||||
assert.equal(env.engine.pause(), undefined)
|
||||
assert.deepEqual(env.calls, ['audio:pause', 'video:stop', 'pause'])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
for (const operation of ['pause', 'destroy', 'source']) {
|
||||
test(`MediaBunny coordinator discards pending play after ${operation}`, async () => {
|
||||
const env = environment()
|
||||
const audio = deferred()
|
||||
env.engine.audio.play = () => audio.promise
|
||||
const playing = env.engine.play()
|
||||
if (operation === 'pause') {
|
||||
env.engine.pause()
|
||||
}
|
||||
else if (operation === 'destroy') {
|
||||
env.art.emit('destroy')
|
||||
}
|
||||
else {
|
||||
env.engine.performLoad = async () => {}
|
||||
await env.engine.load('new.mp4')
|
||||
}
|
||||
env.calls.length = 0
|
||||
audio.resolve()
|
||||
await playing
|
||||
assert.equal(env.engine.paused, true)
|
||||
assert.equal(env.calls.includes('video:start'), false)
|
||||
assert.equal(env.calls.includes('play'), false)
|
||||
assert.equal(env.calls.includes('playing'), false)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny coordinator restores paused state after a play rejection and allows retry', async () => {
|
||||
const env = environment()
|
||||
const error = new Error('resume denied')
|
||||
let attempts = 0
|
||||
env.engine.audio.play = async () => {
|
||||
if (++attempts === 1)
|
||||
throw error
|
||||
}
|
||||
await assert.rejects(env.engine.play(), value => value === error)
|
||||
assert.equal(env.engine.paused, true)
|
||||
await env.engine.play()
|
||||
assert.equal(attempts, 2)
|
||||
assert.equal(env.engine.paused, false)
|
||||
assert.equal(env.calls.filter(name => name === 'video:start').length, 1)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator shares delayed audio resume across play-pause-play and starts video once', async () => {
|
||||
const env = environment()
|
||||
const audio = deferred()
|
||||
let attempts = 0
|
||||
env.engine.audio.play = () => {
|
||||
attempts++
|
||||
return audio.promise
|
||||
}
|
||||
const first = env.engine.play()
|
||||
env.engine.pause()
|
||||
const second = env.engine.play()
|
||||
audio.resolve()
|
||||
await Promise.all([first, second])
|
||||
assert.equal(attempts, 1)
|
||||
assert.equal(env.engine.paused, false)
|
||||
assert.equal(env.calls.filter(name => name === 'video:start').length, 1)
|
||||
assert.equal(env.calls.filter(name => name === 'play').length, 1)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator stops the play event sequence when a listener pauses', async () => {
|
||||
const env = environment()
|
||||
env.canvas.events.addEventListener('play', () => env.engine.pause())
|
||||
await env.engine.play()
|
||||
assert.equal(env.engine.paused, true)
|
||||
assert.equal(env.calls.includes('playing'), false)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator honors an explicit pause while a playing seek is pending', async () => {
|
||||
const env = environment()
|
||||
await env.engine.play()
|
||||
const video = deferred()
|
||||
env.engine.video.seek = () => video.promise
|
||||
const seeking = env.engine.seek(4)
|
||||
env.engine.pause()
|
||||
env.calls.length = 0
|
||||
video.resolve()
|
||||
await seeking
|
||||
assert.equal(env.engine.paused, true)
|
||||
assert.equal(env.engine.seeking, false)
|
||||
assert.deepEqual(env.calls.filter(name => ['seeked', 'video:start', 'play'].includes(name)), ['seeked'])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator keeps the newest seek active when an earlier seek finishes first', async () => {
|
||||
const env = environment()
|
||||
const first = deferred()
|
||||
const second = deferred()
|
||||
env.engine.video.seek = time => time === 1 ? first.promise : second.promise
|
||||
const a = env.engine.seek(1)
|
||||
const b = env.engine.seek(2)
|
||||
first.resolve()
|
||||
await a
|
||||
assert.equal(env.engine.seeking, true)
|
||||
assert.equal(env.calls.includes('seeked'), false)
|
||||
second.resolve()
|
||||
await b
|
||||
assert.equal(env.engine.seeking, false)
|
||||
assert.equal(env.calls.filter(name => name === 'seeked').length, 1)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator drops an older seek result after the newest result and preserves playing intent', async () => {
|
||||
const env = environment()
|
||||
await env.engine.play()
|
||||
const first = deferred()
|
||||
const second = deferred()
|
||||
env.engine.video.seek = time => time === 1 ? first.promise : second.promise
|
||||
const a = env.engine.seek(1)
|
||||
const b = env.engine.seek(2)
|
||||
env.calls.length = 0
|
||||
second.resolve()
|
||||
await b
|
||||
assert.equal(env.engine.paused, false)
|
||||
first.resolve()
|
||||
await a
|
||||
assert.equal(env.calls.filter(name => name === 'seeked').length, 1)
|
||||
assert.equal(env.calls.filter(name => name === 'video:start').length, 1)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
for (const operation of ['destroy', 'source']) {
|
||||
test(`MediaBunny coordinator prevents pending seek completion and resume after ${operation}`, async () => {
|
||||
const env = environment()
|
||||
await env.engine.play()
|
||||
const video = deferred()
|
||||
env.engine.video.seek = () => video.promise
|
||||
const seeking = env.engine.seek(4)
|
||||
if (operation === 'destroy') {
|
||||
env.art.emit('destroy')
|
||||
}
|
||||
else {
|
||||
env.engine.performLoad = async () => {}
|
||||
await env.engine.load('new.mp4')
|
||||
}
|
||||
env.calls.length = 0
|
||||
video.resolve()
|
||||
await seeking
|
||||
assert.equal(env.calls.includes('video:start'), false)
|
||||
assert.equal(env.calls.includes('audio:play'), false)
|
||||
assert.equal(env.calls.includes('seeked'), false)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny coordinator does not continue old seek after a seeking listener requests another position', async () => {
|
||||
const env = environment()
|
||||
let replacement
|
||||
let first = true
|
||||
env.canvas.events.addEventListener('seeking', () => {
|
||||
if (first) {
|
||||
first = false
|
||||
replacement = env.engine.seek(7)
|
||||
}
|
||||
})
|
||||
await env.engine.seek(2)
|
||||
await replacement
|
||||
assert.equal(env.calls.includes('audio:seek:2'), false)
|
||||
assert.equal(env.calls.includes('video:seek:2'), false)
|
||||
assert.equal(env.calls.filter(name => name === 'seeked').length, 1)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator waits for pending seek when play is requested, without a resume deadlock', { timeout: 3000 }, async () => {
|
||||
const env = environment()
|
||||
const video = deferred()
|
||||
env.engine.video.seek = () => video.promise
|
||||
const seeking = env.engine.seek(3)
|
||||
const playing = env.engine.play()
|
||||
await flush()
|
||||
assert.equal(env.calls.includes('audio:play'), false)
|
||||
video.resolve()
|
||||
await Promise.all([seeking, playing])
|
||||
assert.equal(env.engine.paused, false)
|
||||
assert.equal(env.calls.filter(name => name === 'video:start').length, 1)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator resets seeking after an active failure and permits another seek', async () => {
|
||||
const env = environment()
|
||||
const error = new Error('seek failed')
|
||||
let attempts = 0
|
||||
env.engine.video.seek = async () => {
|
||||
if (++attempts === 1)
|
||||
throw error
|
||||
}
|
||||
await assert.rejects(env.engine.seek(2), value => value === error)
|
||||
assert.equal(env.engine.seeking, false)
|
||||
await env.engine.seek(3)
|
||||
assert.equal(attempts, 2)
|
||||
assert.equal(env.engine.seeking, false)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator replay after ended seeks to zero then starts normally', async () => {
|
||||
const env = environment()
|
||||
env.engine.ended = true
|
||||
await env.engine.play()
|
||||
assert.equal(env.engine.ended, false)
|
||||
assert.equal(env.engine.paused, false)
|
||||
assert.deepEqual(env.calls, ['seeking', 'waiting', 'audio:seek:0', 'video:seek:0', 'seeked', 'audio:play', 'video:start', 'play', 'playing'])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator preserves a resume-play rejection from the seek Promise', async () => {
|
||||
const env = environment()
|
||||
await env.engine.play()
|
||||
const failure = new Error('resume failed')
|
||||
env.engine.audio.play = async () => {
|
||||
throw failure
|
||||
}
|
||||
await assert.rejects(env.engine.seek(3), error => error === failure)
|
||||
assert.equal(env.engine.paused, true)
|
||||
assert.equal(env.engine.seeking, false)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator keeps original play rejection when cleanup fails and still stops video', async () => {
|
||||
const env = environment()
|
||||
const failure = new Error('resume failed')
|
||||
env.engine.audio.play = async () => {
|
||||
throw failure
|
||||
}
|
||||
env.engine.audio.pause = () => {
|
||||
throw new Error('cleanup failed')
|
||||
}
|
||||
await assert.rejects(env.engine.play(), error => error === failure)
|
||||
assert.equal(env.calls.includes('video:stop'), true)
|
||||
assert.equal(env.engine.paused, true)
|
||||
env.engine.audio.pause = () => {}
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator attempts both decoder cleanups when audio teardown throws', () => {
|
||||
const env = environment()
|
||||
const failure = new Error('audio teardown failed')
|
||||
let videoDestroyed = 0
|
||||
env.engine.audio.destroy = () => {
|
||||
throw failure
|
||||
}
|
||||
env.engine.video.destroy = () => videoDestroyed++
|
||||
assert.throws(() => env.art.emit('destroy'), error => error === failure)
|
||||
assert.equal(videoDestroyed, 1)
|
||||
assert.equal(env.canvas.events.listeners.size, 0)
|
||||
assert.doesNotThrow(() => env.engine.destroy())
|
||||
assert.equal(videoDestroyed, 1)
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator does not resume if a seeked listener pauses', async () => {
|
||||
const env = environment()
|
||||
await env.engine.play()
|
||||
env.canvas.events.addEventListener('seeked', () => env.engine.pause())
|
||||
env.calls.length = 0
|
||||
await env.engine.seek(3)
|
||||
assert.equal(env.engine.paused, true)
|
||||
assert.equal(env.calls.includes('video:start'), false)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator settles canceled play before audio resume returns and observes its late rejection', async () => {
|
||||
const env = environment()
|
||||
const audio = deferred()
|
||||
env.engine.audio.play = () => audio.promise
|
||||
let settled = false
|
||||
const playing = env.engine.play().then(() => settled = true)
|
||||
env.art.emit('destroy')
|
||||
await flush()
|
||||
assert.equal(settled, true)
|
||||
audio.reject(new Error('late resume failure'))
|
||||
await playing
|
||||
await flush()
|
||||
assert.equal(env.calls.includes('video:start'), false)
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator ignores stale seek failure after a newer seek succeeds', async () => {
|
||||
const env = environment()
|
||||
const first = deferred()
|
||||
env.engine.video.seek = time => time === 1 ? first.promise : Promise.resolve()
|
||||
const a = env.engine.seek(1)
|
||||
await env.engine.seek(2)
|
||||
first.reject(new Error('obsolete seek error'))
|
||||
await a
|
||||
await flush()
|
||||
assert.equal(env.engine.error, null)
|
||||
assert.equal(env.engine.seeking, false)
|
||||
assert.equal(env.calls.includes('error'), false)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny synchronous currentTime setter observes an active seek rejection after one error event', async () => {
|
||||
const env = environment()
|
||||
env.engine.video.seek = async () => {
|
||||
throw new Error('seek rejected')
|
||||
}
|
||||
assert.equal(Reflect.set(env.canvas, 'currentTime', 3), true)
|
||||
await flush()
|
||||
assert.equal(env.engine.error.message, 'seek rejected')
|
||||
assert.equal(env.engine.seeking, false)
|
||||
assert.equal(env.calls.filter(name => name === 'error').length, 1)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
async function loadingEnvironment() {
|
||||
const env = environment()
|
||||
const video = deferred()
|
||||
const audio = deferred()
|
||||
const metadata = {}
|
||||
env.engine.video.load = (media, callback) => {
|
||||
metadata.video = callback
|
||||
return video.promise
|
||||
}
|
||||
env.engine.audio.load = (media, callback) => {
|
||||
metadata.audio = callback
|
||||
return audio.promise
|
||||
}
|
||||
const loading = env.engine.load(media)
|
||||
await flush()
|
||||
assert.equal(typeof metadata.video, 'function')
|
||||
assert.equal(typeof metadata.audio, 'function')
|
||||
return { ...env, video, audio, metadata, loading }
|
||||
}
|
||||
|
||||
test('MediaBunny coordinator waits for both metadata participants and publishes readiness once', async () => {
|
||||
const env = await loadingEnvironment()
|
||||
env.metadata.video()
|
||||
assert.equal(env.calls.includes('loadedmetadata'), false)
|
||||
env.metadata.audio()
|
||||
env.metadata.video()
|
||||
env.metadata.audio()
|
||||
env.video.resolve()
|
||||
env.audio.resolve()
|
||||
await env.loading
|
||||
assert.deepEqual(env.calls.filter(name => ['loadedmetadata', 'durationchange', 'loadeddata', 'canplay', 'canplaythrough'].includes(name)), ['loadedmetadata', 'durationchange', 'loadeddata', 'canplay', 'canplaythrough'])
|
||||
assert.equal(env.engine.readyState, 4)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
for (const event of ['loadedmetadata', 'loadeddata']) {
|
||||
test(`MediaBunny coordinator stops the old readiness sequence when ${event} starts another source`, async () => {
|
||||
const env = await loadingEnvironment()
|
||||
let replacement
|
||||
env.canvas.events.addEventListener(event, () => {
|
||||
env.engine.performLoad = async () => {}
|
||||
replacement = env.engine.load('new.mp4')
|
||||
})
|
||||
env.metadata.video()
|
||||
env.metadata.audio()
|
||||
env.video.resolve()
|
||||
env.audio.resolve()
|
||||
await env.loading
|
||||
await replacement
|
||||
const cursor = env.calls.indexOf(event)
|
||||
assert(cursor >= 0)
|
||||
assert.equal(env.calls.slice(cursor + 1).includes('canplay'), false)
|
||||
if (event === 'loadedmetadata')
|
||||
assert.equal(env.calls.slice(cursor + 1).includes('durationchange'), false)
|
||||
assert.equal(env.engine.readyState, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny coordinator keeps failed decoder setup from becoming ready through a later metadata callback', async () => {
|
||||
const env = await loadingEnvironment()
|
||||
env.video.reject(new Error('decoder unavailable'))
|
||||
await env.loading
|
||||
const cursor = env.calls.length
|
||||
env.metadata.audio()
|
||||
env.audio.resolve()
|
||||
await flush()
|
||||
assert.equal(env.engine.error.message, 'decoder unavailable')
|
||||
assert.equal(env.engine.readyState, 0)
|
||||
assert.deepEqual(env.calls.slice(cursor), [])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny coordinator rejects a real trackless container before decoder or AudioContext setup', async () => {
|
||||
const env = environment()
|
||||
const bytes = tracklessMp4(fs.readFileSync(new URL('./browser/media/pattern.mp4', import.meta.url)))
|
||||
let setups = 0
|
||||
env.engine.video.load = async () => setups++
|
||||
env.engine.audio.load = async () => setups++
|
||||
await env.engine.load(new Blob([bytes]))
|
||||
assert.equal(setups, 0)
|
||||
assert.deepEqual({ ...env.engine.error }, { code: 4, message: 'Input has no audio or video tracks.' })
|
||||
assert.equal(env.engine.readyState, 0)
|
||||
assert.equal(env.calls.includes('loadedmetadata'), false)
|
||||
assert.equal(env.calls.includes('canplay'), false)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
for (const [method, query] of [['selectHlsQuality', 'getVideoTracks'], ['selectHlsAudio', 'getAudioTracks']]) {
|
||||
for (const ending of ['source', 'destroy', 'active']) {
|
||||
test(`MediaBunny ${method} ${ending} query rejection respects source ownership`, async () => {
|
||||
const env = environment()
|
||||
const pending = deferred()
|
||||
const failure = new Error('track query failed')
|
||||
env.engine.input = { [query]: () => pending.promise }
|
||||
env.engine.media = { isHls: true }
|
||||
let replaced = 0
|
||||
env.engine.replaceTracks = async () => replaced++
|
||||
const selecting = env.engine[method]('auto')
|
||||
if (ending === 'source') {
|
||||
env.engine.performLoad = async () => {}
|
||||
await env.engine.load('new.mp4')
|
||||
}
|
||||
else if (ending === 'destroy') {
|
||||
env.art.emit('destroy')
|
||||
}
|
||||
pending.reject(failure)
|
||||
if (ending === 'active')
|
||||
await assert.rejects(selecting, error => error === failure)
|
||||
else await selecting
|
||||
assert.equal(replaced, 0)
|
||||
assert.equal(env.engine.error, null)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test(`MediaBunny ${method} retains active replacement rejection after media identity changes`, async () => {
|
||||
const env = environment()
|
||||
const track = { id: 1, canBePairedWith: () => true }
|
||||
env.engine.input = { [query]: async () => [track] }
|
||||
env.engine.media = { isHls: true, videoTrack: track, audioTrack: track }
|
||||
const failure = new Error('replacement failed')
|
||||
env.engine.replaceTracks = async () => {
|
||||
env.engine.media = { ...env.engine.media }
|
||||
throw failure
|
||||
}
|
||||
await assert.rejects(env.engine[method](1), error => error === failure)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
function replacementEnvironment() {
|
||||
const env = environment()
|
||||
const track = { isLive: async () => false }
|
||||
const input = { getDurationFromMetadata: async () => 8 }
|
||||
env.engine.media = { input, videoTrack: track, audioTrack: null, isHls: true, videoMode: 'auto', audioMode: 'auto', duration: 8 }
|
||||
env.engine.video.load = async () => env.calls.push('video:load')
|
||||
env.engine.audio.load = async () => env.calls.push('audio:load')
|
||||
return env
|
||||
}
|
||||
|
||||
test('MediaBunny track replacement preserves normal pause, readiness, seek and resume ordering', async () => {
|
||||
const env = replacementEnvironment()
|
||||
await env.engine.play()
|
||||
env.calls.length = 0
|
||||
await env.engine.replaceTracks({ videoMode: 'manual' })
|
||||
assert.deepEqual(env.calls, ['audio:pause', 'video:stop', 'pause', 'seeking', 'waiting', 'video:load', 'audio:load', 'video:seek:0', 'audio:seek:0', 'loadedmetadata', 'durationchange', 'progress', 'loadeddata', 'canplay', 'canplaythrough', 'seeked', 'audio:play', 'video:start', 'play', 'playing'])
|
||||
assert.equal(env.engine.readyState, 4)
|
||||
assert.equal(env.engine.media.videoMode, 'manual')
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny track replacement honors pause during delayed decoder setup', async () => {
|
||||
const env = replacementEnvironment()
|
||||
await env.engine.play()
|
||||
const loading = deferred()
|
||||
env.engine.video.load = () => loading.promise
|
||||
const replacing = env.engine.replaceTracks({ videoMode: 'manual' })
|
||||
await flush()
|
||||
env.engine.pause()
|
||||
env.calls.length = 0
|
||||
loading.resolve()
|
||||
await replacing
|
||||
assert.equal(env.engine.paused, true)
|
||||
assert.equal(env.calls.includes('video:start'), false)
|
||||
assert.equal(env.calls.includes('seeked'), true)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
for (const ending of ['source', 'destroy']) {
|
||||
test(`MediaBunny track replacement cannot seek or publish readiness after ${ending}`, async () => {
|
||||
const env = replacementEnvironment()
|
||||
const loading = deferred()
|
||||
env.engine.video.load = () => loading.promise
|
||||
const replacing = env.engine.replaceTracks({ videoMode: 'manual' })
|
||||
await flush()
|
||||
if (ending === 'destroy') {
|
||||
env.art.emit('destroy')
|
||||
}
|
||||
else {
|
||||
env.engine.performLoad = async () => {}
|
||||
await env.engine.load('new.mp4')
|
||||
}
|
||||
env.calls.length = 0
|
||||
loading.resolve()
|
||||
await replacing
|
||||
await flush()
|
||||
assert.deepEqual(env.calls.filter(name => !['waiting', 'loadstart'].includes(name)), [])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user