mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-10 04:36:15 -08:00
refactor(mediabunny): [PKG-MB-05] own video iterators, frame pools and rendering
This commit is contained in:
1 parent
5d0b128483
commit
9d037dcbf9
27 files changed
+3652
-595
No files matched your search
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i=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
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i,r,a,s,n,o,c,l,u,d,h,f,m,p,g,k,y,b,w=e=>{throw TypeError(e)},T=(e,t,i)=>t.has(e)||w("Cannot "+i),S=(e,t,i)=>(T(e,t,"read from private field"),i?i.call(e):t.get(e)),v=(e,t,i)=>t.has(e)?w("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,i),P=(e,t,i,r)=>(T(e,t,"write to private field"),r?r.call(e,i):t.set(e,i),i),I=(e,t,i)=>(T(e,t,"access private method"),i),x=(e,t,i,r)=>({set _(r){P(e,t,r,i)},get _(){return S(e,t,r)}});function C(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 B{constructor(){v(this,e,!1),this.listeners=new Map}addEventListener(t,i){S(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(S(this,e))return;const r=Object.assign(new Event(t),{detail:i});this.listeners.get(t)?.forEach(t=>{S(this,e)||t(r)})}destroy(){P(this,e,!0),this.listeners.clear()}}function E(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const _=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},R=e=>e&&e[e.length-1],F=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)},A=(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=F(e);return 0===(1&t)?-(t>>1):t+1>>1},M=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),N=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),O=new TextDecoder,z=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),L={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},U=z(L),q={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},W=z(q),H={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},V=z(H),j=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,K={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},X=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class ${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 G=/^[0-9a-fA-F]+$/,Q=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),Y=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,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},Z=(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},ee=(e,t,i)=>{const r=Z(e,i(t),i);e.splice(r+1,0,t)},te=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},ie=(e,t)=>{const i=e.indexOf(t);-1!==i&&e.splice(i,1)},re=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},ae=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},se=e=>{throw new Error(`Unexpected value: ${e}`)},ne=(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},oe=(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)},ce=(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}}),le=(e,t,i)=>Math.max(t,Math.min(i,e)),ue="und",de=e=>{const t=Math.round(e);return Math.abs(e/t-1)<10*Number.EPSILON?t:e},he=(e,t)=>Math.round(e/t)*t,fe=(e,t)=>Math.round(e*t)/t,me=(e,t)=>Math.floor(e/t)*t,pe=e=>{let t=0;for(;0!==e;)e&=e-1,t++;return t},ge=/^[a-z]{3}$/,ke=e=>ge.test(e),ye=1e6*(1+Number.EPSILON),be=(e,t)=>{const i={...e,...t};if(e.headers||t.headers){const r=e.headers?we(e.headers):{},a=t.headers?we(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},we=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)}catcLine truncated
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i=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
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i,r,n,s,a,o,l,c,u,d,h,f,m,p,g,k,y,b,T=Object.defineProperty,v=Object.defineProperties,S=Object.getOwnPropertyDescriptors,w=Object.getOwnPropertySymbols,P=Object.prototype.hasOwnProperty,I=Object.prototype.propertyIsEnumerable,x=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),C=e=>{throw TypeError(e)},B=Math.pow,E=(e,t,i)=>t in e?T(e,t,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[t]=i,_=(e,t)=>{for(var i in t||(t={}))P.call(t,i)&&E(e,i,t[i]);if(w)for(var i of w(t))I.call(t,i)&&E(e,i,t[i]);return e},R=(e,t)=>v(e,S(t)),F=(e,t,i)=>t.has(e)||C("Cannot "+i),A=(e,t,i)=>(F(e,t,"read from private field"),i?i.call(e):t.get(e)),D=(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),M=(e,t,i,r)=>(F(e,t,"write to private field"),r?r.call(e,i):t.set(e,i),i),N=(e,t,i)=>(F(e,t,"access private method"),i),O=(e,t,i,r)=>({set _(r){M(e,t,r,i)},get _(){return A(e,t,r)}}),z=(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())}),L=function(e,t){this[0]=e,this[1]=t},U=(e,t,i)=>{var r,n=(e,t,r,s)=>{try{var a=i[e](t),o=(t=a.value)instanceof L,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[x("asyncIterator")]=()=>a,s("next"),s("throw"),s("return"),a},q=e=>{var t,i=e[x("asyncIterator")],r=!1,n={};return null==i?(i=e[x("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 L(new Promise(r=>{var n=i[e](t);n instanceof Object||C("Object expected"),r(n)}),1)}}),n[x("iterator")]=()=>n,t("next"),"throw"in i?t("throw"):n.throw=e=>{throw e},"return"in i&&t("return"),n},W=(e,t,i)=>(t=e[x("asyncIterator")])?t.call(e):(e=e[x("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 H(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(){D(this,e,!1),this.listeners=new Map}addEventListener(t,i){var r;A(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(A(this,e))return;const n=Object.assign(new Event(t),{detail:i});null==(r=this.listeners.get(t))||r.forEach(t=>{A(this,e)||t(n)})}destroy(){M(this,e,!0),this.listeners.clear()}}function j(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const K=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},X=e=>e&&e[e.length-1],$=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)},G=(e,t)=>{const i=t+1,r=Math.floor(Math.log2(i));e.writeBits(r,0),e.writeBits(1,1),e.writeBits(r,i-B(2,r))},Q=e=>{const t=$(e);return 0===(1&t)?-(t>>1):t+1>>1},Y=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),J=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),Z=new TextDecoder,ee=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),te={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},ie=ee(te),re={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},ne=ee(re),se={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},ae=ee(se),oe=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,le={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},ce=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class ue{constructor(){this.currentPromise=Promise.resolve(),this.pending=0}acquire(){return z(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 de=/^[0-9a-fA-F]+$/,he=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),fe=e=>(e=(e=(e=(e=(e=e>>1&1431655765|(1431655765&e)<<1)>>2&858993459|(858993459&e)<<2)>>4&252645135|(252645135&e)<<4)>>8&167Line truncated
|
||||
@@ -11,7 +11,16 @@ 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, _playback;
|
||||
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
||||
var __privateWrapper = (obj, member, setter, getter) => ({
|
||||
set _(value) {
|
||||
__privateSet(obj, member, value, setter);
|
||||
},
|
||||
get _() {
|
||||
return __privateGet(obj, member, getter);
|
||||
}
|
||||
});
|
||||
var _closed, _generation, _iteration, _loading, _createSink, _usedSink, _destroyed, _preparing, _poster, _renderer, _onError, _VideoEngine_instances, current_fn, context_fn, report_fn, release_fn, draw_fn, load_fn, reset_fn, _playback;
|
||||
const $audio = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>';
|
||||
const $quality = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
|
||||
function uniqBy(array, property) {
|
||||
@@ -24741,17 +24750,240 @@ async function preflightRange(url, enabled, events, signal, isCurrent = () => tr
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const closing = /* @__PURE__ */ new WeakMap();
|
||||
const reading = /* @__PURE__ */ new WeakMap();
|
||||
function releaseIterator(iterator) {
|
||||
if (!iterator)
|
||||
return Promise.resolve();
|
||||
const previous = closing.get(iterator);
|
||||
if (previous)
|
||||
return previous;
|
||||
const pending = Promise.resolve().then(() => iterator.return()).then(() => {
|
||||
});
|
||||
closing.set(iterator, pending);
|
||||
return pending;
|
||||
}
|
||||
function readCanvas(iterator) {
|
||||
const previous = reading.get(iterator);
|
||||
if (previous)
|
||||
return previous;
|
||||
const pending = Promise.resolve().then(() => iterator.next()).then((result) => result.value ?? null);
|
||||
reading.set(iterator, pending);
|
||||
const clear = () => {
|
||||
if (reading.get(iterator) === pending)
|
||||
reading.delete(iterator);
|
||||
};
|
||||
pending.then(clear, clear);
|
||||
return pending;
|
||||
}
|
||||
function frameAction(timestamp, time, dropLateFrames, avSyncTolerance, playbackRate) {
|
||||
const tolerance = dropLateFrames ? Math.max(0.06, avSyncTolerance / Math.max(1, playbackRate)) : 0;
|
||||
if (dropLateFrames && timestamp < time - tolerance)
|
||||
return "skip";
|
||||
return timestamp <= time + tolerance ? "draw" : "queue";
|
||||
}
|
||||
class Poster {
|
||||
constructor() {
|
||||
this.image = null;
|
||||
}
|
||||
cancel() {
|
||||
if (!this.image)
|
||||
return;
|
||||
this.image.onload = null;
|
||||
this.image.onerror = null;
|
||||
this.image = null;
|
||||
}
|
||||
draw(source, current, draw, failure) {
|
||||
this.cancel();
|
||||
const image = new Image();
|
||||
this.image = image;
|
||||
image.onload = () => {
|
||||
if (this.image !== image || !current())
|
||||
return;
|
||||
this.cancel();
|
||||
try {
|
||||
draw(image);
|
||||
} catch (error) {
|
||||
failure(error);
|
||||
}
|
||||
};
|
||||
image.onerror = () => {
|
||||
if (this.image === image)
|
||||
this.cancel();
|
||||
};
|
||||
image.src = source;
|
||||
}
|
||||
}
|
||||
class Renderer {
|
||||
constructor(host, ready, draw, failure) {
|
||||
this.host = host;
|
||||
this.ready = ready;
|
||||
this.draw = draw;
|
||||
this.failure = failure;
|
||||
this.stopped = false;
|
||||
this.closed = false;
|
||||
this.scheduled = null;
|
||||
this.fetching = null;
|
||||
}
|
||||
current(id) {
|
||||
return !this.closed && !this.stopped && this.host.asyncId === id;
|
||||
}
|
||||
async update(id) {
|
||||
const iterator = this.host.videoIterator;
|
||||
if (!iterator || !this.ready() || !this.current(id) || this.host.isFetching)
|
||||
return;
|
||||
const request = {};
|
||||
this.fetching = request;
|
||||
this.host.isFetching = true;
|
||||
const current = () => this.current(id) && this.host.videoIterator === iterator;
|
||||
try {
|
||||
while (current()) {
|
||||
const frame = await readCanvas(iterator);
|
||||
if (!current() || !frame)
|
||||
return;
|
||||
const time = this.host.audioClock?.currentTime;
|
||||
if (time === void 0)
|
||||
return;
|
||||
const action = frameAction(frame.timestamp, time, this.host.dropLateFrames, this.host.avSyncTolerance, this.host.playbackRate);
|
||||
if (action === "skip")
|
||||
continue;
|
||||
if (action === "draw") {
|
||||
this.draw(frame);
|
||||
} else {
|
||||
this.host.nextFrame = frame;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (current())
|
||||
throw error;
|
||||
} finally {
|
||||
if (this.fetching === request) {
|
||||
this.fetching = null;
|
||||
this.host.isFetching = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
request(id) {
|
||||
void this.update(id).catch((error) => {
|
||||
if (!this.current(id))
|
||||
return;
|
||||
this.stop();
|
||||
this.report(error);
|
||||
});
|
||||
}
|
||||
report(error) {
|
||||
try {
|
||||
this.failure(error);
|
||||
} catch (failure) {
|
||||
console.warn("MediaBunny video error listener:", failure);
|
||||
}
|
||||
}
|
||||
cancelFrame() {
|
||||
if (this.scheduled === null)
|
||||
return;
|
||||
cancelAnimationFrame(this.scheduled);
|
||||
this.scheduled = null;
|
||||
}
|
||||
schedule(id) {
|
||||
if (!this.current(id))
|
||||
return;
|
||||
this.cancelFrame();
|
||||
const frame = requestAnimationFrame(() => {
|
||||
if (this.scheduled !== frame || !this.current(id))
|
||||
return;
|
||||
this.scheduled = null;
|
||||
try {
|
||||
this.render();
|
||||
} catch (error) {
|
||||
this.stop();
|
||||
this.report(error);
|
||||
}
|
||||
});
|
||||
this.scheduled = frame;
|
||||
this.host.rafId = frame;
|
||||
}
|
||||
render() {
|
||||
const id = this.host.asyncId;
|
||||
const current = () => this.current(id);
|
||||
if (!current() || !this.host.audioClock)
|
||||
return;
|
||||
const time = this.host.audioClock.currentTime;
|
||||
const now = Date.now();
|
||||
if (now - this.host.lastTimeUpdate >= this.host.timeupdateInterval) {
|
||||
this.host.events.emit("timeupdate");
|
||||
if (!current())
|
||||
return;
|
||||
this.host.lastTimeUpdate = now;
|
||||
}
|
||||
if (!this.ready()) {
|
||||
this.schedule(id);
|
||||
return;
|
||||
}
|
||||
if (Number.isFinite(this.host.duration) && time >= this.host.duration) {
|
||||
this.stop();
|
||||
this.host.stalled = false;
|
||||
const ended = this.host.asyncId;
|
||||
publish(this.host.events, () => !this.closed && this.host.asyncId === ended, ["ended", "pause", "canplay"]);
|
||||
return;
|
||||
}
|
||||
if (this.host.nextFrame && this.host.nextFrame.timestamp <= time) {
|
||||
this.draw(this.host.nextFrame);
|
||||
this.host.nextFrame = null;
|
||||
this.request(id);
|
||||
if (this.host.stalled) {
|
||||
publish(this.host.events, current, ["canplay", "playing"]);
|
||||
if (!current())
|
||||
return;
|
||||
this.host.stalled = false;
|
||||
}
|
||||
} else if (!this.host.nextFrame) {
|
||||
this.request(id);
|
||||
if (Number.isFinite(this.host.duration) && time < this.host.duration && !this.host.stalled) {
|
||||
this.host.stalled = true;
|
||||
this.host.events.emit("waiting");
|
||||
}
|
||||
}
|
||||
this.schedule(id);
|
||||
}
|
||||
start(clock) {
|
||||
if (this.closed)
|
||||
return;
|
||||
this.stop();
|
||||
this.stopped = false;
|
||||
this.host.audioClock = clock;
|
||||
this.host.stalled = false;
|
||||
if (!this.host.nextFrame)
|
||||
this.request(this.host.asyncId);
|
||||
this.schedule(this.host.asyncId);
|
||||
}
|
||||
stop() {
|
||||
this.stopped = true;
|
||||
this.host.asyncId++;
|
||||
this.cancelFrame();
|
||||
this.fetching = null;
|
||||
this.host.isFetching = false;
|
||||
}
|
||||
destroy() {
|
||||
if (this.closed)
|
||||
return;
|
||||
this.stop();
|
||||
this.closed = true;
|
||||
}
|
||||
}
|
||||
class VideoEngine {
|
||||
constructor({
|
||||
canvas,
|
||||
ctx,
|
||||
events,
|
||||
timeupdateInterval = 250,
|
||||
avSyncTolerance = 0.12,
|
||||
dropLateFrames = false,
|
||||
poster = "",
|
||||
preflightRange: preflightRange2 = false
|
||||
}) {
|
||||
constructor({ canvas, ctx, events, timeupdateInterval = 250, avSyncTolerance = 0.12, dropLateFrames = false, poster = "", preflightRange: preflightRange2 = false, onError }) {
|
||||
__privateAdd(this, _VideoEngine_instances);
|
||||
__privateAdd(this, _generation, 0);
|
||||
__privateAdd(this, _iteration, 0);
|
||||
__privateAdd(this, _loading, null);
|
||||
__privateAdd(this, _createSink, null);
|
||||
__privateAdd(this, _usedSink, null);
|
||||
__privateAdd(this, _destroyed, false);
|
||||
__privateAdd(this, _preparing, false);
|
||||
__privateAdd(this, _poster, new Poster());
|
||||
__privateAdd(this, _renderer);
|
||||
__privateAdd(this, _onError);
|
||||
this.canvas = canvas;
|
||||
this.ctx = ctx;
|
||||
this.events = events;
|
||||
@@ -24775,33 +25007,171 @@ class VideoEngine {
|
||||
this.playbackRate = 1;
|
||||
this.posterDrawn = false;
|
||||
this.isFetching = false;
|
||||
__privateSet(this, _onError, onError);
|
||||
__privateSet(this, _renderer, new Renderer(this, () => !__privateGet(this, _preparing), (frame) => __privateMethod(this, _VideoEngine_instances, draw_fn).call(this, frame, true), (error) => __privateMethod(this, _VideoEngine_instances, report_fn).call(this, error)));
|
||||
}
|
||||
async preflight(url, signal, isCurrent) {
|
||||
return preflightRange(url, this.preflightRange, this.events, signal, isCurrent);
|
||||
async preflight(source, signal, current) {
|
||||
return preflightRange(source, this.preflightRange, this.events, signal, current);
|
||||
}
|
||||
drawPoster() {
|
||||
if (!this.poster || this.posterDrawn)
|
||||
if (__privateGet(this, _destroyed) || !this.poster || this.posterDrawn)
|
||||
return;
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.canvas.width = img.naturalWidth || this.canvas.width;
|
||||
this.canvas.height = img.naturalHeight || this.canvas.height;
|
||||
this.ctx.drawImage(img, 0, 0, this.canvas.width, this.canvas.height);
|
||||
const generation = __privateGet(this, _generation);
|
||||
__privateGet(this, _poster).draw(this.poster, () => __privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation), (image) => {
|
||||
this.clear();
|
||||
this.canvas.width = image.naturalWidth || this.canvas.width;
|
||||
this.canvas.height = image.naturalHeight || this.canvas.height;
|
||||
__privateMethod(this, _VideoEngine_instances, context_fn).call(this).drawImage(image, 0, 0, this.canvas.width, this.canvas.height);
|
||||
this.posterDrawn = true;
|
||||
};
|
||||
img.src = this.poster;
|
||||
}, (error) => __privateMethod(this, _VideoEngine_instances, report_fn).call(this, error));
|
||||
}
|
||||
async stopIterator() {
|
||||
await this.videoIterator?.return();
|
||||
const iterator = this.videoIterator;
|
||||
this.videoIterator = null;
|
||||
this.nextFrame = null;
|
||||
await releaseIterator(iterator);
|
||||
}
|
||||
clear() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
__privateMethod(this, _VideoEngine_instances, context_fn).call(this).clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
cancelPending() {
|
||||
__privateWrapper(this, _generation)._++;
|
||||
__privateWrapper(this, _iteration)._++;
|
||||
this.stop();
|
||||
__privateMethod(this, _VideoEngine_instances, release_fn).call(this);
|
||||
__privateGet(this, _poster).cancel();
|
||||
__privateSet(this, _preparing, false);
|
||||
__privateSet(this, _loading, null);
|
||||
__privateSet(this, _createSink, null);
|
||||
__privateSet(this, _usedSink, null);
|
||||
this.videoSink = null;
|
||||
this.input = null;
|
||||
}
|
||||
async load(media, onMetadata) {
|
||||
++this.asyncId;
|
||||
await this.stopIterator();
|
||||
if (__privateGet(this, _destroyed))
|
||||
return;
|
||||
this.cancelPending();
|
||||
const generation = __privateGet(this, _generation);
|
||||
const iteration = __privateGet(this, _iteration);
|
||||
__privateSet(this, _preparing, true);
|
||||
const pending = Promise.resolve().then(() => __privateMethod(this, _VideoEngine_instances, load_fn).call(this, media, generation, iteration, onMetadata));
|
||||
__privateSet(this, _loading, pending);
|
||||
try {
|
||||
await pending;
|
||||
} finally {
|
||||
if (__privateGet(this, _loading) === pending)
|
||||
__privateSet(this, _loading, null);
|
||||
}
|
||||
}
|
||||
handleNoVideoTrack() {
|
||||
if (__privateGet(this, _destroyed))
|
||||
return;
|
||||
this.videoSink = null;
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.canvas.width = 0;
|
||||
this.canvas.height = 0;
|
||||
this.clear();
|
||||
this.drawPoster();
|
||||
}
|
||||
async resetIterator(time) {
|
||||
if (__privateGet(this, _destroyed))
|
||||
return;
|
||||
const generation = __privateGet(this, _generation);
|
||||
const iteration = ++__privateWrapper(this, _iteration)._;
|
||||
const loading = __privateGet(this, _loading);
|
||||
this.stop();
|
||||
__privateGet(this, _poster).cancel();
|
||||
__privateSet(this, _preparing, true);
|
||||
try {
|
||||
if (loading)
|
||||
await loading;
|
||||
await __privateMethod(this, _VideoEngine_instances, reset_fn).call(this, time, generation, iteration);
|
||||
} catch (error) {
|
||||
if (__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation) && __privateGet(this, _iteration) === iteration) {
|
||||
__privateMethod(this, _VideoEngine_instances, release_fn).call(this);
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
if (__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation) && __privateGet(this, _iteration) === iteration)
|
||||
__privateSet(this, _preparing, false);
|
||||
}
|
||||
}
|
||||
async updateNextFrame(localId) {
|
||||
await __privateGet(this, _renderer).update(localId);
|
||||
}
|
||||
render() {
|
||||
__privateGet(this, _renderer).render();
|
||||
}
|
||||
start(audio) {
|
||||
__privateGet(this, _renderer).start(audio);
|
||||
}
|
||||
stop() {
|
||||
__privateGet(this, _renderer).stop();
|
||||
}
|
||||
async seek(time) {
|
||||
await this.resetIterator(time);
|
||||
}
|
||||
setPlaybackRate(rate) {
|
||||
this.playbackRate = Math.max(0.1, Number(rate) || 1);
|
||||
}
|
||||
destroy() {
|
||||
if (__privateGet(this, _destroyed))
|
||||
return;
|
||||
__privateSet(this, _destroyed, true);
|
||||
__privateWrapper(this, _generation)._++;
|
||||
__privateGet(this, _renderer).destroy();
|
||||
__privateGet(this, _poster).cancel();
|
||||
__privateMethod(this, _VideoEngine_instances, release_fn).call(this);
|
||||
this.posterDrawn = false;
|
||||
__privateSet(this, _createSink, null);
|
||||
__privateSet(this, _usedSink, null);
|
||||
this.videoSink = null;
|
||||
this.input = null;
|
||||
this.audioClock = null;
|
||||
}
|
||||
}
|
||||
_generation = new WeakMap();
|
||||
_iteration = new WeakMap();
|
||||
_loading = new WeakMap();
|
||||
_createSink = new WeakMap();
|
||||
_usedSink = new WeakMap();
|
||||
_destroyed = new WeakMap();
|
||||
_preparing = new WeakMap();
|
||||
_poster = new WeakMap();
|
||||
_renderer = new WeakMap();
|
||||
_onError = new WeakMap();
|
||||
_VideoEngine_instances = new WeakSet();
|
||||
current_fn = function(generation) {
|
||||
return !__privateGet(this, _destroyed) && __privateGet(this, _generation) === generation;
|
||||
};
|
||||
context_fn = function() {
|
||||
if (!this.ctx)
|
||||
throw new Error("Canvas 2D context is unavailable.");
|
||||
return this.ctx;
|
||||
};
|
||||
report_fn = function(error) {
|
||||
if (__privateGet(this, _destroyed))
|
||||
return;
|
||||
__privateMethod(this, _VideoEngine_instances, release_fn).call(this);
|
||||
if (__privateGet(this, _onError))
|
||||
__privateGet(this, _onError).call(this, error);
|
||||
else
|
||||
this.events.emit("error", error);
|
||||
};
|
||||
release_fn = function() {
|
||||
void this.stopIterator().catch((error) => console.warn("MediaBunny iterator cleanup error:", error));
|
||||
};
|
||||
draw_fn = function(frame, clear) {
|
||||
__privateGet(this, _poster).cancel();
|
||||
if (clear)
|
||||
this.clear();
|
||||
__privateMethod(this, _VideoEngine_instances, context_fn).call(this).drawImage(frame.canvas, 0, 0);
|
||||
};
|
||||
load_fn = async function(media, generation, iteration, onMetadata) {
|
||||
if (!__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation))
|
||||
return;
|
||||
try {
|
||||
this.clear();
|
||||
this.posterDrawn = false;
|
||||
const { input, videoTrack, duration } = media;
|
||||
@@ -24812,139 +25182,61 @@ class VideoEngine {
|
||||
onMetadata?.();
|
||||
return;
|
||||
}
|
||||
if (videoTrack.codec === null || !await videoTrack.canDecode()) {
|
||||
const decodable = videoTrack.codec !== null && await videoTrack.canDecode();
|
||||
if (!__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation))
|
||||
return;
|
||||
if (!decodable) {
|
||||
this.handleNoVideoTrack();
|
||||
onMetadata?.();
|
||||
return;
|
||||
}
|
||||
const transparent = await videoTrack.canBeTransparent();
|
||||
this.videoSink = new CanvasSink(videoTrack, {
|
||||
poolSize: 2,
|
||||
fit: "contain",
|
||||
alpha: transparent
|
||||
});
|
||||
if (!__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation))
|
||||
return;
|
||||
__privateSet(this, _createSink, () => new CanvasSink(videoTrack, { poolSize: 2, fit: "contain", alpha: transparent }));
|
||||
this.videoSink = __privateGet(this, _createSink).call(this);
|
||||
this.width = videoTrack.displayWidth;
|
||||
this.height = videoTrack.displayHeight;
|
||||
this.canvas.width = this.width;
|
||||
this.canvas.height = this.height;
|
||||
onMetadata?.();
|
||||
await this.resetIterator(0);
|
||||
if (__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation))
|
||||
await __privateMethod(this, _VideoEngine_instances, reset_fn).call(this, 0, generation, iteration);
|
||||
} catch (error) {
|
||||
if (__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation)) {
|
||||
__privateMethod(this, _VideoEngine_instances, release_fn).call(this);
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
if (__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation) && __privateGet(this, _iteration) === iteration)
|
||||
__privateSet(this, _preparing, false);
|
||||
}
|
||||
handleNoVideoTrack() {
|
||||
this.videoSink = null;
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.canvas.width = 0;
|
||||
this.canvas.height = 0;
|
||||
this.clear();
|
||||
};
|
||||
reset_fn = async function(time, generation, iteration) {
|
||||
if (!__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation) || __privateGet(this, _iteration) !== iteration)
|
||||
return;
|
||||
__privateMethod(this, _VideoEngine_instances, release_fn).call(this);
|
||||
if (__privateGet(this, _createSink) && this.videoSink === __privateGet(this, _usedSink))
|
||||
this.videoSink = __privateGet(this, _createSink).call(this);
|
||||
const sink = this.videoSink;
|
||||
if (!sink || !__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation))
|
||||
return;
|
||||
__privateSet(this, _usedSink, sink);
|
||||
const iterator = sink.canvases(time);
|
||||
this.videoIterator = iterator;
|
||||
const current = () => __privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation) && __privateGet(this, _iteration) === iteration && this.videoIterator === iterator;
|
||||
const first = await readCanvas(iterator);
|
||||
if (!current())
|
||||
return;
|
||||
const second = await readCanvas(iterator);
|
||||
if (!current())
|
||||
return;
|
||||
this.nextFrame = second;
|
||||
if (first)
|
||||
__privateMethod(this, _VideoEngine_instances, draw_fn).call(this, first, false);
|
||||
else
|
||||
this.drawPoster();
|
||||
}
|
||||
async resetIterator(time) {
|
||||
await this.stopIterator();
|
||||
if (!this.videoSink)
|
||||
return;
|
||||
this.videoIterator = this.videoSink.canvases(time);
|
||||
const first = (await this.videoIterator.next()).value ?? null;
|
||||
const second = (await this.videoIterator.next()).value ?? null;
|
||||
this.nextFrame = second;
|
||||
if (first) {
|
||||
this.ctx.drawImage(first.canvas, 0, 0);
|
||||
} else {
|
||||
this.drawPoster();
|
||||
}
|
||||
}
|
||||
async updateNextFrame(localId) {
|
||||
if (!this.videoIterator || this.isFetching)
|
||||
return;
|
||||
this.isFetching = true;
|
||||
try {
|
||||
while (true) {
|
||||
const frame = (await this.videoIterator.next()).value ?? null;
|
||||
if (!frame || localId !== this.asyncId)
|
||||
return;
|
||||
const t = this.audioClock.currentTime;
|
||||
const tolerance = this.dropLateFrames ? Math.max(0.06, this.avSyncTolerance / Math.max(1, this.playbackRate)) : 0;
|
||||
if (this.dropLateFrames && frame.timestamp < t - tolerance) {
|
||||
continue;
|
||||
}
|
||||
if (frame.timestamp <= t + tolerance) {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.ctx.drawImage(frame.canvas, 0, 0);
|
||||
if (!this.dropLateFrames && frame.timestamp > t) {
|
||||
this.nextFrame = null;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.nextFrame = frame;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.isFetching = false;
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (!this.audioClock)
|
||||
return;
|
||||
const t = this.audioClock.currentTime;
|
||||
const now = Date.now();
|
||||
if (now - this.lastTimeUpdate >= this.timeupdateInterval) {
|
||||
this.events.emit("timeupdate");
|
||||
this.lastTimeUpdate = now;
|
||||
}
|
||||
if (Number.isFinite(this.duration) && t >= this.duration) {
|
||||
this.stop();
|
||||
this.stalled = false;
|
||||
this.events.emit("ended");
|
||||
this.events.emit("pause");
|
||||
this.events.emit("canplay");
|
||||
return;
|
||||
}
|
||||
if (this.nextFrame && this.nextFrame.timestamp <= t) {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.ctx.drawImage(this.nextFrame.canvas, 0, 0);
|
||||
this.nextFrame = null;
|
||||
this.updateNextFrame(this.asyncId);
|
||||
if (this.stalled) {
|
||||
this.events.emit("canplay");
|
||||
this.events.emit("playing");
|
||||
this.stalled = false;
|
||||
}
|
||||
} else if (!this.nextFrame) {
|
||||
this.updateNextFrame(this.asyncId);
|
||||
if (!this.nextFrame && Number.isFinite(this.duration) && t < this.duration && !this.stalled) {
|
||||
this.stalled = true;
|
||||
this.events.emit("waiting");
|
||||
}
|
||||
}
|
||||
this.rafId = requestAnimationFrame(() => this.render());
|
||||
}
|
||||
start(audioEngine) {
|
||||
this.audioClock = audioEngine;
|
||||
this.asyncId++;
|
||||
this.stalled = false;
|
||||
this.updateNextFrame(this.asyncId);
|
||||
this.rafId = requestAnimationFrame(() => this.render());
|
||||
}
|
||||
stop() {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
}
|
||||
async seek(time) {
|
||||
this.asyncId++;
|
||||
await this.resetIterator(time);
|
||||
}
|
||||
setPlaybackRate(rate) {
|
||||
this.playbackRate = Math.max(0.1, Number(rate) || 1);
|
||||
}
|
||||
destroy() {
|
||||
this.asyncId++;
|
||||
this.stop();
|
||||
this.stopIterator();
|
||||
this.posterDrawn = false;
|
||||
this.videoSink = null;
|
||||
this.input = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
class MediaBunnyEngine {
|
||||
constructor({ canvas, ctx, events, option = {} }) {
|
||||
__privateAdd(this, _playback);
|
||||
@@ -24959,7 +25251,15 @@ class MediaBunnyEngine {
|
||||
avSyncTolerance: option.avSyncTolerance ?? 0.12,
|
||||
dropLateFrames: option.dropLateFrames ?? false,
|
||||
poster: option.poster ?? "",
|
||||
preflightRange: option.preflightRange ?? false
|
||||
preflightRange: option.preflightRange ?? false,
|
||||
onError: (error) => {
|
||||
try {
|
||||
this.pause();
|
||||
} catch (failure) {
|
||||
console.warn("MediaBunny video failure cleanup:", failure);
|
||||
}
|
||||
this.reportError(error);
|
||||
}
|
||||
});
|
||||
this.paused = true;
|
||||
this.ended = false;
|
||||
@@ -24986,6 +25286,7 @@ class MediaBunnyEngine {
|
||||
return;
|
||||
const id = ++this.loadSeq;
|
||||
__privateGet(this, _playback).invalidate();
|
||||
this.video.cancelPending();
|
||||
this.loadSession?.cancel();
|
||||
if (id !== this.loadSeq || this.destroyed)
|
||||
return;
|
||||
@@ -25009,6 +25310,7 @@ class MediaBunnyEngine {
|
||||
return;
|
||||
const failed = ++this.loadSeq;
|
||||
__privateGet(this, _playback).invalidate();
|
||||
this.video.cancelPending();
|
||||
session.cancel();
|
||||
if (failed !== this.loadSeq || this.destroyed)
|
||||
return;
|
||||
|
||||
+2
-2
@@ -51,7 +51,7 @@
|
||||
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
|
||||
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
|
||||
"typecheck": "node scripts/typecheck.mjs",
|
||||
"test:unit": "node --test test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
|
||||
"test:unit": "node --test test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
|
||||
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
|
||||
"test": "yarn test:node && yarn test:baseline",
|
||||
"test:browser": "playwright test",
|
||||
@@ -76,7 +76,7 @@
|
||||
"test:canvas-types-package": "node refactor/scripts/canvas-package-types.mjs",
|
||||
"test:dpip": "node --test test/dpip.test.js test/dpip-lifecycle.test.js",
|
||||
"test:dpip-types-package": "node refactor/scripts/dpip-package-types.mjs",
|
||||
"test:mediabunny": "node --test test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js"
|
||||
"test:mediabunny": "node --test test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -19,18 +19,22 @@ declarations, the factory signature, canvas forwarding, or package entrypoints.
|
||||
| `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 |
|
||||
| `VideoEngine.ts` | Video capability/setup, source and seek ownership, first frames and terminal cleanup |
|
||||
| `video-frames.ts` | Shared pending iterator reads, idempotent iterator release and existing frame timing policy |
|
||||
| `video-renderer.ts` | Owned RAF, stale frame rejection, clock-driven draw/stall/end events |
|
||||
| `video-poster.ts` | Per-request image callbacks, source guards and handler cleanup |
|
||||
| `AudioEngine.js` | Remaining Web Audio clock, audio decoding and scheduling implementation |
|
||||
| `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` / `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 |
|
||||
| `AudioEngine.d.ts` | Temporary boundary for the remaining JS audio engine, removed in MB-06 |
|
||||
| `m3u8.js` | Current ArtPlayer control/setting integration |
|
||||
|
||||
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.
|
||||
The twenty production TypeScript modules use strict checking with `skipLibCheck: false`
|
||||
and no ambient Node types. The coordinator and video engine are checked implementations.
|
||||
The adjacent audio declaration is a temporary boundary, not checking of its JavaScript
|
||||
implementation. Remaining JavaScript is scheduled in PKG-MB-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
|
||||
@@ -82,7 +86,7 @@ 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.
|
||||
video frame ownership is detailed below; remaining audio work belongs to MB-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,
|
||||
@@ -99,6 +103,43 @@ This corrects the old false-ready state; normal valid input event order is prese
|
||||
Existing-track unsupported-codec handling remains a decoder capability task, not evidence
|
||||
that every capability failure has been fixed.
|
||||
|
||||
## Video frame ownership
|
||||
|
||||
VideoEngine keeps source/load generation separate from seek iteration and renderer activity.
|
||||
A new source calls cancelPending before track selection starts, so an old seek cannot draw
|
||||
while the new source is still parsing. Seek during capability detection waits for that load
|
||||
without canceling its metadata; only the latest seek may install or draw its frames.
|
||||
Ordinary paused seek still displays its first target frame and queues the second.
|
||||
|
||||
Iterator detachment is synchronous. The old return Promise is observed without blocking a
|
||||
new load on obsolete reads; repeated release calls return the same cleanup Promise. Late
|
||||
completion cannot clear the current iterator or its fetching flag. Stop/start shares an
|
||||
already pending next() for the same iterator instead of issuing concurrent reads. Pause
|
||||
invalidates render work; it does not cancel a legitimate paused seek's target frame.
|
||||
|
||||
Each seek after the initial iterator creates a fresh CanvasSink with its own two-canvas pool.
|
||||
Concurrent obsolete decoders can otherwise overwrite the pixels of a queued frame even when
|
||||
the proxy discards their returned objects. Sink identity is internal and may change on seek;
|
||||
public canvas identity stays stable. Tests intercept sink prototypes to follow new instances.
|
||||
CanvasSink retains poolSize 2, contain fit and track transparency. Its implementation closes
|
||||
VideoSample after drawing into the pooled canvas. The proxy releases iterators and references;
|
||||
WrappedCanvas is not a VideoFrame and must not receive close(). Default dropLateFrames remains
|
||||
false, with the same tolerance/rate formula when enabled. Start preserves the queued second
|
||||
frame until it is consumed instead of replacing it through premature prefetch. There is no
|
||||
added frame copy.
|
||||
|
||||
Each renderer owns one RAF, including ID zero. Stop/destroy and event-listener reentry make
|
||||
queued callbacks inert; an instance that never scheduled a frame cannot cancel another
|
||||
instance's frame. Normal timeupdate, stall recovery and ended ordering is preserved, while
|
||||
reentrant pause/source/destroy/restart stops the obsolete event sequence. Poster image
|
||||
callbacks have source and request identity and are detached on replacement, draw or destroy.
|
||||
|
||||
Background decoder failure stops rendering and reports one media error through the coordinator,
|
||||
even if audio cleanup fails. Iterator cleanup failures are observed and warned; explicit
|
||||
stopIterator still rejects. Missing 2D context fails at use with a descriptive error, rather
|
||||
than becoming falsely ready. Capability handling for existing undecodable tracks remains
|
||||
under MB-CAP-01/MB-READY-01; the current no-video fallback is preserved for that separate review.
|
||||
|
||||
## Types and dependencies
|
||||
|
||||
The locked runtime SDK remains MediaBunny 1.56.1. Its pinned WebCodecs declaration
|
||||
@@ -118,6 +159,7 @@ node node_modules/typescript/bin/tsc -p packages/artplayer-proxy-mediabunny/tsco
|
||||
yarn build artplayer-proxy-mediabunny
|
||||
yarn test:browser test/browser/mediabunny-inputs.spec.js test/browser/mediabunny.spec.js test/browser/mediabunny-load.spec.js
|
||||
yarn test:browser test/browser/mediabunny-shim.spec.js
|
||||
yarn test:browser test/browser/mediabunny-video.spec.js
|
||||
```
|
||||
|
||||
Node tests use actual SDK parsing and controlled lifecycle interleavings. Browser tests
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i=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
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i,r,a,s,n,o,c,l,u,d,h,f,m,p,g,k,y,b,w=e=>{throw TypeError(e)},T=(e,t,i)=>t.has(e)||w("Cannot "+i),S=(e,t,i)=>(T(e,t,"read from private field"),i?i.call(e):t.get(e)),v=(e,t,i)=>t.has(e)?w("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,i),P=(e,t,i,r)=>(T(e,t,"write to private field"),r?r.call(e,i):t.set(e,i),i),I=(e,t,i)=>(T(e,t,"access private method"),i),x=(e,t,i,r)=>({set _(r){P(e,t,r,i)},get _(){return S(e,t,r)}});function C(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 B{constructor(){v(this,e,!1),this.listeners=new Map}addEventListener(t,i){S(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(S(this,e))return;const r=Object.assign(new Event(t),{detail:i});this.listeners.get(t)?.forEach(t=>{S(this,e)||t(r)})}destroy(){P(this,e,!0),this.listeners.clear()}}function E(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const _=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},R=e=>e&&e[e.length-1],F=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)},A=(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=F(e);return 0===(1&t)?-(t>>1):t+1>>1},M=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),N=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),O=new TextDecoder,z=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),L={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},U=z(L),q={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},W=z(q),H={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},V=z(H),j=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,K={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},X=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class ${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 G=/^[0-9a-fA-F]+$/,Q=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),Y=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,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},Z=(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},ee=(e,t,i)=>{const r=Z(e,i(t),i);e.splice(r+1,0,t)},te=()=>{let e,t;return{promise:new Promise((i,r)=>{e=i,t=r}),resolve:e,reject:t}},ie=(e,t)=>{const i=e.indexOf(t);-1!==i&&e.splice(i,1)},re=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return e[i]},ae=(e,t)=>{for(let i=e.length-1;i>=0;i--)if(t(e[i]))return i;return-1},se=e=>{throw new Error(`Unexpected value: ${e}`)},ne=(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},oe=(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)},ce=(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}}),le=(e,t,i)=>Math.max(t,Math.min(i,e)),ue="und",de=e=>{const t=Math.round(e);return Math.abs(e/t-1)<10*Number.EPSILON?t:e},he=(e,t)=>Math.round(e/t)*t,fe=(e,t)=>Math.round(e*t)/t,me=(e,t)=>Math.floor(e/t)*t,pe=e=>{let t=0;for(;0!==e;)e&=e-1,t++;return t},ge=/^[a-z]{3}$/,ke=e=>ge.test(e),ye=1e6*(1+Number.EPSILON),be=(e,t)=>{const i={...e,...t};if(e.headers||t.headers){const r=e.headers?we(e.headers):{},a=t.headers?we(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},we=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)}catcLine truncated
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
* (c) 2017-2026 Harvey Zhao
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i=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
|
||||
!function(e,t){"object"===typeof exports&&"undefined"!==typeof module?module.exports=t():"function"==typeof define&&define.amd?(e.artplayerProxyMediabunny=t(),define(function(){return e.artplayerProxyMediabunny})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerProxyMediabunny=t()}(this,function(){"use strict";var e,t,i,r,n,s,a,o,l,c,u,d,h,f,m,p,g,k,y,b,T=Object.defineProperty,v=Object.defineProperties,S=Object.getOwnPropertyDescriptors,w=Object.getOwnPropertySymbols,P=Object.prototype.hasOwnProperty,I=Object.prototype.propertyIsEnumerable,x=(e,t)=>(t=Symbol[e])?t:Symbol.for("Symbol."+e),C=e=>{throw TypeError(e)},B=Math.pow,E=(e,t,i)=>t in e?T(e,t,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[t]=i,_=(e,t)=>{for(var i in t||(t={}))P.call(t,i)&&E(e,i,t[i]);if(w)for(var i of w(t))I.call(t,i)&&E(e,i,t[i]);return e},R=(e,t)=>v(e,S(t)),F=(e,t,i)=>t.has(e)||C("Cannot "+i),A=(e,t,i)=>(F(e,t,"read from private field"),i?i.call(e):t.get(e)),D=(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),M=(e,t,i,r)=>(F(e,t,"write to private field"),r?r.call(e,i):t.set(e,i),i),N=(e,t,i)=>(F(e,t,"access private method"),i),O=(e,t,i,r)=>({set _(r){M(e,t,r,i)},get _(){return A(e,t,r)}}),z=(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())}),L=function(e,t){this[0]=e,this[1]=t},U=(e,t,i)=>{var r,n=(e,t,r,s)=>{try{var a=i[e](t),o=(t=a.value)instanceof L,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[x("asyncIterator")]=()=>a,s("next"),s("throw"),s("return"),a},q=e=>{var t,i=e[x("asyncIterator")],r=!1,n={};return null==i?(i=e[x("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 L(new Promise(r=>{var n=i[e](t);n instanceof Object||C("Object expected"),r(n)}),1)}}),n[x("iterator")]=()=>n,t("next"),"throw"in i?t("throw"):n.throw=e=>{throw e},"return"in i&&t("return"),n},W=(e,t,i)=>(t=e[x("asyncIterator")])?t.call(e):(e=e[x("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 H(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(){D(this,e,!1),this.listeners=new Map}addEventListener(t,i){var r;A(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(A(this,e))return;const n=Object.assign(new Event(t),{detail:i});null==(r=this.listeners.get(t))||r.forEach(t=>{A(this,e)||t(n)})}destroy(){M(this,e,!0),this.listeners.clear()}}function j(e){if(!e)throw new Error("Assertion failed.")}e=new WeakMap;const K=e=>{const t=(e%360+360)%360;if(0===t||90===t||180===t||270===t)return t;throw new Error(`Invalid rotation ${e}.`)},X=e=>e&&e[e.length-1],$=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)},G=(e,t)=>{const i=t+1,r=Math.floor(Math.log2(i));e.writeBits(r,0),e.writeBits(1,1),e.writeBits(r,i-B(2,r))},Q=e=>{const t=$(e);return 0===(1&t)?-(t>>1):t+1>>1},Y=e=>e.constructor===Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e),J=e=>e.constructor===DataView?e:ArrayBuffer.isView(e)?new DataView(e.buffer,e.byteOffset,e.byteLength):new DataView(e),Z=new TextDecoder,ee=e=>Object.fromEntries(Object.entries(e).map(([e,t])=>[t,e])),te={bt709:1,bt470bg:5,smpte170m:6,bt2020:9,smpte432:12},ie=ee(te),re={bt709:1,smpte170m:6,linear:8,"iec61966-2-1":13,pq:16,hlg:18},ne=ee(re),se={rgb:0,bt709:1,bt470bg:5,smpte170m:6,"bt2020-ncl":9},ae=ee(se),oe=e=>!!e&&!!e.primaries&&!!e.transfer&&!!e.matrix&&void 0!==e.fullRange,le={primaries:void 0,transfer:void 0,matrix:void 0,fullRange:void 0},ce=e=>e instanceof ArrayBuffer||"undefined"!==typeof SharedArrayBuffer&&e instanceof SharedArrayBuffer||ArrayBuffer.isView(e);class ue{constructor(){this.currentPromise=Promise.resolve(),this.pending=0}acquire(){return z(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 de=/^[0-9a-fA-F]+$/,he=e=>[...e].map(e=>e.toString(16).padStart(2,"0")).join(""),fe=e=>(e=(e=(e=(e=(e=e>>1&1431655765|(1431655765&e)<<1)>>2&858993459|(858993459&e)<<2)>>4&252645135|(252645135&e)<<4)>>8&167Line truncated
|
||||
+450
-148
@@ -11,7 +11,16 @@ 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, _playback;
|
||||
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
||||
var __privateWrapper = (obj, member, setter, getter) => ({
|
||||
set _(value) {
|
||||
__privateSet(obj, member, value, setter);
|
||||
},
|
||||
get _() {
|
||||
return __privateGet(obj, member, getter);
|
||||
}
|
||||
});
|
||||
var _closed, _generation, _iteration, _loading, _createSink, _usedSink, _destroyed, _preparing, _poster, _renderer, _onError, _VideoEngine_instances, current_fn, context_fn, report_fn, release_fn, draw_fn, load_fn, reset_fn, _playback;
|
||||
const $audio = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>';
|
||||
const $quality = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="18"><path fill="#fff" d="M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6l96 0 32 0 208 0c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>';
|
||||
function uniqBy(array, property) {
|
||||
@@ -24741,17 +24750,240 @@ async function preflightRange(url, enabled, events, signal, isCurrent = () => tr
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const closing = /* @__PURE__ */ new WeakMap();
|
||||
const reading = /* @__PURE__ */ new WeakMap();
|
||||
function releaseIterator(iterator) {
|
||||
if (!iterator)
|
||||
return Promise.resolve();
|
||||
const previous = closing.get(iterator);
|
||||
if (previous)
|
||||
return previous;
|
||||
const pending = Promise.resolve().then(() => iterator.return()).then(() => {
|
||||
});
|
||||
closing.set(iterator, pending);
|
||||
return pending;
|
||||
}
|
||||
function readCanvas(iterator) {
|
||||
const previous = reading.get(iterator);
|
||||
if (previous)
|
||||
return previous;
|
||||
const pending = Promise.resolve().then(() => iterator.next()).then((result) => result.value ?? null);
|
||||
reading.set(iterator, pending);
|
||||
const clear = () => {
|
||||
if (reading.get(iterator) === pending)
|
||||
reading.delete(iterator);
|
||||
};
|
||||
pending.then(clear, clear);
|
||||
return pending;
|
||||
}
|
||||
function frameAction(timestamp, time, dropLateFrames, avSyncTolerance, playbackRate) {
|
||||
const tolerance = dropLateFrames ? Math.max(0.06, avSyncTolerance / Math.max(1, playbackRate)) : 0;
|
||||
if (dropLateFrames && timestamp < time - tolerance)
|
||||
return "skip";
|
||||
return timestamp <= time + tolerance ? "draw" : "queue";
|
||||
}
|
||||
class Poster {
|
||||
constructor() {
|
||||
this.image = null;
|
||||
}
|
||||
cancel() {
|
||||
if (!this.image)
|
||||
return;
|
||||
this.image.onload = null;
|
||||
this.image.onerror = null;
|
||||
this.image = null;
|
||||
}
|
||||
draw(source, current, draw, failure) {
|
||||
this.cancel();
|
||||
const image = new Image();
|
||||
this.image = image;
|
||||
image.onload = () => {
|
||||
if (this.image !== image || !current())
|
||||
return;
|
||||
this.cancel();
|
||||
try {
|
||||
draw(image);
|
||||
} catch (error) {
|
||||
failure(error);
|
||||
}
|
||||
};
|
||||
image.onerror = () => {
|
||||
if (this.image === image)
|
||||
this.cancel();
|
||||
};
|
||||
image.src = source;
|
||||
}
|
||||
}
|
||||
class Renderer {
|
||||
constructor(host, ready, draw, failure) {
|
||||
this.host = host;
|
||||
this.ready = ready;
|
||||
this.draw = draw;
|
||||
this.failure = failure;
|
||||
this.stopped = false;
|
||||
this.closed = false;
|
||||
this.scheduled = null;
|
||||
this.fetching = null;
|
||||
}
|
||||
current(id) {
|
||||
return !this.closed && !this.stopped && this.host.asyncId === id;
|
||||
}
|
||||
async update(id) {
|
||||
const iterator = this.host.videoIterator;
|
||||
if (!iterator || !this.ready() || !this.current(id) || this.host.isFetching)
|
||||
return;
|
||||
const request = {};
|
||||
this.fetching = request;
|
||||
this.host.isFetching = true;
|
||||
const current = () => this.current(id) && this.host.videoIterator === iterator;
|
||||
try {
|
||||
while (current()) {
|
||||
const frame = await readCanvas(iterator);
|
||||
if (!current() || !frame)
|
||||
return;
|
||||
const time = this.host.audioClock?.currentTime;
|
||||
if (time === void 0)
|
||||
return;
|
||||
const action = frameAction(frame.timestamp, time, this.host.dropLateFrames, this.host.avSyncTolerance, this.host.playbackRate);
|
||||
if (action === "skip")
|
||||
continue;
|
||||
if (action === "draw") {
|
||||
this.draw(frame);
|
||||
} else {
|
||||
this.host.nextFrame = frame;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (current())
|
||||
throw error;
|
||||
} finally {
|
||||
if (this.fetching === request) {
|
||||
this.fetching = null;
|
||||
this.host.isFetching = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
request(id) {
|
||||
void this.update(id).catch((error) => {
|
||||
if (!this.current(id))
|
||||
return;
|
||||
this.stop();
|
||||
this.report(error);
|
||||
});
|
||||
}
|
||||
report(error) {
|
||||
try {
|
||||
this.failure(error);
|
||||
} catch (failure) {
|
||||
console.warn("MediaBunny video error listener:", failure);
|
||||
}
|
||||
}
|
||||
cancelFrame() {
|
||||
if (this.scheduled === null)
|
||||
return;
|
||||
cancelAnimationFrame(this.scheduled);
|
||||
this.scheduled = null;
|
||||
}
|
||||
schedule(id) {
|
||||
if (!this.current(id))
|
||||
return;
|
||||
this.cancelFrame();
|
||||
const frame = requestAnimationFrame(() => {
|
||||
if (this.scheduled !== frame || !this.current(id))
|
||||
return;
|
||||
this.scheduled = null;
|
||||
try {
|
||||
this.render();
|
||||
} catch (error) {
|
||||
this.stop();
|
||||
this.report(error);
|
||||
}
|
||||
});
|
||||
this.scheduled = frame;
|
||||
this.host.rafId = frame;
|
||||
}
|
||||
render() {
|
||||
const id = this.host.asyncId;
|
||||
const current = () => this.current(id);
|
||||
if (!current() || !this.host.audioClock)
|
||||
return;
|
||||
const time = this.host.audioClock.currentTime;
|
||||
const now = Date.now();
|
||||
if (now - this.host.lastTimeUpdate >= this.host.timeupdateInterval) {
|
||||
this.host.events.emit("timeupdate");
|
||||
if (!current())
|
||||
return;
|
||||
this.host.lastTimeUpdate = now;
|
||||
}
|
||||
if (!this.ready()) {
|
||||
this.schedule(id);
|
||||
return;
|
||||
}
|
||||
if (Number.isFinite(this.host.duration) && time >= this.host.duration) {
|
||||
this.stop();
|
||||
this.host.stalled = false;
|
||||
const ended = this.host.asyncId;
|
||||
publish(this.host.events, () => !this.closed && this.host.asyncId === ended, ["ended", "pause", "canplay"]);
|
||||
return;
|
||||
}
|
||||
if (this.host.nextFrame && this.host.nextFrame.timestamp <= time) {
|
||||
this.draw(this.host.nextFrame);
|
||||
this.host.nextFrame = null;
|
||||
this.request(id);
|
||||
if (this.host.stalled) {
|
||||
publish(this.host.events, current, ["canplay", "playing"]);
|
||||
if (!current())
|
||||
return;
|
||||
this.host.stalled = false;
|
||||
}
|
||||
} else if (!this.host.nextFrame) {
|
||||
this.request(id);
|
||||
if (Number.isFinite(this.host.duration) && time < this.host.duration && !this.host.stalled) {
|
||||
this.host.stalled = true;
|
||||
this.host.events.emit("waiting");
|
||||
}
|
||||
}
|
||||
this.schedule(id);
|
||||
}
|
||||
start(clock) {
|
||||
if (this.closed)
|
||||
return;
|
||||
this.stop();
|
||||
this.stopped = false;
|
||||
this.host.audioClock = clock;
|
||||
this.host.stalled = false;
|
||||
if (!this.host.nextFrame)
|
||||
this.request(this.host.asyncId);
|
||||
this.schedule(this.host.asyncId);
|
||||
}
|
||||
stop() {
|
||||
this.stopped = true;
|
||||
this.host.asyncId++;
|
||||
this.cancelFrame();
|
||||
this.fetching = null;
|
||||
this.host.isFetching = false;
|
||||
}
|
||||
destroy() {
|
||||
if (this.closed)
|
||||
return;
|
||||
this.stop();
|
||||
this.closed = true;
|
||||
}
|
||||
}
|
||||
class VideoEngine {
|
||||
constructor({
|
||||
canvas,
|
||||
ctx,
|
||||
events,
|
||||
timeupdateInterval = 250,
|
||||
avSyncTolerance = 0.12,
|
||||
dropLateFrames = false,
|
||||
poster = "",
|
||||
preflightRange: preflightRange2 = false
|
||||
}) {
|
||||
constructor({ canvas, ctx, events, timeupdateInterval = 250, avSyncTolerance = 0.12, dropLateFrames = false, poster = "", preflightRange: preflightRange2 = false, onError }) {
|
||||
__privateAdd(this, _VideoEngine_instances);
|
||||
__privateAdd(this, _generation, 0);
|
||||
__privateAdd(this, _iteration, 0);
|
||||
__privateAdd(this, _loading, null);
|
||||
__privateAdd(this, _createSink, null);
|
||||
__privateAdd(this, _usedSink, null);
|
||||
__privateAdd(this, _destroyed, false);
|
||||
__privateAdd(this, _preparing, false);
|
||||
__privateAdd(this, _poster, new Poster());
|
||||
__privateAdd(this, _renderer);
|
||||
__privateAdd(this, _onError);
|
||||
this.canvas = canvas;
|
||||
this.ctx = ctx;
|
||||
this.events = events;
|
||||
@@ -24775,33 +25007,171 @@ class VideoEngine {
|
||||
this.playbackRate = 1;
|
||||
this.posterDrawn = false;
|
||||
this.isFetching = false;
|
||||
__privateSet(this, _onError, onError);
|
||||
__privateSet(this, _renderer, new Renderer(this, () => !__privateGet(this, _preparing), (frame) => __privateMethod(this, _VideoEngine_instances, draw_fn).call(this, frame, true), (error) => __privateMethod(this, _VideoEngine_instances, report_fn).call(this, error)));
|
||||
}
|
||||
async preflight(url, signal, isCurrent) {
|
||||
return preflightRange(url, this.preflightRange, this.events, signal, isCurrent);
|
||||
async preflight(source, signal, current) {
|
||||
return preflightRange(source, this.preflightRange, this.events, signal, current);
|
||||
}
|
||||
drawPoster() {
|
||||
if (!this.poster || this.posterDrawn)
|
||||
if (__privateGet(this, _destroyed) || !this.poster || this.posterDrawn)
|
||||
return;
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.canvas.width = img.naturalWidth || this.canvas.width;
|
||||
this.canvas.height = img.naturalHeight || this.canvas.height;
|
||||
this.ctx.drawImage(img, 0, 0, this.canvas.width, this.canvas.height);
|
||||
const generation = __privateGet(this, _generation);
|
||||
__privateGet(this, _poster).draw(this.poster, () => __privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation), (image) => {
|
||||
this.clear();
|
||||
this.canvas.width = image.naturalWidth || this.canvas.width;
|
||||
this.canvas.height = image.naturalHeight || this.canvas.height;
|
||||
__privateMethod(this, _VideoEngine_instances, context_fn).call(this).drawImage(image, 0, 0, this.canvas.width, this.canvas.height);
|
||||
this.posterDrawn = true;
|
||||
};
|
||||
img.src = this.poster;
|
||||
}, (error) => __privateMethod(this, _VideoEngine_instances, report_fn).call(this, error));
|
||||
}
|
||||
async stopIterator() {
|
||||
await this.videoIterator?.return();
|
||||
const iterator = this.videoIterator;
|
||||
this.videoIterator = null;
|
||||
this.nextFrame = null;
|
||||
await releaseIterator(iterator);
|
||||
}
|
||||
clear() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
__privateMethod(this, _VideoEngine_instances, context_fn).call(this).clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
cancelPending() {
|
||||
__privateWrapper(this, _generation)._++;
|
||||
__privateWrapper(this, _iteration)._++;
|
||||
this.stop();
|
||||
__privateMethod(this, _VideoEngine_instances, release_fn).call(this);
|
||||
__privateGet(this, _poster).cancel();
|
||||
__privateSet(this, _preparing, false);
|
||||
__privateSet(this, _loading, null);
|
||||
__privateSet(this, _createSink, null);
|
||||
__privateSet(this, _usedSink, null);
|
||||
this.videoSink = null;
|
||||
this.input = null;
|
||||
}
|
||||
async load(media, onMetadata) {
|
||||
++this.asyncId;
|
||||
await this.stopIterator();
|
||||
if (__privateGet(this, _destroyed))
|
||||
return;
|
||||
this.cancelPending();
|
||||
const generation = __privateGet(this, _generation);
|
||||
const iteration = __privateGet(this, _iteration);
|
||||
__privateSet(this, _preparing, true);
|
||||
const pending = Promise.resolve().then(() => __privateMethod(this, _VideoEngine_instances, load_fn).call(this, media, generation, iteration, onMetadata));
|
||||
__privateSet(this, _loading, pending);
|
||||
try {
|
||||
await pending;
|
||||
} finally {
|
||||
if (__privateGet(this, _loading) === pending)
|
||||
__privateSet(this, _loading, null);
|
||||
}
|
||||
}
|
||||
handleNoVideoTrack() {
|
||||
if (__privateGet(this, _destroyed))
|
||||
return;
|
||||
this.videoSink = null;
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.canvas.width = 0;
|
||||
this.canvas.height = 0;
|
||||
this.clear();
|
||||
this.drawPoster();
|
||||
}
|
||||
async resetIterator(time) {
|
||||
if (__privateGet(this, _destroyed))
|
||||
return;
|
||||
const generation = __privateGet(this, _generation);
|
||||
const iteration = ++__privateWrapper(this, _iteration)._;
|
||||
const loading = __privateGet(this, _loading);
|
||||
this.stop();
|
||||
__privateGet(this, _poster).cancel();
|
||||
__privateSet(this, _preparing, true);
|
||||
try {
|
||||
if (loading)
|
||||
await loading;
|
||||
await __privateMethod(this, _VideoEngine_instances, reset_fn).call(this, time, generation, iteration);
|
||||
} catch (error) {
|
||||
if (__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation) && __privateGet(this, _iteration) === iteration) {
|
||||
__privateMethod(this, _VideoEngine_instances, release_fn).call(this);
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
if (__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation) && __privateGet(this, _iteration) === iteration)
|
||||
__privateSet(this, _preparing, false);
|
||||
}
|
||||
}
|
||||
async updateNextFrame(localId) {
|
||||
await __privateGet(this, _renderer).update(localId);
|
||||
}
|
||||
render() {
|
||||
__privateGet(this, _renderer).render();
|
||||
}
|
||||
start(audio) {
|
||||
__privateGet(this, _renderer).start(audio);
|
||||
}
|
||||
stop() {
|
||||
__privateGet(this, _renderer).stop();
|
||||
}
|
||||
async seek(time) {
|
||||
await this.resetIterator(time);
|
||||
}
|
||||
setPlaybackRate(rate) {
|
||||
this.playbackRate = Math.max(0.1, Number(rate) || 1);
|
||||
}
|
||||
destroy() {
|
||||
if (__privateGet(this, _destroyed))
|
||||
return;
|
||||
__privateSet(this, _destroyed, true);
|
||||
__privateWrapper(this, _generation)._++;
|
||||
__privateGet(this, _renderer).destroy();
|
||||
__privateGet(this, _poster).cancel();
|
||||
__privateMethod(this, _VideoEngine_instances, release_fn).call(this);
|
||||
this.posterDrawn = false;
|
||||
__privateSet(this, _createSink, null);
|
||||
__privateSet(this, _usedSink, null);
|
||||
this.videoSink = null;
|
||||
this.input = null;
|
||||
this.audioClock = null;
|
||||
}
|
||||
}
|
||||
_generation = new WeakMap();
|
||||
_iteration = new WeakMap();
|
||||
_loading = new WeakMap();
|
||||
_createSink = new WeakMap();
|
||||
_usedSink = new WeakMap();
|
||||
_destroyed = new WeakMap();
|
||||
_preparing = new WeakMap();
|
||||
_poster = new WeakMap();
|
||||
_renderer = new WeakMap();
|
||||
_onError = new WeakMap();
|
||||
_VideoEngine_instances = new WeakSet();
|
||||
current_fn = function(generation) {
|
||||
return !__privateGet(this, _destroyed) && __privateGet(this, _generation) === generation;
|
||||
};
|
||||
context_fn = function() {
|
||||
if (!this.ctx)
|
||||
throw new Error("Canvas 2D context is unavailable.");
|
||||
return this.ctx;
|
||||
};
|
||||
report_fn = function(error) {
|
||||
if (__privateGet(this, _destroyed))
|
||||
return;
|
||||
__privateMethod(this, _VideoEngine_instances, release_fn).call(this);
|
||||
if (__privateGet(this, _onError))
|
||||
__privateGet(this, _onError).call(this, error);
|
||||
else
|
||||
this.events.emit("error", error);
|
||||
};
|
||||
release_fn = function() {
|
||||
void this.stopIterator().catch((error) => console.warn("MediaBunny iterator cleanup error:", error));
|
||||
};
|
||||
draw_fn = function(frame, clear) {
|
||||
__privateGet(this, _poster).cancel();
|
||||
if (clear)
|
||||
this.clear();
|
||||
__privateMethod(this, _VideoEngine_instances, context_fn).call(this).drawImage(frame.canvas, 0, 0);
|
||||
};
|
||||
load_fn = async function(media, generation, iteration, onMetadata) {
|
||||
if (!__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation))
|
||||
return;
|
||||
try {
|
||||
this.clear();
|
||||
this.posterDrawn = false;
|
||||
const { input, videoTrack, duration } = media;
|
||||
@@ -24812,139 +25182,61 @@ class VideoEngine {
|
||||
onMetadata?.();
|
||||
return;
|
||||
}
|
||||
if (videoTrack.codec === null || !await videoTrack.canDecode()) {
|
||||
const decodable = videoTrack.codec !== null && await videoTrack.canDecode();
|
||||
if (!__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation))
|
||||
return;
|
||||
if (!decodable) {
|
||||
this.handleNoVideoTrack();
|
||||
onMetadata?.();
|
||||
return;
|
||||
}
|
||||
const transparent = await videoTrack.canBeTransparent();
|
||||
this.videoSink = new CanvasSink(videoTrack, {
|
||||
poolSize: 2,
|
||||
fit: "contain",
|
||||
alpha: transparent
|
||||
});
|
||||
if (!__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation))
|
||||
return;
|
||||
__privateSet(this, _createSink, () => new CanvasSink(videoTrack, { poolSize: 2, fit: "contain", alpha: transparent }));
|
||||
this.videoSink = __privateGet(this, _createSink).call(this);
|
||||
this.width = videoTrack.displayWidth;
|
||||
this.height = videoTrack.displayHeight;
|
||||
this.canvas.width = this.width;
|
||||
this.canvas.height = this.height;
|
||||
onMetadata?.();
|
||||
await this.resetIterator(0);
|
||||
if (__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation))
|
||||
await __privateMethod(this, _VideoEngine_instances, reset_fn).call(this, 0, generation, iteration);
|
||||
} catch (error) {
|
||||
if (__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation)) {
|
||||
__privateMethod(this, _VideoEngine_instances, release_fn).call(this);
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
if (__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation) && __privateGet(this, _iteration) === iteration)
|
||||
__privateSet(this, _preparing, false);
|
||||
}
|
||||
handleNoVideoTrack() {
|
||||
this.videoSink = null;
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.canvas.width = 0;
|
||||
this.canvas.height = 0;
|
||||
this.clear();
|
||||
};
|
||||
reset_fn = async function(time, generation, iteration) {
|
||||
if (!__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation) || __privateGet(this, _iteration) !== iteration)
|
||||
return;
|
||||
__privateMethod(this, _VideoEngine_instances, release_fn).call(this);
|
||||
if (__privateGet(this, _createSink) && this.videoSink === __privateGet(this, _usedSink))
|
||||
this.videoSink = __privateGet(this, _createSink).call(this);
|
||||
const sink = this.videoSink;
|
||||
if (!sink || !__privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation))
|
||||
return;
|
||||
__privateSet(this, _usedSink, sink);
|
||||
const iterator = sink.canvases(time);
|
||||
this.videoIterator = iterator;
|
||||
const current = () => __privateMethod(this, _VideoEngine_instances, current_fn).call(this, generation) && __privateGet(this, _iteration) === iteration && this.videoIterator === iterator;
|
||||
const first = await readCanvas(iterator);
|
||||
if (!current())
|
||||
return;
|
||||
const second = await readCanvas(iterator);
|
||||
if (!current())
|
||||
return;
|
||||
this.nextFrame = second;
|
||||
if (first)
|
||||
__privateMethod(this, _VideoEngine_instances, draw_fn).call(this, first, false);
|
||||
else
|
||||
this.drawPoster();
|
||||
}
|
||||
async resetIterator(time) {
|
||||
await this.stopIterator();
|
||||
if (!this.videoSink)
|
||||
return;
|
||||
this.videoIterator = this.videoSink.canvases(time);
|
||||
const first = (await this.videoIterator.next()).value ?? null;
|
||||
const second = (await this.videoIterator.next()).value ?? null;
|
||||
this.nextFrame = second;
|
||||
if (first) {
|
||||
this.ctx.drawImage(first.canvas, 0, 0);
|
||||
} else {
|
||||
this.drawPoster();
|
||||
}
|
||||
}
|
||||
async updateNextFrame(localId) {
|
||||
if (!this.videoIterator || this.isFetching)
|
||||
return;
|
||||
this.isFetching = true;
|
||||
try {
|
||||
while (true) {
|
||||
const frame = (await this.videoIterator.next()).value ?? null;
|
||||
if (!frame || localId !== this.asyncId)
|
||||
return;
|
||||
const t = this.audioClock.currentTime;
|
||||
const tolerance = this.dropLateFrames ? Math.max(0.06, this.avSyncTolerance / Math.max(1, this.playbackRate)) : 0;
|
||||
if (this.dropLateFrames && frame.timestamp < t - tolerance) {
|
||||
continue;
|
||||
}
|
||||
if (frame.timestamp <= t + tolerance) {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.ctx.drawImage(frame.canvas, 0, 0);
|
||||
if (!this.dropLateFrames && frame.timestamp > t) {
|
||||
this.nextFrame = null;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.nextFrame = frame;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.isFetching = false;
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (!this.audioClock)
|
||||
return;
|
||||
const t = this.audioClock.currentTime;
|
||||
const now = Date.now();
|
||||
if (now - this.lastTimeUpdate >= this.timeupdateInterval) {
|
||||
this.events.emit("timeupdate");
|
||||
this.lastTimeUpdate = now;
|
||||
}
|
||||
if (Number.isFinite(this.duration) && t >= this.duration) {
|
||||
this.stop();
|
||||
this.stalled = false;
|
||||
this.events.emit("ended");
|
||||
this.events.emit("pause");
|
||||
this.events.emit("canplay");
|
||||
return;
|
||||
}
|
||||
if (this.nextFrame && this.nextFrame.timestamp <= t) {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.ctx.drawImage(this.nextFrame.canvas, 0, 0);
|
||||
this.nextFrame = null;
|
||||
this.updateNextFrame(this.asyncId);
|
||||
if (this.stalled) {
|
||||
this.events.emit("canplay");
|
||||
this.events.emit("playing");
|
||||
this.stalled = false;
|
||||
}
|
||||
} else if (!this.nextFrame) {
|
||||
this.updateNextFrame(this.asyncId);
|
||||
if (!this.nextFrame && Number.isFinite(this.duration) && t < this.duration && !this.stalled) {
|
||||
this.stalled = true;
|
||||
this.events.emit("waiting");
|
||||
}
|
||||
}
|
||||
this.rafId = requestAnimationFrame(() => this.render());
|
||||
}
|
||||
start(audioEngine) {
|
||||
this.audioClock = audioEngine;
|
||||
this.asyncId++;
|
||||
this.stalled = false;
|
||||
this.updateNextFrame(this.asyncId);
|
||||
this.rafId = requestAnimationFrame(() => this.render());
|
||||
}
|
||||
stop() {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
}
|
||||
async seek(time) {
|
||||
this.asyncId++;
|
||||
await this.resetIterator(time);
|
||||
}
|
||||
setPlaybackRate(rate) {
|
||||
this.playbackRate = Math.max(0.1, Number(rate) || 1);
|
||||
}
|
||||
destroy() {
|
||||
this.asyncId++;
|
||||
this.stop();
|
||||
this.stopIterator();
|
||||
this.posterDrawn = false;
|
||||
this.videoSink = null;
|
||||
this.input = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
class MediaBunnyEngine {
|
||||
constructor({ canvas, ctx, events, option = {} }) {
|
||||
__privateAdd(this, _playback);
|
||||
@@ -24959,7 +25251,15 @@ class MediaBunnyEngine {
|
||||
avSyncTolerance: option.avSyncTolerance ?? 0.12,
|
||||
dropLateFrames: option.dropLateFrames ?? false,
|
||||
poster: option.poster ?? "",
|
||||
preflightRange: option.preflightRange ?? false
|
||||
preflightRange: option.preflightRange ?? false,
|
||||
onError: (error) => {
|
||||
try {
|
||||
this.pause();
|
||||
} catch (failure) {
|
||||
console.warn("MediaBunny video failure cleanup:", failure);
|
||||
}
|
||||
this.reportError(error);
|
||||
}
|
||||
});
|
||||
this.paused = true;
|
||||
this.ended = false;
|
||||
@@ -24986,6 +25286,7 @@ class MediaBunnyEngine {
|
||||
return;
|
||||
const id = ++this.loadSeq;
|
||||
__privateGet(this, _playback).invalidate();
|
||||
this.video.cancelPending();
|
||||
this.loadSession?.cancel();
|
||||
if (id !== this.loadSeq || this.destroyed)
|
||||
return;
|
||||
@@ -25009,6 +25310,7 @@ class MediaBunnyEngine {
|
||||
return;
|
||||
const failed = ++this.loadSeq;
|
||||
__privateGet(this, _playback).invalidate();
|
||||
this.video.cancelPending();
|
||||
session.cancel();
|
||||
if (failed !== this.loadSeq || this.destroyed)
|
||||
return;
|
||||
|
||||
@@ -45,6 +45,15 @@ export default class MediaBunnyEngine implements EnginePort {
|
||||
dropLateFrames: option.dropLateFrames ?? false,
|
||||
poster: option.poster ?? '',
|
||||
preflightRange: option.preflightRange ?? false,
|
||||
onError: (error) => {
|
||||
try {
|
||||
this.pause()
|
||||
}
|
||||
catch (failure) {
|
||||
console.warn('MediaBunny video failure cleanup:', failure)
|
||||
}
|
||||
this.reportError(error)
|
||||
},
|
||||
})
|
||||
this.paused = true
|
||||
this.ended = false
|
||||
@@ -72,6 +81,7 @@ export default class MediaBunnyEngine implements EnginePort {
|
||||
return
|
||||
const id = ++this.loadSeq
|
||||
this.#playback.invalidate()
|
||||
this.video.cancelPending()
|
||||
this.loadSession?.cancel()
|
||||
if (id !== this.loadSeq || this.destroyed)
|
||||
return
|
||||
@@ -96,6 +106,7 @@ export default class MediaBunnyEngine implements EnginePort {
|
||||
return
|
||||
const failed = ++this.loadSeq
|
||||
this.#playback.invalidate()
|
||||
this.video.cancelPending()
|
||||
session.cancel()
|
||||
if (failed !== this.loadSeq || this.destroyed)
|
||||
return
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
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
|
||||
@@ -1,272 +0,0 @@
|
||||
/**
|
||||
* Video Engine for MediaBunny
|
||||
* Handles video frame rendering and synchronization
|
||||
*/
|
||||
import {
|
||||
CanvasSink,
|
||||
} from 'mediabunny'
|
||||
import { preflightRange } from './preflight'
|
||||
|
||||
export default class VideoEngine {
|
||||
constructor({
|
||||
canvas,
|
||||
ctx,
|
||||
events,
|
||||
timeupdateInterval = 250,
|
||||
avSyncTolerance = 0.12,
|
||||
dropLateFrames = false,
|
||||
poster = '',
|
||||
preflightRange = false,
|
||||
}) {
|
||||
this.canvas = canvas
|
||||
this.ctx = ctx
|
||||
this.events = events
|
||||
this.timeupdateInterval = timeupdateInterval
|
||||
this.avSyncTolerance = avSyncTolerance
|
||||
this.dropLateFrames = dropLateFrames
|
||||
this.poster = poster
|
||||
this.preflightRange = preflightRange
|
||||
|
||||
// MediaBunny instances
|
||||
this.input = null
|
||||
this.videoSink = null
|
||||
this.videoIterator = null
|
||||
|
||||
// Frame rendering
|
||||
this.nextFrame = null
|
||||
this.rafId = 0
|
||||
this.asyncId = 0
|
||||
|
||||
// Video properties
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.duration = Number.NaN
|
||||
|
||||
// Playback state
|
||||
this.audioClock = null
|
||||
this.lastTimeUpdate = 0
|
||||
this.stalled = false
|
||||
this.playbackRate = 1
|
||||
this.posterDrawn = false
|
||||
this.isFetching = false
|
||||
}
|
||||
|
||||
async preflight(url, signal, isCurrent) {
|
||||
return preflightRange(url, this.preflightRange, this.events, signal, isCurrent)
|
||||
}
|
||||
|
||||
drawPoster() {
|
||||
if (!this.poster || this.posterDrawn)
|
||||
return
|
||||
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
this.canvas.width = img.naturalWidth || this.canvas.width
|
||||
this.canvas.height = img.naturalHeight || this.canvas.height
|
||||
this.ctx.drawImage(img, 0, 0, this.canvas.width, this.canvas.height)
|
||||
this.posterDrawn = true
|
||||
}
|
||||
img.src = this.poster
|
||||
}
|
||||
|
||||
async stopIterator() {
|
||||
await this.videoIterator?.return()
|
||||
this.videoIterator = null
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
}
|
||||
|
||||
async load(media, onMetadata) {
|
||||
++this.asyncId
|
||||
|
||||
await this.stopIterator()
|
||||
this.clear()
|
||||
this.posterDrawn = false
|
||||
|
||||
const { input, videoTrack, duration } = media
|
||||
this.input = input
|
||||
this.duration = duration
|
||||
|
||||
if (!videoTrack) {
|
||||
this.handleNoVideoTrack()
|
||||
onMetadata?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (videoTrack.codec === null || !(await videoTrack.canDecode())) {
|
||||
this.handleNoVideoTrack()
|
||||
onMetadata?.()
|
||||
return
|
||||
}
|
||||
|
||||
const transparent = await videoTrack.canBeTransparent()
|
||||
this.videoSink = new CanvasSink(videoTrack, {
|
||||
poolSize: 2,
|
||||
fit: 'contain',
|
||||
alpha: transparent,
|
||||
})
|
||||
|
||||
this.width = videoTrack.displayWidth
|
||||
this.height = videoTrack.displayHeight
|
||||
|
||||
this.canvas.width = this.width
|
||||
this.canvas.height = this.height
|
||||
|
||||
onMetadata?.()
|
||||
|
||||
await this.resetIterator(0)
|
||||
}
|
||||
|
||||
handleNoVideoTrack() {
|
||||
this.videoSink = null
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.canvas.width = 0
|
||||
this.canvas.height = 0
|
||||
this.clear()
|
||||
this.drawPoster()
|
||||
}
|
||||
|
||||
async resetIterator(time) {
|
||||
await this.stopIterator()
|
||||
|
||||
if (!this.videoSink)
|
||||
return
|
||||
|
||||
this.videoIterator = this.videoSink.canvases(time)
|
||||
|
||||
const first = (await this.videoIterator.next()).value ?? null
|
||||
const second = (await this.videoIterator.next()).value ?? null
|
||||
|
||||
this.nextFrame = second
|
||||
|
||||
if (first) {
|
||||
this.ctx.drawImage(first.canvas, 0, 0)
|
||||
}
|
||||
else {
|
||||
this.drawPoster()
|
||||
}
|
||||
}
|
||||
|
||||
async updateNextFrame(localId) {
|
||||
if (!this.videoIterator || this.isFetching)
|
||||
return
|
||||
|
||||
this.isFetching = true
|
||||
try {
|
||||
while (true) {
|
||||
const frame = (await this.videoIterator.next()).value ?? null
|
||||
if (!frame || localId !== this.asyncId)
|
||||
return
|
||||
|
||||
const t = this.audioClock.currentTime
|
||||
const tolerance = this.dropLateFrames
|
||||
? Math.max(0.06, this.avSyncTolerance / Math.max(1, this.playbackRate))
|
||||
: 0
|
||||
|
||||
if (this.dropLateFrames && frame.timestamp < t - tolerance) {
|
||||
// Skip late frame
|
||||
continue
|
||||
}
|
||||
|
||||
if (frame.timestamp <= t + tolerance) {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
this.ctx.drawImage(frame.canvas, 0, 0)
|
||||
|
||||
if (!this.dropLateFrames && frame.timestamp > t) {
|
||||
this.nextFrame = null
|
||||
return
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.nextFrame = frame
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.isFetching = false
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.audioClock)
|
||||
return
|
||||
|
||||
const t = this.audioClock.currentTime
|
||||
const now = Date.now()
|
||||
|
||||
// Emit timeupdate event
|
||||
if (now - this.lastTimeUpdate >= this.timeupdateInterval) {
|
||||
this.events.emit('timeupdate')
|
||||
this.lastTimeUpdate = now
|
||||
}
|
||||
|
||||
// Check if reached end
|
||||
if (Number.isFinite(this.duration) && t >= this.duration) {
|
||||
this.stop()
|
||||
this.stalled = false
|
||||
this.events.emit('ended')
|
||||
this.events.emit('pause')
|
||||
this.events.emit('canplay')
|
||||
return
|
||||
}
|
||||
|
||||
// Render next frame if ready
|
||||
if (this.nextFrame && this.nextFrame.timestamp <= t) {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
this.ctx.drawImage(this.nextFrame.canvas, 0, 0)
|
||||
this.nextFrame = null
|
||||
this.updateNextFrame(this.asyncId)
|
||||
|
||||
if (this.stalled) {
|
||||
this.events.emit('canplay')
|
||||
this.events.emit('playing')
|
||||
this.stalled = false
|
||||
}
|
||||
}
|
||||
else if (!this.nextFrame) {
|
||||
this.updateNextFrame(this.asyncId)
|
||||
|
||||
if (!this.nextFrame && Number.isFinite(this.duration) && t < this.duration && !this.stalled) {
|
||||
this.stalled = true
|
||||
this.events.emit('waiting')
|
||||
}
|
||||
}
|
||||
|
||||
this.rafId = requestAnimationFrame(() => this.render())
|
||||
}
|
||||
|
||||
start(audioEngine) {
|
||||
this.audioClock = audioEngine
|
||||
this.asyncId++
|
||||
this.stalled = false
|
||||
this.updateNextFrame(this.asyncId)
|
||||
this.rafId = requestAnimationFrame(() => this.render())
|
||||
}
|
||||
|
||||
stop() {
|
||||
cancelAnimationFrame(this.rafId)
|
||||
}
|
||||
|
||||
async seek(time) {
|
||||
this.asyncId++
|
||||
await this.resetIterator(time)
|
||||
}
|
||||
|
||||
setPlaybackRate(rate) {
|
||||
this.playbackRate = Math.max(0.1, Number(rate) || 1)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.asyncId++
|
||||
this.stop()
|
||||
this.stopIterator()
|
||||
this.posterDrawn = false
|
||||
this.videoSink = null
|
||||
this.input = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import type { Input, WrappedCanvas } from 'mediabunny'
|
||||
import type { VideoOptions, VideoPort } from './engine-ports'
|
||||
import type EventTarget from './EventTarget'
|
||||
import type { PlaybackMedia } from './media'
|
||||
import type { FrameIterator } from './video-frames'
|
||||
import { CanvasSink } from 'mediabunny'
|
||||
import { preflightRange } from './preflight'
|
||||
import { readCanvas, releaseIterator } from './video-frames'
|
||||
import Poster from './video-poster'
|
||||
import Renderer from './video-renderer'
|
||||
|
||||
export default class VideoEngine implements VideoPort {
|
||||
declare canvas: HTMLCanvasElement
|
||||
declare ctx: CanvasRenderingContext2D | null
|
||||
declare events: EventTarget
|
||||
declare timeupdateInterval: number
|
||||
declare avSyncTolerance: number
|
||||
declare dropLateFrames: boolean
|
||||
declare poster: string
|
||||
declare preflightRange: boolean
|
||||
declare input: Input | null
|
||||
declare videoSink: CanvasSink | null
|
||||
declare videoIterator: FrameIterator | null
|
||||
declare nextFrame: WrappedCanvas | null
|
||||
declare rafId: number
|
||||
declare asyncId: number
|
||||
declare width: number
|
||||
declare height: number
|
||||
declare duration: number
|
||||
declare audioClock: { readonly currentTime: number } | null
|
||||
declare lastTimeUpdate: number
|
||||
declare stalled: boolean
|
||||
declare playbackRate: number
|
||||
declare posterDrawn: boolean
|
||||
declare isFetching: boolean
|
||||
#generation = 0
|
||||
#iteration = 0
|
||||
#loading: Promise<void> | null = null
|
||||
#createSink: (() => CanvasSink) | null = null
|
||||
#usedSink: CanvasSink | null = null
|
||||
#destroyed = false
|
||||
#preparing = false
|
||||
#poster = new Poster()
|
||||
#renderer: Renderer
|
||||
#onError: ((error: unknown) => void) | undefined
|
||||
|
||||
constructor({ canvas, ctx, events, timeupdateInterval = 250, avSyncTolerance = 0.12, dropLateFrames = false, poster = '', preflightRange = false, onError }: VideoOptions) {
|
||||
this.canvas = canvas
|
||||
this.ctx = ctx
|
||||
this.events = events
|
||||
this.timeupdateInterval = timeupdateInterval
|
||||
this.avSyncTolerance = avSyncTolerance
|
||||
this.dropLateFrames = dropLateFrames
|
||||
this.poster = poster
|
||||
this.preflightRange = preflightRange
|
||||
this.input = null
|
||||
this.videoSink = null
|
||||
this.videoIterator = null
|
||||
this.nextFrame = null
|
||||
this.rafId = 0
|
||||
this.asyncId = 0
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.duration = Number.NaN
|
||||
this.audioClock = null
|
||||
this.lastTimeUpdate = 0
|
||||
this.stalled = false
|
||||
this.playbackRate = 1
|
||||
this.posterDrawn = false
|
||||
this.isFetching = false
|
||||
this.#onError = onError
|
||||
this.#renderer = new Renderer(this, () => !this.#preparing, frame => this.#draw(frame, true), error => this.#report(error))
|
||||
}
|
||||
|
||||
#current(generation: number): boolean {
|
||||
return !this.#destroyed && this.#generation === generation
|
||||
}
|
||||
|
||||
#context(): CanvasRenderingContext2D {
|
||||
if (!this.ctx)
|
||||
throw new Error('Canvas 2D context is unavailable.')
|
||||
return this.ctx
|
||||
}
|
||||
|
||||
#report(error: unknown): void {
|
||||
if (this.#destroyed)
|
||||
return
|
||||
this.#release()
|
||||
if (this.#onError)
|
||||
this.#onError(error)
|
||||
else
|
||||
this.events.emit('error', error)
|
||||
}
|
||||
|
||||
#release(): void {
|
||||
void this.stopIterator().catch(error => console.warn('MediaBunny iterator cleanup error:', error))
|
||||
}
|
||||
|
||||
#draw(frame: WrappedCanvas, clear: boolean): void {
|
||||
this.#poster.cancel()
|
||||
if (clear)
|
||||
this.clear()
|
||||
this.#context().drawImage(frame.canvas, 0, 0)
|
||||
}
|
||||
|
||||
async preflight(source: unknown, signal?: AbortSignal, current?: () => boolean): Promise<boolean> {
|
||||
return preflightRange(source, this.preflightRange, this.events, signal, current)
|
||||
}
|
||||
|
||||
drawPoster(): void {
|
||||
if (this.#destroyed || !this.poster || this.posterDrawn)
|
||||
return
|
||||
const generation = this.#generation
|
||||
this.#poster.draw(this.poster, () => this.#current(generation), (image) => {
|
||||
this.clear()
|
||||
this.canvas.width = image.naturalWidth || this.canvas.width
|
||||
this.canvas.height = image.naturalHeight || this.canvas.height
|
||||
this.#context().drawImage(image, 0, 0, this.canvas.width, this.canvas.height)
|
||||
this.posterDrawn = true
|
||||
}, error => this.#report(error))
|
||||
}
|
||||
|
||||
async stopIterator(): Promise<void> {
|
||||
const iterator = this.videoIterator
|
||||
this.videoIterator = null
|
||||
this.nextFrame = null
|
||||
await releaseIterator(iterator)
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.#context().clearRect(0, 0, this.canvas.width, this.canvas.height)
|
||||
}
|
||||
|
||||
cancelPending(): void {
|
||||
this.#generation++
|
||||
this.#iteration++
|
||||
this.stop()
|
||||
this.#release()
|
||||
this.#poster.cancel()
|
||||
this.#preparing = false
|
||||
this.#loading = null
|
||||
this.#createSink = null
|
||||
this.#usedSink = null
|
||||
this.videoSink = null
|
||||
this.input = null
|
||||
}
|
||||
|
||||
async load(media: PlaybackMedia, onMetadata?: () => void): Promise<void> {
|
||||
if (this.#destroyed)
|
||||
return
|
||||
this.cancelPending()
|
||||
const generation = this.#generation
|
||||
const iteration = this.#iteration
|
||||
this.#preparing = true
|
||||
const pending = Promise.resolve().then(() => this.#load(media, generation, iteration, onMetadata))
|
||||
this.#loading = pending
|
||||
try {
|
||||
await pending
|
||||
}
|
||||
finally {
|
||||
if (this.#loading === pending)
|
||||
this.#loading = null
|
||||
}
|
||||
}
|
||||
|
||||
async #load(media: PlaybackMedia, generation: number, iteration: number, onMetadata?: () => void): Promise<void> {
|
||||
if (!this.#current(generation))
|
||||
return
|
||||
try {
|
||||
this.clear()
|
||||
this.posterDrawn = false
|
||||
const { input, videoTrack, duration } = media
|
||||
this.input = input
|
||||
this.duration = duration
|
||||
if (!videoTrack) {
|
||||
this.handleNoVideoTrack()
|
||||
onMetadata?.()
|
||||
return
|
||||
}
|
||||
const decodable = videoTrack.codec !== null && await videoTrack.canDecode()
|
||||
if (!this.#current(generation))
|
||||
return
|
||||
if (!decodable) {
|
||||
this.handleNoVideoTrack()
|
||||
onMetadata?.()
|
||||
return
|
||||
}
|
||||
const transparent = await videoTrack.canBeTransparent()
|
||||
if (!this.#current(generation))
|
||||
return
|
||||
this.#createSink = () => new CanvasSink(videoTrack, { poolSize: 2, fit: 'contain', alpha: transparent })
|
||||
this.videoSink = this.#createSink()
|
||||
this.width = videoTrack.displayWidth
|
||||
this.height = videoTrack.displayHeight
|
||||
this.canvas.width = this.width
|
||||
this.canvas.height = this.height
|
||||
onMetadata?.()
|
||||
if (this.#current(generation))
|
||||
await this.#reset(0, generation, iteration)
|
||||
}
|
||||
catch (error) {
|
||||
if (this.#current(generation)) {
|
||||
this.#release()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (this.#current(generation) && this.#iteration === iteration)
|
||||
this.#preparing = false
|
||||
}
|
||||
}
|
||||
|
||||
handleNoVideoTrack(): void {
|
||||
if (this.#destroyed)
|
||||
return
|
||||
this.videoSink = null
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.canvas.width = 0
|
||||
this.canvas.height = 0
|
||||
this.clear()
|
||||
this.drawPoster()
|
||||
}
|
||||
|
||||
async #reset(time: number, generation: number, iteration: number): Promise<void> {
|
||||
if (!this.#current(generation) || this.#iteration !== iteration)
|
||||
return
|
||||
this.#release()
|
||||
if (this.#createSink && this.videoSink === this.#usedSink)
|
||||
this.videoSink = this.#createSink()
|
||||
const sink = this.videoSink
|
||||
if (!sink || !this.#current(generation))
|
||||
return
|
||||
this.#usedSink = sink
|
||||
const iterator = sink.canvases(time)
|
||||
this.videoIterator = iterator
|
||||
const current = () => this.#current(generation) && this.#iteration === iteration && this.videoIterator === iterator
|
||||
const first = await readCanvas(iterator)
|
||||
if (!current())
|
||||
return
|
||||
const second = await readCanvas(iterator)
|
||||
if (!current())
|
||||
return
|
||||
this.nextFrame = second
|
||||
if (first)
|
||||
this.#draw(first, false)
|
||||
else
|
||||
this.drawPoster()
|
||||
}
|
||||
|
||||
async resetIterator(time: number): Promise<void> {
|
||||
if (this.#destroyed)
|
||||
return
|
||||
const generation = this.#generation
|
||||
const iteration = ++this.#iteration
|
||||
const loading = this.#loading
|
||||
this.stop()
|
||||
this.#poster.cancel()
|
||||
this.#preparing = true
|
||||
try {
|
||||
if (loading)
|
||||
await loading
|
||||
await this.#reset(time, generation, iteration)
|
||||
}
|
||||
catch (error) {
|
||||
if (this.#current(generation) && this.#iteration === iteration) {
|
||||
this.#release()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (this.#current(generation) && this.#iteration === iteration)
|
||||
this.#preparing = false
|
||||
}
|
||||
}
|
||||
|
||||
async updateNextFrame(localId: number): Promise<void> {
|
||||
await this.#renderer.update(localId)
|
||||
}
|
||||
|
||||
render(): void {
|
||||
this.#renderer.render()
|
||||
}
|
||||
|
||||
start(audio: { readonly currentTime: number }): void {
|
||||
this.#renderer.start(audio)
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.#renderer.stop()
|
||||
}
|
||||
|
||||
async seek(time: number): Promise<void> {
|
||||
await this.resetIterator(time)
|
||||
}
|
||||
|
||||
setPlaybackRate(rate: number): void {
|
||||
this.playbackRate = Math.max(0.1, Number(rate) || 1)
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.#destroyed)
|
||||
return
|
||||
this.#destroyed = true
|
||||
this.#generation++
|
||||
this.#renderer.destroy()
|
||||
this.#poster.cancel()
|
||||
this.#release()
|
||||
this.posterDrawn = false
|
||||
this.#createSink = null
|
||||
this.#usedSink = null
|
||||
this.videoSink = null
|
||||
this.input = null
|
||||
this.audioClock = null
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export interface AudioPort {
|
||||
}
|
||||
|
||||
export interface VideoPort {
|
||||
cancelPending: () => void
|
||||
width: number
|
||||
height: number
|
||||
duration: number
|
||||
@@ -32,6 +33,7 @@ export interface VideoPort {
|
||||
}
|
||||
|
||||
export interface VideoOptions {
|
||||
onError?: (error: unknown) => void
|
||||
canvas: HTMLCanvasElement
|
||||
ctx: CanvasRenderingContext2D | null
|
||||
events: EventTarget
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { WrappedCanvas } from 'mediabunny'
|
||||
|
||||
export type FrameIterator = Pick<AsyncGenerator<WrappedCanvas, void, unknown>, 'next' | 'return'>
|
||||
const closing = new WeakMap<FrameIterator, Promise<void>>()
|
||||
const reading = new WeakMap<FrameIterator, Promise<WrappedCanvas | null>>()
|
||||
|
||||
export function releaseIterator(iterator: FrameIterator | null): Promise<void> {
|
||||
if (!iterator)
|
||||
return Promise.resolve()
|
||||
const previous = closing.get(iterator)
|
||||
if (previous)
|
||||
return previous
|
||||
const pending = Promise.resolve().then(() => iterator.return()).then(() => {})
|
||||
closing.set(iterator, pending)
|
||||
return pending
|
||||
}
|
||||
|
||||
export function readCanvas(iterator: FrameIterator): Promise<WrappedCanvas | null> {
|
||||
const previous = reading.get(iterator)
|
||||
if (previous)
|
||||
return previous
|
||||
const pending = Promise.resolve().then(() => iterator.next()).then(result => result.value ?? null)
|
||||
reading.set(iterator, pending)
|
||||
const clear = () => {
|
||||
if (reading.get(iterator) === pending)
|
||||
reading.delete(iterator)
|
||||
}
|
||||
pending.then(clear, clear)
|
||||
return pending
|
||||
}
|
||||
|
||||
export function frameAction(timestamp: number, time: number, dropLateFrames: boolean, avSyncTolerance: number, playbackRate: number): 'skip' | 'draw' | 'queue' {
|
||||
const tolerance = dropLateFrames ? Math.max(0.06, avSyncTolerance / Math.max(1, playbackRate)) : 0
|
||||
if (dropLateFrames && timestamp < time - tolerance)
|
||||
return 'skip'
|
||||
return timestamp <= time + tolerance ? 'draw' : 'queue'
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export default class Poster {
|
||||
private image: HTMLImageElement | null = null
|
||||
|
||||
cancel(): void {
|
||||
if (!this.image)
|
||||
return
|
||||
this.image.onload = null
|
||||
this.image.onerror = null
|
||||
this.image = null
|
||||
}
|
||||
|
||||
draw(source: string, current: () => boolean, draw: (image: HTMLImageElement) => void, failure: (error: unknown) => void): void {
|
||||
this.cancel()
|
||||
const image = new Image()
|
||||
this.image = image
|
||||
image.onload = () => {
|
||||
if (this.image !== image || !current())
|
||||
return
|
||||
this.cancel()
|
||||
try {
|
||||
draw(image)
|
||||
}
|
||||
catch (error) {
|
||||
failure(error)
|
||||
}
|
||||
}
|
||||
image.onerror = () => {
|
||||
if (this.image === image)
|
||||
this.cancel()
|
||||
}
|
||||
image.src = source
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import type { WrappedCanvas } from 'mediabunny'
|
||||
import type EventTarget from './EventTarget'
|
||||
import type { FrameIterator } from './video-frames'
|
||||
import { publish } from './readiness'
|
||||
import { frameAction, readCanvas } from './video-frames'
|
||||
|
||||
export interface RenderHost {
|
||||
events: EventTarget
|
||||
audioClock: { readonly currentTime: number } | null
|
||||
videoIterator: FrameIterator | null
|
||||
nextFrame: WrappedCanvas | null
|
||||
rafId: number
|
||||
asyncId: number
|
||||
duration: number
|
||||
lastTimeUpdate: number
|
||||
timeupdateInterval: number
|
||||
avSyncTolerance: number
|
||||
dropLateFrames: boolean
|
||||
playbackRate: number
|
||||
stalled: boolean
|
||||
isFetching: boolean
|
||||
}
|
||||
|
||||
export default class Renderer {
|
||||
private stopped = false
|
||||
private closed = false
|
||||
private scheduled: number | null = null
|
||||
private fetching: object | null = null
|
||||
|
||||
constructor(private host: RenderHost, private ready: () => boolean, private draw: (frame: WrappedCanvas) => void, private failure: (error: unknown) => void) {}
|
||||
|
||||
private current(id: number): boolean {
|
||||
return !this.closed && !this.stopped && this.host.asyncId === id
|
||||
}
|
||||
|
||||
async update(id: number): Promise<void> {
|
||||
const iterator = this.host.videoIterator
|
||||
if (!iterator || !this.ready() || !this.current(id) || this.host.isFetching)
|
||||
return
|
||||
const request = {}
|
||||
this.fetching = request
|
||||
this.host.isFetching = true
|
||||
const current = () => this.current(id) && this.host.videoIterator === iterator
|
||||
try {
|
||||
while (current()) {
|
||||
const frame = await readCanvas(iterator)
|
||||
if (!current() || !frame)
|
||||
return
|
||||
const time = this.host.audioClock?.currentTime
|
||||
if (time === undefined)
|
||||
return
|
||||
const action = frameAction(frame.timestamp, time, this.host.dropLateFrames, this.host.avSyncTolerance, this.host.playbackRate)
|
||||
if (action === 'skip')
|
||||
continue
|
||||
if (action === 'draw') {
|
||||
this.draw(frame)
|
||||
}
|
||||
else {
|
||||
this.host.nextFrame = frame
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (current())
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
if (this.fetching === request) {
|
||||
this.fetching = null
|
||||
this.host.isFetching = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private request(id: number): void {
|
||||
void this.update(id).catch((error) => {
|
||||
if (!this.current(id))
|
||||
return
|
||||
this.stop()
|
||||
this.report(error)
|
||||
})
|
||||
}
|
||||
|
||||
private report(error: unknown): void {
|
||||
try {
|
||||
this.failure(error)
|
||||
}
|
||||
catch (failure) {
|
||||
console.warn('MediaBunny video error listener:', failure)
|
||||
}
|
||||
}
|
||||
|
||||
private cancelFrame(): void {
|
||||
if (this.scheduled === null)
|
||||
return
|
||||
cancelAnimationFrame(this.scheduled)
|
||||
this.scheduled = null
|
||||
}
|
||||
|
||||
private schedule(id: number): void {
|
||||
if (!this.current(id))
|
||||
return
|
||||
this.cancelFrame()
|
||||
const frame = requestAnimationFrame(() => {
|
||||
if (this.scheduled !== frame || !this.current(id))
|
||||
return
|
||||
this.scheduled = null
|
||||
try {
|
||||
this.render()
|
||||
}
|
||||
catch (error) {
|
||||
this.stop()
|
||||
this.report(error)
|
||||
}
|
||||
})
|
||||
this.scheduled = frame
|
||||
this.host.rafId = frame
|
||||
}
|
||||
|
||||
render(): void {
|
||||
const id = this.host.asyncId
|
||||
const current = () => this.current(id)
|
||||
if (!current() || !this.host.audioClock)
|
||||
return
|
||||
const time = this.host.audioClock.currentTime
|
||||
const now = Date.now()
|
||||
if (now - this.host.lastTimeUpdate >= this.host.timeupdateInterval) {
|
||||
this.host.events.emit('timeupdate')
|
||||
if (!current())
|
||||
return
|
||||
this.host.lastTimeUpdate = now
|
||||
}
|
||||
if (!this.ready()) {
|
||||
this.schedule(id)
|
||||
return
|
||||
}
|
||||
if (Number.isFinite(this.host.duration) && time >= this.host.duration) {
|
||||
this.stop()
|
||||
this.host.stalled = false
|
||||
const ended = this.host.asyncId
|
||||
publish(this.host.events, () => !this.closed && this.host.asyncId === ended, ['ended', 'pause', 'canplay'])
|
||||
return
|
||||
}
|
||||
if (this.host.nextFrame && this.host.nextFrame.timestamp <= time) {
|
||||
this.draw(this.host.nextFrame)
|
||||
this.host.nextFrame = null
|
||||
this.request(id)
|
||||
if (this.host.stalled) {
|
||||
publish(this.host.events, current, ['canplay', 'playing'])
|
||||
if (!current())
|
||||
return
|
||||
this.host.stalled = false
|
||||
}
|
||||
}
|
||||
else if (!this.host.nextFrame) {
|
||||
this.request(id)
|
||||
if (Number.isFinite(this.host.duration) && time < this.host.duration && !this.host.stalled) {
|
||||
this.host.stalled = true
|
||||
this.host.events.emit('waiting')
|
||||
}
|
||||
}
|
||||
this.schedule(id)
|
||||
}
|
||||
|
||||
start(clock: { readonly currentTime: number }): void {
|
||||
if (this.closed)
|
||||
return
|
||||
this.stop()
|
||||
this.stopped = false
|
||||
this.host.audioClock = clock
|
||||
this.host.stalled = false
|
||||
if (!this.host.nextFrame)
|
||||
this.request(this.host.asyncId)
|
||||
this.schedule(this.host.asyncId)
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.stopped = true
|
||||
this.host.asyncId++
|
||||
this.cancelFrame()
|
||||
this.fetching = null
|
||||
this.host.isFetching = false
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.closed)
|
||||
return
|
||||
this.stop()
|
||||
this.closed = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"task": "PKG-MB-05",
|
||||
"sourceCommit": "5d0b128483a78a5374bb7163adc1421ba6a4d4c8",
|
||||
"sourceSha256LF": {
|
||||
"packages/artplayer-proxy-mediabunny/src/VideoEngine.js": "5ac862b0fd9c196dc6656f8908a214c1b3a0a53a7e85ff845d9068888831986b",
|
||||
"packages/artplayer-proxy-mediabunny/dist/artplayer-proxy-mediabunny.js": "01b88c915804e1c42b90c8c37aa948029472c7f47f6eb3a266629ed945be9fb1",
|
||||
"node_modules/mediabunny/src/media-sink.ts": "9fbec4b6b0ec6c613d8f546fa91d8f14aac1c7c4a53ec7ed14b31319d43510f2"
|
||||
},
|
||||
"sdkNote": "Installed 1.56.1 CanvasSink closes each VideoSample after drawing into its pooled canvas. Consumer owns the iterator return, not VideoFrame.close on WrappedCanvas."
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,80 @@
|
||||
# PKG-MB-05:视频迭代器、seek、晚帧与RAF
|
||||
|
||||
任务通过完整CI后标记done,并立即以专用本地提交交付。冻结来源为5d0b1284,源码与实际main哈希见
|
||||
mb-video-source.json;SDK仍为已锁定MediaBunny 1.56.1,未升级依赖。
|
||||
|
||||
## 实际实现
|
||||
|
||||
VideoEngine.js已迁移为严格TS并删除VideoEngine.d.ts桥。拆出video-frames.ts、
|
||||
video-renderer.ts、video-poster.ts,包内共20个生产TS模块;仅AudioEngine.d.ts仍是
|
||||
显式的临时JS边界。ARCHITECTURE.md记录文件地图和状态/资源归属。
|
||||
|
||||
- source/load、seek迭代和render活动分别拥有身份;旧load、第一/第二seek帧、能力查询和
|
||||
异步frame读取结果不得覆盖新source或seek。新source开始解析时即取消旧视频操作。
|
||||
- 加载能力检测期间seek等待该加载,不能把合法metadata一起取消;暂停状态的正常seek
|
||||
仍显示目标第一帧并保留第二帧。load/reset不等待废弃迭代器无限期返回。
|
||||
- 迭代器先同步脱离当前字段,再观察return;每个迭代器只释放一次。stop/start对同一
|
||||
pending next共享读取,旧finally不清掉新fetch状态。
|
||||
- RAF每实例单一归属,ID 0有效;暂停、销毁与事件回调重入后不再排程旧任务。
|
||||
未创建RAF的实例不能取消其他实例的ID 0。正常timeupdate/ended/stall事件顺序保持。
|
||||
- 海报请求有源身份和请求身份,替换、真正绘制帧、销毁时解除回调。
|
||||
- 活动后台解码失败停止渲染并通过协调器报告一次MediaError,音频清理异常不覆盖原错误。
|
||||
后台iterator return拒绝有观察和警告;显式stopIterator保持Promise拒绝。
|
||||
- 无2D context时在实际使用阶段报明确错误,替代旧的空值TypeError;不会伪造就绪。
|
||||
|
||||
默认dropLateFrames=false、容差/倍速公式、CanvasSink poolSize=2、contain、透明度保持。
|
||||
SDK的CanvasSink把VideoSample画入池化canvas后自行close;代理负责迭代器,不对WrappedCanvas
|
||||
调用VideoFrame.close,不增加每帧拷贝。公开factory/类型/Canvas门面与三种产物入口保持。
|
||||
|
||||
## 证据
|
||||
|
||||
38项新视频断言对5d0b1284实际main为7通过/31失败;候选全部通过。包专项146项通过。
|
||||
main/legacy各110项通过,106候选断言+4冻结正常对照。三格式构建与严格类型检查通过。
|
||||
最终浏览器结果归档并记录在mb-video-validation.json。54项由12普通真实播放、10真实
|
||||
解码帧竞态、9原生Stream取消、9synthetic RAF、11Windows WebKit能力对照、3无轨道拒绝组成。
|
||||
其中10个新竞态场景对旧main全部失败,另5能力对照通过;失败trace与报告完整保留。
|
||||
新场景使用实际SDK解析/解码、实际CanvasSink帧和浏览器时钟,只延迟一帧的交付来控制
|
||||
暂停/连续seek/切源时序。timeupdate回调暂停检查真实RAF,不是mock codec或纯事件出口测试。
|
||||
|
||||
开发时修正了测试宿主:控制音频seek来隔离视频测试;两个实例的RAF测试直接创建两个实际
|
||||
VideoEngine,避免测试helper重复返回同一canvas。旧32项测试中25失败,扩展到37项后30失败。
|
||||
初始复现读取必须在下一次返回done,避免旧实现缺陷造成测试自身无限循环;没有扩大超时。
|
||||
|
||||
## 后续
|
||||
|
||||
MB-06继续实际音频解码、时钟与节点释放;MB-07继续并发HLS选轨和UI拓扑;MB-08完善
|
||||
消费者声明矩阵。MB-09仍需要长播放、AV同步和完整核心/插件/设备组合。既有轨道但不可解码
|
||||
时的fallback还需MB-CAP-01/MB-READY-01评审,不以这次晚帧修复宣布所有能力问题已关闭。
|
||||
MPL通知、8082示例与真实安装包在MB-10;未推送、打tag或发布。
|
||||
|
||||
最终复核另复现了启动预取覆盖已排队第二帧,修复为消费该帧后再取下一帧,保持默认
|
||||
不丢帧意图及容差策略。新增第38项回归,旧main为7通过/31失败。首轮完整CI1306通过
|
||||
之后因这项实际源码修复重新构建、验证产物/浏览器并运行完整CI,不复用旧的绿色结果。
|
||||
|
||||
进一步使用两个实际解码VideoSample在进入CanvasSink池化转换前暂停交付,连续seek
|
||||
1→3→5后释放旧样本:当前帧timestamp仍属于5秒,但排队canvas像素校验从1220654181
|
||||
变成2730873815。此缺陷在已通过第二轮CI的实现上也复现,报告为mb05-pool-before-results。
|
||||
已改为每个seek独立CanvasSink池(仍2个canvas,无每帧拷贝),保留公开canvas实例;
|
||||
内部sink身份可变化。对应浏览器测试跟踪SDK原型以覆盖新sink,而非绑定一次旧实例。
|
||||
修复后像素稳定;最终54项浏览器及对应旧main15项对照另行归档。
|
||||
|
||||
构建中两次发生Windows copyfile UNKNOWN,仅覆盖已有docs/compiled legacy目标失败;
|
||||
源码和目标读取、r+打开及复制到cache均成功,磁盘空间充足。备份并移除该可再生目标,
|
||||
由正常yarn build重新生成后三格式构建成功,没有手改产物内容或扩大文件删除范围。
|
||||
首次54项浏览器所用main与重新完整构建的main哈希相同,仍有效;legacy重新运行110项检查。
|
||||
|
||||
进一步补充新迭代器保持断言:Firefox旧切源在某次运行中未多画旧帧,却已被旧清理
|
||||
清空新迭代器,原画面断言不足以发现。新增断言后最终旧main10个原生竞态均失败,
|
||||
5个能力对照通过;先前9失败/6通过报告另存,不把同一副作用的不同调度结果隐去。
|
||||
|
||||
最终浏览器清理断言改为按现有expect超时等待AudioContext真实closed状态;close本来是
|
||||
异步操作,固定两帧不足以判定失败。此处只调整测试等待,生产代码未改。末次完整CI
|
||||
1307项覆盖最终生产源码;之后单独重跑浏览器spec lint和全部54浏览器用例。旧main
|
||||
同样重跑15项以保留清理后的原始视频断言结论,未提高超时或跳过失败。
|
||||
|
||||
最终完整CI1307项(1152单元+14工程+141基线)通过,另44重复契约;310生产TS。
|
||||
源码、测试、产物与报告哈希已核验,三格式docs/compiled与dist逐字节一致。
|
||||
38视频断言在旧main为7通过/31失败;最终包专项146、main/legacy各110、浏览器54均通过。
|
||||
旧产物10个原生帧竞态失败均为绘制计数、帧像素、迭代器保留或RAF状态断言,不是超时:暂停后绘制2→10,
|
||||
旧seek结果在新seek/source后绘制2→3,timeupdate暂停后残留1个RAF;候选均无此残留。
|
||||
回退可revert专用提交,原始5d0b1284及基线保持可追溯。下一项PKG-MB-06继续音频引擎。
|
||||
@@ -54,3 +54,10 @@ VideoShim/EventTarget与主协调器已拆为严格TS模块,详情见
|
||||
无轨道容器由历史readyState 4改为code 4错误;有效媒体的正常顺序保持,重复就绪和
|
||||
失效操作结果被阻止。不要把synthetic RAF、受控竞态或Windows WebKit能力对照计为真实播放。
|
||||
音视频解码器仍通过两个显式声明桥连接,MB-05/06继续实际实现迁移和底层晚操作清理。
|
||||
|
||||
## MB-05 video ownership
|
||||
|
||||
VideoEngine与迭代器/渲染/海报职责已实际迁移TS,删除视频声明桥;只剩音频声明桥。
|
||||
执行yarn test:mediabunny及新增mediabunny-video.spec.js,按实际报告区分普通播放、
|
||||
真实帧竞态、能力对照和synthetic RAF。[变更](changes/2026-09-12-PKG-MB-05-video.md)与
|
||||
[验证](baselines/mb-video-validation.json)记录38个新断言、旧产物复现及真实解码场景。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 114 / doing 7 / blocked 0 / done 101 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 113 / doing 7 / blocked 0 / done 102 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -333,7 +333,7 @@
|
||||
| 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 | done |
|
||||
| PKG-MB-05 | artplayer-proxy-mediabunny<br>整理视频解码、seek 和帧释放 | PKG-MB-04 | VideoEngine 调度、队列、晚帧处理和资源释放 | 暂停/seek/切源无旧帧覆盖,默认丢帧策略不变 | H | todo |
|
||||
| PKG-MB-05 | artplayer-proxy-mediabunny<br>整理视频解码、seek 和帧释放 | PKG-MB-04 | VideoEngine 调度、队列、晚帧处理和资源释放 | 暂停/seek/切源无旧帧覆盖,默认丢帧策略不变 | H | done |
|
||||
| PKG-MB-06 | artplayer-proxy-mediabunny<br>整理音频解码、时钟与同步 | PKG-MB-04 | AudioEngine 时钟、缓冲和 AudioContext 归属 | AV sync、倍速、无音轨、静音/音量和暂停恢复通过 | H | todo |
|
||||
| PKG-MB-07 | artplayer-proxy-mediabunny<br>整理 HLS 配对轨道与 selector | PKG-MB-05, PKG-MB-06, CORE-14 | m3u8 配对、质量/音频选择及拓扑清理 | 实际选择高亮、切到无轨道来源清理、无重复 readiness | H | todo |
|
||||
| PKG-MB-08 | artplayer-proxy-mediabunny<br>完成 TS 与媒体能力声明 | PKG-MB-07, ENG-04, ENG-06 | 8 个自有 JS 模块迁移与 Result/shim 类型 | 解码器/Stream/DOM 类型清楚,旧 Option 和 art.mediabunny 使用保持 | H | todo |
|
||||
@@ -524,4 +524,5 @@
|
||||
- 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) [记录](changes/2026-09-12-PKG-MB-04-coordination.md) [记录](baselines/mb-coordination-validation.json)
|
||||
- PKG-MB-05: [记录](baselines/mb-video-source.json) [记录](changes/2026-09-12-PKG-MB-05-video.md) [记录](baselines/mb-video-validation.json)
|
||||
- PKG-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,19 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-MB-05完成:视频迭代器、seek、晚帧与RAF
|
||||
|
||||
VideoEngine实际迁移TS,拆出frame读取/释放、renderer与poster职责,删除视频声明桥。
|
||||
新source立即取消旧视频操作;seek与加载、暂停分别管理,旧帧不覆盖新状态;单实例RAF
|
||||
与海报回调有清理;保留默认丢帧策略并修复启动预取覆盖第二帧,以及多seek共享
|
||||
Canvas池导致排队帧像素被旧样本改写。包内20生产TS,
|
||||
只剩AudioEngine显式声明桥。38项新断言对旧main7通过/31失败、候选全通过;包专项
|
||||
146、main/legacy各110通过。最终54浏览器:12普通播放+10真实解码帧竞态+9Stream
|
||||
取消+9synthetic RAF+11能力对照+3无轨道拒绝。旧main10项真实帧竞态全部失败,
|
||||
候选全部通过,完整报告/trace归档。最终完整CI1307通过,另44重复契约,310生产TS。
|
||||
见[变更](changes/2026-09-12-PKG-MB-05-video.md)及[证据](baselines/mb-video-validation.json)。
|
||||
222项:102 done、7 doing、113 todo;立即专用本地提交,下一项MB-06音频引擎。
|
||||
音频、HLS拓扑、长播放/AV同步与codec能力剩余项继续,不构成npm发布验收。
|
||||
|
||||
## PKG-MB-04完成:主协调器TS与操作隔离
|
||||
|
||||
MediaBunnyEngine实际迁移TS,并拆分playback/readiness/HLS查询职责;正常接口和事件顺序
|
||||
|
||||
+4
-2
@@ -3648,7 +3648,7 @@
|
||||
"PKG-MB-07",
|
||||
"PKG-MB-09"
|
||||
],
|
||||
"compatibleResolution": "Reproduce pending operations and destroy/source-switch races before migrating each owner; preserve public event and promise behavior while preventing stale resource use. MB-02 now reproduces late load rejection and uncancelled waiting/loadstart timers after destroy in all three historical implementations using a controlled host. Other interleavings and full-core native behavior are not yet verified. Expanded MB-02 baseline reproduces play-after-pause/destroy, rejected-play false state, seek-after-destroy and retained successful-load timeout timers in all three actual historical implementations; Range and active-timeout normal controls remain separate. MB-03 candidate owns pending Input before track selection and cancels load/HEAD/timers on source replacement, timeout and destroy; source/main/legacy regressions pass. Pending play/seek, late decoder work and full readiness coordination remain MB-04/05/06, so this risk stays open. MB-04 shim checkpoint fixes synthetic RAF retention and terminal event-listener cleanup, including engine cleanup exceptions. Closing the event bridge does not prove pending engine operations stopped; coordination remains in progress. MB-04 now guards play/seek/replacement completion, preserves explicit pause, observes stale failures and isolates reentrant event sequences. Decoder-internal late frames/audio and HLS UI races remain MB-05/06/07; risk stays open.",
|
||||
"compatibleResolution": "Reproduce pending operations and destroy/source-switch races before migrating each owner; preserve public event and promise behavior while preventing stale resource use. MB-02 now reproduces late load rejection and uncancelled waiting/loadstart timers after destroy in all three historical implementations using a controlled host. Other interleavings and full-core native behavior are not yet verified. Expanded MB-02 baseline reproduces play-after-pause/destroy, rejected-play false state, seek-after-destroy and retained successful-load timeout timers in all three actual historical implementations; Range and active-timeout normal controls remain separate. MB-03 candidate owns pending Input before track selection and cancels load/HEAD/timers on source replacement, timeout and destroy; source/main/legacy regressions pass. Pending play/seek, late decoder work and full readiness coordination remain MB-04/05/06, so this risk stays open. MB-04 shim checkpoint fixes synthetic RAF retention and terminal event-listener cleanup, including engine cleanup exceptions. Closing the event bridge does not prove pending engine operations stopped; coordination remains in progress. MB-04 now guards play/seek/replacement completion, preserves explicit pause, observes stale failures and isolates reentrant event sequences. Decoder-internal late frames/audio and HLS UI races remain MB-05/06/07; risk stays open. MB-05 migrates VideoEngine and owns iterators, late first/second seek frames, pending reads, per-instance RAF and poster callbacks; same native race assertions fail on old main and pass on candidate. Independent CanvasSink pools also prevent multiple obsolete seek samples from overwriting queued current-frame pixels. Audio nodes, long-run AV sync and HLS UI topology remain MB-06/07/09.",
|
||||
"closureCriteria": "Historical failing interleavings and candidate cancellation/cleanup pass with real media and event evidence, including long playback and track topology.",
|
||||
"status": "open",
|
||||
"evidence": [
|
||||
@@ -3662,7 +3662,9 @@
|
||||
"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-coordination.md",
|
||||
"refactor/baselines/mb-coordination-validation.json"
|
||||
"refactor/baselines/mb-coordination-validation.json",
|
||||
"refactor/changes/2026-09-12-PKG-MB-05-video.md",
|
||||
"refactor/baselines/mb-video-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+6
-2
@@ -3482,11 +3482,15 @@
|
||||
"dependsOn": [
|
||||
"PKG-MB-04"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "VideoEngine 调度、队列、晚帧处理和资源释放",
|
||||
"acceptance": "暂停/seek/切源无旧帧覆盖,默认丢帧策略不变",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"baselines/mb-video-source.json",
|
||||
"changes/2026-09-12-PKG-MB-05-video.md",
|
||||
"baselines/mb-video-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-MB-06",
|
||||
|
||||
@@ -145,9 +145,10 @@ for (const implementation of implementations) {
|
||||
const video = canvas.engine.video
|
||||
if (video.videoSink) {
|
||||
const times = new WeakMap()
|
||||
const canvases = video.videoSink.canvases.bind(video.videoSink)
|
||||
video.videoSink.canvases = (...args) => {
|
||||
const iterator = canvases(...args)
|
||||
const prototype = Object.getPrototypeOf(video.videoSink)
|
||||
const canvases = prototype.canvases
|
||||
prototype.canvases = function (...args) {
|
||||
const iterator = canvases.apply(this, args)
|
||||
const next = iterator.next.bind(iterator)
|
||||
iterator.next = async (...values) => {
|
||||
const result = await next(...values)
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { mbCandidate } from '../helpers/mediabunny.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const implementation = await mbCandidate()
|
||||
for (const scenario of ['pause-frame', 'seek-source', 'seek-newer', 'timeupdate-pause', 'seek-pool']) {
|
||||
test(`MediaBunny ${implementation.name}: native video ownership ${scenario}`, async ({ page, browserName }, testInfo) => {
|
||||
await page.goto('/test/player.html?core=published')
|
||||
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports;
|
||||
${implementation.code}; window.mbFactory = module.exports.default || module.exports; })();` })
|
||||
let outcome = 'unverified'
|
||||
let result
|
||||
await page.evaluate(async () => {
|
||||
const listeners = new Map()
|
||||
window.mbEvents = []
|
||||
window.mbDraws = []
|
||||
window.mbFrames = new Set()
|
||||
const raf = requestAnimationFrame.bind(window)
|
||||
const cancel = cancelAnimationFrame.bind(window)
|
||||
window.mbFlush = () => new Promise(resolve => raf(() => raf(resolve)))
|
||||
window.requestAnimationFrame = (callback) => {
|
||||
const id = raf((time) => {
|
||||
window.mbFrames.delete(id)
|
||||
callback(time)
|
||||
})
|
||||
window.mbFrames.add(id)
|
||||
return id
|
||||
}
|
||||
window.cancelAnimationFrame = (id) => {
|
||||
window.mbFrames.delete(id)
|
||||
cancel(id)
|
||||
}
|
||||
window.mbHost = {
|
||||
constructor: window.Artplayer,
|
||||
option: { url: '', autoSize: false },
|
||||
template: { $player: document.querySelector('.player') },
|
||||
on(name, callback) {
|
||||
const list = listeners.get(name) || []
|
||||
list.push(callback)
|
||||
listeners.set(name, list)
|
||||
},
|
||||
emit(name, ...args) {
|
||||
window.mbEvents.push(name)
|
||||
for (const callback of listeners.get(name) || []) callback(...args)
|
||||
},
|
||||
}
|
||||
window.mbCanvas = window.mbFactory()(window.mbHost)
|
||||
const context = window.mbCanvas.engine.video.ctx
|
||||
const draw = context.drawImage.bind(context)
|
||||
context.drawImage = (...args) => {
|
||||
window.mbDraws.push({ time: window.mbCanvas.currentTime, paused: window.mbCanvas.paused })
|
||||
return draw(...args)
|
||||
}
|
||||
await window.mbCanvas.engine.load('/test/pattern.mp4')
|
||||
const button = document.createElement('button')
|
||||
button.id = 'mb-play'
|
||||
button.textContent = 'Play'
|
||||
button.onclick = () => {
|
||||
window.mbPlay = window.mbCanvas.play()
|
||||
}
|
||||
document.body.append(button)
|
||||
window.mbHold = (iterator) => {
|
||||
const next = iterator.next.bind(iterator)
|
||||
let first = true
|
||||
iterator.next = async (...args) => {
|
||||
const value = await next(...args)
|
||||
if (first) {
|
||||
first = false
|
||||
window.mbHeldTimestamp = value.value?.timestamp
|
||||
window.mbHeldReads = 1
|
||||
return new Promise((resolve) => {
|
||||
window.mbRelease = () => resolve(value)
|
||||
})
|
||||
}
|
||||
window.mbHeldReads++
|
||||
return value
|
||||
}
|
||||
return iterator
|
||||
}
|
||||
})
|
||||
try {
|
||||
const capabilities = await page.evaluate(() => ({ VideoDecoder: typeof VideoDecoder, AudioContext: typeof AudioContext, webkitAudioContext: typeof window.webkitAudioContext }))
|
||||
if (capabilities.AudioContext === 'undefined' && capabilities.webkitAudioContext === 'undefined') {
|
||||
expect(browserName).toBe('webkit')
|
||||
expect(capabilities.VideoDecoder).toBe('undefined')
|
||||
expect(await page.evaluate(() => window.mbCanvas.error?.code)).toBe(4)
|
||||
expect(await page.evaluate(() => window.mbCanvas.readyState)).toBeLessThan(4)
|
||||
outcome = 'unsupported-capability-control'
|
||||
return
|
||||
}
|
||||
expect(await page.evaluate(() => window.mbCanvas.error)).toBeNull()
|
||||
expect(await page.evaluate(() => window.mbCanvas.readyState)).toBe(4)
|
||||
expect(await page.evaluate(() => [window.mbCanvas.videoWidth, window.mbCanvas.videoHeight])).toEqual([320, 180])
|
||||
if (scenario === 'pause-frame') {
|
||||
await page.evaluate(() => window.mbHold(window.mbCanvas.engine.video.videoIterator))
|
||||
await page.click('#mb-play')
|
||||
await expect.poll(() => page.evaluate(() => typeof window.mbRelease)).toBe('function')
|
||||
await expect.poll(() => page.evaluate(() => window.mbCanvas.currentTime > window.mbHeldTimestamp + 0.1)).toBe(true)
|
||||
result = await page.evaluate(async () => {
|
||||
window.mbCanvas.pause()
|
||||
const before = window.mbDraws.length
|
||||
window.mbRelease()
|
||||
await window.mbPlay
|
||||
await window.mbFlush()
|
||||
return { before, after: window.mbDraws.length, paused: window.mbCanvas.paused, frames: window.mbFrames.size }
|
||||
})
|
||||
expect(result.after).toBe(result.before)
|
||||
expect(result.paused).toBe(true)
|
||||
expect(result.frames).toBe(0)
|
||||
}
|
||||
else if (scenario === 'seek-pool') {
|
||||
await page.evaluate(() => {
|
||||
const prototype = Object.getPrototypeOf(window.mbCanvas.engine.video.videoSink._videoSampleSink)
|
||||
const samples = prototype.samples
|
||||
window.mbSampleReleases = {}
|
||||
prototype.samples = function (...args) {
|
||||
const iterator = samples.apply(this, args)
|
||||
const next = iterator.next.bind(iterator)
|
||||
let first = true
|
||||
iterator.next = async (...values) => {
|
||||
const result = await next(...values)
|
||||
if (first && [1, 3].includes(args[0]) && !result.done) {
|
||||
first = false
|
||||
return new Promise((resolve) => {
|
||||
window.mbSampleReleases[args[0]] = () => resolve(result)
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
return iterator
|
||||
}
|
||||
window.mbOldFirst = window.mbCanvas.engine.seek(1)
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => typeof window.mbSampleReleases[1])).toBe('function')
|
||||
await page.evaluate(() => {
|
||||
window.mbOldSecond = window.mbCanvas.engine.seek(3)
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => typeof window.mbSampleReleases[3])).toBe('function')
|
||||
result = await page.evaluate(async () => {
|
||||
await window.mbCanvas.engine.seek(5)
|
||||
const frame = window.mbCanvas.engine.video.nextFrame
|
||||
const fingerprint = () => {
|
||||
let hash = 2166136261
|
||||
for (const value of frame.canvas.getContext('2d').getImageData(0, 0, frame.canvas.width, frame.canvas.height).data)
|
||||
hash = Math.imul(hash ^ value, 16777619) >>> 0
|
||||
return hash
|
||||
}
|
||||
const before = fingerprint()
|
||||
window.mbSampleReleases[1]()
|
||||
window.mbSampleReleases[3]()
|
||||
await Promise.all([window.mbOldFirst, window.mbOldSecond])
|
||||
await window.mbFlush()
|
||||
return { before, after: fingerprint(), timestamp: frame.timestamp, currentFrameUnchanged: frame === window.mbCanvas.engine.video.nextFrame, time: window.mbCanvas.currentTime }
|
||||
})
|
||||
expect(result.after).toBe(result.before)
|
||||
expect(result.currentFrameUnchanged).toBe(true)
|
||||
expect(result.timestamp).toBeGreaterThan(5)
|
||||
expect(result.time).toBe(5)
|
||||
}
|
||||
else if (scenario.startsWith('seek-')) {
|
||||
await page.evaluate(() => {
|
||||
const prototype = Object.getPrototypeOf(window.mbCanvas.engine.video.videoSink)
|
||||
const canvases = prototype.canvases
|
||||
prototype.canvases = function (...args) {
|
||||
const iterator = canvases.apply(this, args)
|
||||
return args[0] === 3 ? window.mbHold(iterator) : iterator
|
||||
}
|
||||
window.mbSeek = window.mbCanvas.engine.seek(3)
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => typeof window.mbRelease)).toBe('function')
|
||||
result = await page.evaluate(async (scenario) => {
|
||||
if (scenario === 'seek-source')
|
||||
await window.mbCanvas.engine.load('/test/pattern.mp4?replacement=1')
|
||||
else await window.mbCanvas.engine.seek(5)
|
||||
const iterator = window.mbCanvas.engine.video.videoIterator
|
||||
const before = window.mbDraws.length
|
||||
window.mbRelease()
|
||||
await window.mbSeek
|
||||
await window.mbFlush()
|
||||
return { before, after: window.mbDraws.length, heldReads: window.mbHeldReads, currentIteratorKept: Boolean(iterator) && iterator === window.mbCanvas.engine.video.videoIterator, frames: window.mbFrames.size, time: window.mbCanvas.currentTime, error: window.mbCanvas.error }
|
||||
}, scenario)
|
||||
expect(result.after).toBe(result.before)
|
||||
expect(result.heldReads).toBe(1)
|
||||
expect(result.currentIteratorKept).toBe(true)
|
||||
expect(result.frames).toBe(0)
|
||||
expect(result.error).toBeNull()
|
||||
expect(result.time).toBe(scenario === 'seek-source' ? 0 : 5)
|
||||
}
|
||||
else {
|
||||
await page.evaluate(() => window.mbCanvas.events.addEventListener('timeupdate', () => window.mbCanvas.pause()))
|
||||
await page.click('#mb-play')
|
||||
await expect.poll(() => page.evaluate(() => window.mbEvents.includes('video:timeupdate'))).toBe(true)
|
||||
result = await page.evaluate(async () => {
|
||||
await window.mbFlush()
|
||||
return { paused: window.mbCanvas.paused, frames: window.mbFrames.size }
|
||||
})
|
||||
expect(result).toEqual({ paused: true, frames: 0 })
|
||||
}
|
||||
outcome = 'native-decoded-video-race'
|
||||
}
|
||||
finally {
|
||||
const state = await page.evaluate(async () => {
|
||||
const canvas = window.mbCanvas
|
||||
window.mbRelease?.()
|
||||
for (const release of Object.values(window.mbSampleReleases || {})) release()
|
||||
const context = canvas.engine.audio.audioContext
|
||||
window.mbCleanupContext = context
|
||||
window.mbHost.emit('destroy')
|
||||
await window.mbFlush()
|
||||
return { events: window.mbEvents, draws: window.mbDraws.length, error: canvas.error, audioContext: context?.state || 'not-created', frames: window.mbFrames.size }
|
||||
})
|
||||
expect(state.frames).toBe(0)
|
||||
await expect.poll(() => page.evaluate(() => window.mbCleanupContext?.state || 'not-created')).toMatch(/^(?:closed|not-created)$/)
|
||||
state.audioContext = await page.evaluate(() => window.mbCleanupContext?.state || 'not-created')
|
||||
await testInfo.attach('mediabunny-video-ownership', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), scenario, outcome, result, state, scope: 'Actual SDK MP4 decoding and browser clock/RAF; frame delivery or two raw VideoSample deliveries before CanvasSink conversion control interleaving. Source/seek ownership, pause and timeupdate reentry verified; not long-run AV sync or a mock codec.' }) })
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Real compiled video engine with controlled SDK iterator interleavings.
|
||||
import test from 'node:test'
|
||||
import { mbCandidate, mbEnvironment } from './helpers/mediabunny.js'
|
||||
|
||||
const implementation = await mbCandidate()
|
||||
const flush = () => new Promise(resolve => setImmediate(resolve))
|
||||
function deferred() {
|
||||
let resolve, reject
|
||||
const promise = new Promise((yes, no) => {
|
||||
resolve = yes
|
||||
reject = no
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
const frame = (name, timestamp = 0) => ({ canvas: { name }, timestamp, duration: 0.1 })
|
||||
const item = value => ({ value, done: !value })
|
||||
function iterator(values = []) {
|
||||
let reads = 0
|
||||
let returns = 0
|
||||
return {
|
||||
next: async () => {
|
||||
reads++
|
||||
return item(values.shift())
|
||||
},
|
||||
return: async () => {
|
||||
returns++
|
||||
return item()
|
||||
},
|
||||
get reads() { return reads },
|
||||
get returns() { return returns },
|
||||
}
|
||||
}
|
||||
function environment(options = {}) {
|
||||
const env = mbEnvironment(implementation)
|
||||
env.eventNames.push('timeupdate', 'ended', 'waiting', 'playing', 'canplay')
|
||||
const canvas = env.factory(options)(env.art)
|
||||
const video = canvas.engine.video
|
||||
canvas.engine.audio.seek = async () => {}
|
||||
const draws = []
|
||||
env.context.drawImage = source => draws.push(source.name)
|
||||
video.audioClock = { currentTime: 1 }
|
||||
return { ...env, canvas, video, draws }
|
||||
}
|
||||
|
||||
test('MediaBunny video preserves first seek frame and queues the second with exact iterator timestamp', async () => {
|
||||
const env = environment()
|
||||
const frames = iterator([frame('first', 3), frame('second', 3.1)])
|
||||
env.video.videoSink = { canvases: (time) => {
|
||||
assert.equal(time, 3)
|
||||
return frames
|
||||
} }
|
||||
await env.video.seek(3)
|
||||
assert.deepEqual(env.draws, ['first'])
|
||||
assert.equal(env.video.nextFrame.canvas.name, 'second')
|
||||
assert.equal(frames.reads, 2)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny video detaches iterator before awaiting release, and releases it once', async () => {
|
||||
const env = environment()
|
||||
const release = deferred()
|
||||
let returns = 0
|
||||
env.video.videoIterator = { return: () => {
|
||||
returns++
|
||||
return release.promise
|
||||
} }
|
||||
const closing = env.video.stopIterator()
|
||||
assert.equal(env.video.videoIterator, null)
|
||||
const again = env.video.stopIterator()
|
||||
release.resolve(item())
|
||||
await Promise.all([closing, again])
|
||||
assert.equal(returns, 1)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny old iterator return cannot erase a newer iterator', async () => {
|
||||
const env = environment()
|
||||
const release = deferred()
|
||||
env.video.videoIterator = { return: () => release.promise }
|
||||
const closing = env.video.stopIterator()
|
||||
const next = iterator()
|
||||
env.video.videoIterator = next
|
||||
release.resolve(item())
|
||||
await closing
|
||||
assert.equal(env.video.videoIterator, next)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
for (const ending of ['stop', 'destroy']) {
|
||||
test(`MediaBunny pending render frame cannot draw after ${ending}`, async () => {
|
||||
const env = environment()
|
||||
const pending = deferred()
|
||||
let reads = 0
|
||||
env.video.videoIterator = { next: () => ++reads === 1 ? pending.promise : Promise.resolve(item()), return: async () => item() }
|
||||
const reading = env.video.updateNextFrame(env.video.asyncId)
|
||||
if (ending === 'stop')
|
||||
env.video.stop()
|
||||
else env.art.emit('destroy')
|
||||
pending.resolve(item(frame('obsolete')))
|
||||
await reading
|
||||
assert.deepEqual(env.draws, [])
|
||||
assert.equal(env.video.nextFrame, null)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
for (const ending of ['destroy', 'seek']) {
|
||||
test(`MediaBunny pending first seek frame cannot consume or overwrite frames after ${ending}`, async () => {
|
||||
const env = environment()
|
||||
const pending = deferred()
|
||||
let oldReads = 0
|
||||
const old = { next: async () => {
|
||||
oldReads++
|
||||
return oldReads === 1 ? pending.promise : item(frame('old-second'))
|
||||
}, return: async () => item() }
|
||||
const latest = iterator([frame('new-first'), frame('new-second')])
|
||||
env.video.videoSink = { canvases: time => time === 1 ? old : latest }
|
||||
const seeking = env.video.seek(1)
|
||||
await flush()
|
||||
if (ending === 'destroy')
|
||||
env.art.emit('destroy')
|
||||
else await env.video.seek(2)
|
||||
pending.resolve(item(frame('old-first')))
|
||||
await seeking
|
||||
assert.deepEqual(env.draws, ending === 'destroy' ? [] : ['new-first'])
|
||||
assert.equal(oldReads, 1)
|
||||
if (ending === 'seek')
|
||||
assert.equal(env.video.nextFrame.canvas.name, 'new-second')
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny a new load does not wait for obsolete iterator disposal', async () => {
|
||||
const env = environment()
|
||||
const release = deferred()
|
||||
env.video.videoIterator = { return: () => release.promise }
|
||||
let metadata = 0
|
||||
const loading = env.video.load({ input: {}, videoTrack: null, duration: 9 }, () => metadata++)
|
||||
await flush()
|
||||
assert.equal(metadata, 1)
|
||||
assert.equal(env.video.duration, 9)
|
||||
release.resolve(item())
|
||||
await loading
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
for (const ending of ['destroy', 'source']) {
|
||||
test(`MediaBunny stale codec capability result cannot alter metadata after ${ending}`, async () => {
|
||||
const env = environment()
|
||||
const capability = deferred()
|
||||
let metadata = 0
|
||||
const loading = env.video.load({ input: {}, videoTrack: { codec: 'avc', canDecode: () => capability.promise }, duration: 8 }, () => metadata++)
|
||||
await flush()
|
||||
if (ending === 'destroy')
|
||||
env.art.emit('destroy')
|
||||
else await env.video.load({ input: {}, videoTrack: null, duration: 4 })
|
||||
env.canvas.width = 42
|
||||
capability.resolve(false)
|
||||
await loading
|
||||
assert.equal(metadata, 0)
|
||||
assert.equal(env.canvas.width, 42)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
for (const ending of ['stop', 'destroy']) {
|
||||
test(`MediaBunny timeupdate listener ${ending} prevents rendering and RAF rescheduling`, () => {
|
||||
const env = environment()
|
||||
env.video.nextFrame = frame('due')
|
||||
env.video.events.addEventListener('timeupdate', () => ending === 'stop' ? env.video.stop() : env.art.emit('destroy'))
|
||||
env.video.render()
|
||||
assert.deepEqual(env.draws, [])
|
||||
assert.equal(env.frames.size, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny repeated video start owns only one native RAF including id zero', async () => {
|
||||
const env = environment()
|
||||
env.video.start({ currentTime: 0 })
|
||||
assert.equal(env.frames.size, 1)
|
||||
env.video.start({ currentTime: 0 })
|
||||
assert.equal(env.frames.size, 1)
|
||||
env.video.stop()
|
||||
assert.equal(env.frames.size, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny already queued RAF callback stays inert after stop', () => {
|
||||
const env = environment()
|
||||
env.video.start({ currentTime: 0 })
|
||||
const callback = [...env.frames.values()][0]
|
||||
env.video.stop()
|
||||
env.emitted.length = 0
|
||||
callback()
|
||||
assert.deepEqual(env.emitted, [])
|
||||
assert.equal(env.frames.size, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
for (const ending of ['destroy', 'source']) {
|
||||
test(`MediaBunny poster callback cannot overwrite canvas after ${ending}`, async () => {
|
||||
const env = environment({ poster: 'poster.jpg' })
|
||||
const images = []
|
||||
env.globals.Image = class {
|
||||
constructor() {
|
||||
this.name = 'poster'
|
||||
this.naturalWidth = 320
|
||||
this.naturalHeight = 180
|
||||
images.push(this)
|
||||
}
|
||||
}
|
||||
env.video.drawPoster()
|
||||
const callback = images[0].onload
|
||||
if (ending === 'destroy') {
|
||||
env.art.emit('destroy')
|
||||
}
|
||||
else {
|
||||
env.video.poster = ''
|
||||
await env.video.load({ input: {}, videoTrack: null, duration: 9 })
|
||||
}
|
||||
env.canvas.width = 42
|
||||
callback()
|
||||
assert.deepEqual(env.draws, [])
|
||||
assert.equal(env.canvas.width, 42)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
for (const dropLateFrames of [false, true]) {
|
||||
test(`MediaBunny video retains default/tolerance frame selection with dropLateFrames=${dropLateFrames}`, async () => {
|
||||
const env = environment({ dropLateFrames, avSyncTolerance: 0.12 })
|
||||
env.video.playbackRate = 2
|
||||
env.video.videoIterator = iterator([frame('late', 0.8), frame('near', 0.97), frame('future', 1.2)])
|
||||
await env.video.updateNextFrame(env.video.asyncId)
|
||||
assert.deepEqual(env.draws, dropLateFrames ? ['near'] : ['late', 'near'])
|
||||
assert.equal(env.video.nextFrame.canvas.name, 'future')
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny waiting listener pause prevents a new RAF', () => {
|
||||
const env = environment()
|
||||
env.video.duration = 8
|
||||
env.video.events.addEventListener('waiting', () => env.video.stop())
|
||||
env.video.render()
|
||||
assert.equal(env.frames.size, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny stalled canplay listener pause suppresses playing and another RAF', () => {
|
||||
const env = environment()
|
||||
env.video.stalled = true
|
||||
env.video.nextFrame = frame('due')
|
||||
env.video.events.addEventListener('canplay', () => env.video.stop())
|
||||
env.video.render()
|
||||
assert.equal(env.emitted.some(event => event.name === 'video:playing'), false)
|
||||
assert.equal(env.frames.size, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny seek during pending capability detection preserves the current load metadata', async () => {
|
||||
const env = environment()
|
||||
const capability = deferred()
|
||||
let metadata = 0
|
||||
const loading = env.video.load({ input: {}, videoTrack: { codec: 'avc', canDecode: () => capability.promise }, duration: 8 }, () => metadata++)
|
||||
await flush()
|
||||
const seeking = env.video.seek(3)
|
||||
capability.resolve(false)
|
||||
await Promise.all([loading, seeking])
|
||||
assert.equal(metadata, 1)
|
||||
assert.equal(env.video.duration, 8)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny new source cancels a pending seek before its own track selection finishes', async () => {
|
||||
const env = environment()
|
||||
const pending = deferred()
|
||||
let reads = 0
|
||||
env.video.videoSink = { canvases: () => ({ next: async () => ++reads === 1 ? pending.promise : item(frame('second')), return: async () => item() }) }
|
||||
const seeking = env.canvas.engine.seek(2)
|
||||
await flush()
|
||||
const source = deferred()
|
||||
env.canvas.engine.performLoad = () => source.promise
|
||||
const loading = env.canvas.engine.load('new.mp4')
|
||||
pending.resolve(item(frame('old-source')))
|
||||
await seeking
|
||||
await flush()
|
||||
assert.deepEqual(env.draws, [])
|
||||
source.resolve()
|
||||
await loading
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny paused seek still renders its requested first frame', async () => {
|
||||
const env = environment()
|
||||
env.video.stop()
|
||||
env.video.videoSink = { canvases: () => iterator([frame('target'), frame('next')]) }
|
||||
await env.canvas.engine.seek(3)
|
||||
assert.deepEqual(env.draws, ['target'])
|
||||
assert.equal(env.canvas.paused, true)
|
||||
assert.equal(env.frames.size, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny late seek failure cannot release the current iterator', async () => {
|
||||
const env = environment()
|
||||
const pending = deferred()
|
||||
const latest = iterator([frame('current'), frame('next')])
|
||||
env.video.videoSink = { canvases: time => time === 1 ? { next: () => pending.promise, return: async () => item() } : latest }
|
||||
const old = env.video.seek(1)
|
||||
await flush()
|
||||
await env.video.seek(2)
|
||||
pending.reject(new Error('old decoder failed'))
|
||||
await old
|
||||
assert.equal(env.video.videoIterator, latest)
|
||||
assert.equal(latest.returns, 0)
|
||||
assert.deepEqual(env.draws, ['current'])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny stale transparency rejection is observed after source replacement', async () => {
|
||||
const env = environment()
|
||||
const pending = deferred()
|
||||
const loading = env.video.load({ input: {}, videoTrack: { codec: 'avc', canDecode: async () => true, canBeTransparent: () => pending.promise }, duration: 8 })
|
||||
await flush()
|
||||
await env.video.load({ input: {}, videoTrack: null, duration: 4 })
|
||||
pending.reject(new Error('old capability failed'))
|
||||
await loading
|
||||
assert.equal(env.video.duration, 4)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny old frame fetch completion cannot unlock a newer pending fetch', async () => {
|
||||
const env = environment()
|
||||
const old = deferred()
|
||||
const latest = deferred()
|
||||
env.video.videoIterator = { next: () => old.promise, return: async () => item() }
|
||||
const reading = env.video.updateNextFrame(env.video.asyncId)
|
||||
await flush()
|
||||
env.video.stop()
|
||||
env.video.videoIterator = { next: () => latest.promise, return: async () => item() }
|
||||
env.video.start({ currentTime: 1 })
|
||||
await flush()
|
||||
old.resolve(item(frame('obsolete')))
|
||||
await reading
|
||||
assert.equal(env.video.isFetching, true)
|
||||
latest.resolve(item(frame('future', 3)))
|
||||
await flush()
|
||||
assert.equal(env.video.isFetching, false)
|
||||
assert.equal(env.video.nextFrame.canvas.name, 'future')
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny stop/start shares a pending read without concurrent iterator.next calls', async () => {
|
||||
const env = environment()
|
||||
const pending = deferred()
|
||||
let reads = 0
|
||||
env.video.videoIterator = { next: () => {
|
||||
reads++
|
||||
return pending.promise
|
||||
}, return: async () => item() }
|
||||
const old = env.video.updateNextFrame(env.video.asyncId)
|
||||
await flush()
|
||||
env.video.stop()
|
||||
env.video.start({ currentTime: 1 })
|
||||
await flush()
|
||||
assert.equal(reads, 1)
|
||||
pending.resolve(item(frame('future', 2)))
|
||||
await old
|
||||
await flush()
|
||||
assert.equal(env.video.nextFrame.canvas.name, 'future')
|
||||
assert.equal(reads, 1)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny renderer observes decoder failure, stops and reports one media error', async () => {
|
||||
const env = environment()
|
||||
const failure = new Error('decode failed')
|
||||
const active = iterator()
|
||||
active.next = async () => {
|
||||
throw failure
|
||||
}
|
||||
env.video.videoIterator = active
|
||||
env.video.start({ currentTime: 1 })
|
||||
await flush()
|
||||
assert.deepEqual({ ...env.canvas.error }, { code: 4, message: failure.message })
|
||||
assert.equal(env.emitted.filter(event => event.name === 'video:error').length, 1)
|
||||
assert.equal(env.frames.size, 0)
|
||||
assert.equal(active.returns, 1)
|
||||
assert.equal(env.video.videoIterator, null)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny explicit iterator release preserves rejection after detachment', async () => {
|
||||
const env = environment()
|
||||
const failure = new Error('release failed')
|
||||
env.video.videoIterator = { return: async () => {
|
||||
throw failure
|
||||
} }
|
||||
await assert.rejects(env.video.stopIterator(), error => error === failure)
|
||||
assert.equal(env.video.videoIterator, null)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny normal ended sequence remains timeupdate, ended, pause, canplay', () => {
|
||||
const env = environment()
|
||||
env.video.duration = 1
|
||||
env.video.render()
|
||||
assert.deepEqual(env.emitted.map(event => event.name), ['video:timeupdate', 'video:ended', 'video:pause', 'video:canplay'])
|
||||
assert.equal(env.frames.size, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny ended listener restart suppresses old pause/canplay without canceling its new RAF', () => {
|
||||
const env = environment()
|
||||
env.video.duration = 1
|
||||
env.video.events.addEventListener('ended', () => env.video.start({ currentTime: 0 }))
|
||||
env.video.render()
|
||||
assert.deepEqual(env.emitted.map(event => event.name), ['video:timeupdate', 'video:ended'])
|
||||
assert.equal(env.frames.size, 1)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny inactive instance cleanup cannot cancel another instance RAF id zero', () => {
|
||||
const env = environment()
|
||||
const other = new env.video.constructor({ canvas: { width: 0, height: 0 }, ctx: env.context, events: env.video.events })
|
||||
env.video.start({ currentTime: 0 })
|
||||
assert.equal(env.frames.size, 1)
|
||||
other.destroy()
|
||||
assert.equal(env.frames.size, 1)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny decoder failure remains observable if audio cleanup throws', async () => {
|
||||
const env = environment()
|
||||
env.globals.console = { ...console, warn() {} }
|
||||
env.canvas.engine.paused = false
|
||||
env.canvas.engine.audio.pause = () => {
|
||||
throw new Error('audio cleanup failed')
|
||||
}
|
||||
env.video.videoIterator = { next: async () => {
|
||||
throw new Error('original decode failure')
|
||||
}, return: async () => item() }
|
||||
env.video.start({ currentTime: 1 })
|
||||
await flush()
|
||||
env.canvas.engine.audio.pause = () => {}
|
||||
assert.equal(env.canvas.error.message, 'original decode failure')
|
||||
assert.equal(env.canvas.paused, true)
|
||||
assert.equal(env.frames.size, 0)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny destroy observes iterator release failure and keeps terminal references cleared', async () => {
|
||||
const env = environment()
|
||||
const warnings = []
|
||||
env.globals.console = { ...console, warn: (...values) => warnings.push(values) }
|
||||
const failure = new Error('iterator return failed')
|
||||
env.video.videoIterator = { return: async () => {
|
||||
throw failure
|
||||
} }
|
||||
env.art.emit('destroy')
|
||||
await flush()
|
||||
assert.equal(warnings.length, 1)
|
||||
assert.equal(warnings[0][1], failure)
|
||||
assert.equal(env.video.videoIterator, null)
|
||||
assert.equal(env.video.nextFrame, null)
|
||||
assert.equal(env.video.input, null)
|
||||
assert.equal(env.video.videoSink, null)
|
||||
})
|
||||
|
||||
for (const ending of ['destroy', 'seek']) {
|
||||
test(`MediaBunny pending second seek frame cannot overwrite the current image after ${ending}`, async () => {
|
||||
const env = environment()
|
||||
const second = deferred()
|
||||
let reads = 0
|
||||
const old = { next: async () => ++reads === 1 ? item(frame('old-first')) : second.promise, return: async () => item() }
|
||||
env.video.videoSink = { canvases: time => time === 1 ? old : iterator([frame('new-first'), frame('new-second')]) }
|
||||
const seeking = env.video.seek(1)
|
||||
await flush()
|
||||
assert.equal(reads, 2)
|
||||
if (ending === 'destroy')
|
||||
env.art.emit('destroy')
|
||||
else await env.video.seek(2)
|
||||
second.resolve(item(frame('old-second')))
|
||||
await seeking
|
||||
assert.deepEqual(env.draws, ending === 'destroy' ? [] : ['new-first'])
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
}
|
||||
|
||||
test('MediaBunny missing 2D context rejects loading with a descriptive error', async () => {
|
||||
const env = environment()
|
||||
env.video.ctx = null
|
||||
await assert.rejects(env.video.load({ input: {}, videoTrack: null, duration: 8 }), /Canvas 2D context is unavailable/)
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
|
||||
test('MediaBunny start retains the queued second frame until its clock time before fetching another', async () => {
|
||||
const env = environment()
|
||||
const frames = iterator([frame('third', 1)])
|
||||
env.video.nextFrame = frame('second', 0.5)
|
||||
env.video.videoIterator = frames
|
||||
const clock = { currentTime: 0 }
|
||||
env.video.start(clock)
|
||||
await flush()
|
||||
assert.equal(frames.reads, 0)
|
||||
assert.equal(env.video.nextFrame.canvas.name, 'second')
|
||||
clock.currentTime = 0.5
|
||||
env.video.render()
|
||||
await flush()
|
||||
assert.deepEqual(env.draws, ['second'])
|
||||
assert.equal(frames.reads, 1)
|
||||
assert.equal(env.video.nextFrame.canvas.name, 'third')
|
||||
env.art.emit('destroy')
|
||||
})
|
||||
Reference in new issue
Block a user