11032 lines
400 KiB
JavaScript
11032 lines
400 KiB
JavaScript
var f={APPLY:"apply",CALLBACK:"callback",PING:"ping",PONG:"pong"},b={PROVIDER:"provider",INJECTOR:"injector"},E=e=>{let n=!!e?.type&&Object.values(f).includes(e.type),t=!!e?.id&&typeof e.id=="string",a=!!e?.path&&Array.isArray(e.path)&&e.path.every(p=>typeof p=="string"),i=!!e?.sender&&Object.values(b).includes(e.sender),r=e?.callbackIds===void 0||Array.isArray(e.callbackIds)&&e.callbackIds.every(p=>typeof p=="string"),m=e?.args===void 0||Array.isArray(e.args),s=e?.error===void 0||typeof e.error=="string",o=!!e?.meta&&e.meta!==null&&typeof e.meta=="object",c=!!e?.namespace&&typeof e.namespace=="string",u=!!e?.timeStamp&&typeof e.timeStamp=="number";return n&&t&&a&&i&&r&&m&&s&&o&&c&&u},N=()=>[void 0,void 0,void 0,void 0].map(()=>Math.floor(Math.random()*(2**53-1)).toString(16)).join("-"),C=N,V=(e,n,...t)=>{let a=setTimeout(()=>{clearTimeout(a),e(...t),a=setInterval(e,n,...t)},0);return()=>clearInterval(a)},W=V,H=(e,n)=>n!==void 0&&e instanceof n,d=H,z=e=>{let n=new WeakSet,t=a=>!a||typeof a!="object"||n.has(a)?[]:(n.add(a),d(a,globalThis.ArrayBuffer)||d(a,globalThis.MessagePort)||d(a,globalThis.ImageBitmap)||d(a,globalThis.OffscreenCanvas)||d(a,globalThis.AudioData)||d(a,globalThis.VideoFrame)||d(a,globalThis.RTCDataChannel)||d(a,globalThis.MediaSourceHandle)||d(a,globalThis.MIDIAccess)||d(a,globalThis.MediaStreamTrack)||d(a,globalThis.ReadableStream)||d(a,globalThis.WritableStream)||d(a,globalThis.TransformStream)?[a]:Array.isArray(a)?a.flatMap(t):Object.values(a).flatMap(t));return t(e)},w=z,A=Symbol("PROXY_MARKER"),G=async(e,n)=>{let t,a,i=new Set,r=new Promise((s,o)=>{t=W(async()=>{try{let c=C(),u=await e.onMessage(l=>{if(!E(l))return;let g=l;g.namespace===n.namespace&&g.sender===b.PROVIDER&&g.type===f.PONG&&g.id===c&&s()});u&&i.add(u);let p={type:f.PING,sender:b.INJECTOR,id:c,path:[],meta:{},namespace:n.namespace,timeStamp:Date.now()};e.sendMessage(p,n.transfer?w(p):[])}catch(c){o(c)}},n.heartbeatInterval)}),m=new Promise((s,o)=>{let c=setTimeout(()=>o(Error(`Provider unavailable: heartbeat check timeout ${n.heartbeatTimeout}ms.`)),n.heartbeatTimeout);a=()=>clearTimeout(c)});await Promise.race([r,m]).finally(()=>{t(),a(),i.forEach(s=>s()),i.clear()})},j=(e,n,t)=>(n.onMessage(async a=>{if(!E(a))return;let i=a;if(i.namespace===t.namespace&&i.sender===b.INJECTOR)switch(i.type){case f.PING:{let r={type:f.PONG,sender:b.PROVIDER,id:i.id,path:i.path,meta:i.meta,namespace:t.namespace,timeStamp:Date.now()};n.sendMessage(r,t.transfer?w(r):[]);break}case f.APPLY:{try{let m=i.args?.map(s=>i.callbackIds?.includes(s)?(...o)=>{let c={type:f.CALLBACK,sender:b.PROVIDER,id:s,path:i.path,meta:i.meta,data:o,namespace:t.namespace,timeStamp:Date.now()};n.sendMessage(c,t.transfer?w(c):[])}:s);i.data=await(i.path?.reduce((s,o)=>s[o],e)).apply(e,m||[])}catch(m){i.error=m.message}let r={type:f.APPLY,sender:b.PROVIDER,id:i.id,path:i.path,data:i.data,error:i.error,meta:i.meta,namespace:t.namespace,timeStamp:Date.now()};n.sendMessage(r,t.transfer?w(r):[]);break}}}),e),q=(e,n,t)=>{let a=(i,r)=>new Proxy(i,{get(s,o,c){if(s[A])return Reflect.get(s,o,c);let u=()=>{};return u[A]=!0,a(u,[...r,o])},apply(s,o,c){return new Promise(async(u,p)=>{try{t.heartbeatCheck&&await G(n,t);let l=[],g=c.map(y=>{if(typeof y=="function"){let h=C();return l.push(h),n.onMessage(L=>{if(!E(L))return;let k=L;k.namespace===t.namespace&&k.sender===b.PROVIDER&&k.type===f.CALLBACK&&k.id===h&&y(...k.data)}),h}else return y}),D=C(),U=await n.onMessage(y=>{if(!E(y))return;let h=y;h.namespace===t.namespace&&h.sender===b.PROVIDER&&h.type===f.APPLY&&h.id===D&&(h.error?p(Error(h.error)):u(h.data),U?.())}),M={type:f.APPLY,sender:b.INJECTOR,id:D,path:r,args:g,meta:{},callbackIds:l,timeStamp:Date.now(),namespace:t.namespace};n.sendMessage(M,t.transfer?w(M):[])}catch(l){p(l)}})}});return a(e,[])},$=(e,n)=>{let t;return(a,...i)=>t??=j(e(...i),a,n)},X=(e,n)=>{let t;return a=>t??=q(n.backup?Object.freeze(e()):{},a,n)},P=(e,n)=>{let t={namespace:n?.namespace??"__comctx__",heartbeatCheck:n?.heartbeatCheck??!0,heartbeatInterval:n?.heartbeatInterval??300,heartbeatTimeout:n?.heartbeatTimeout??1e3,transfer:n?.transfer??!1,backup:n?.backup??!1};if(t.heartbeatTimeout<=t.heartbeatInterval)throw Error(`Invalid heartbeat config: timeout (${t.heartbeatTimeout}ms) must exceed interval (${t.heartbeatInterval}ms).`);return[$(e,t),X(e,t)]};var R=Symbol("worker");var Q=class{worker;constructor(e){this.worker=e}sendMessage=(e,n)=>{this.worker.postMessage(e,n)};onMessage=e=>{let n=t=>e(t.data);return this.worker.addEventListener("message",n),()=>this.worker.removeEventListener("message",n)}},S=new WeakMap;function x(e,n){e.failure??=n;for(let t of e.pendingRejects)t(e.failure);e.pendingRejects.clear()}function _(e){let[,n]=P(()=>({}),{namespace:"__wordpress_worker__",heartbeatCheck:!1,transfer:!0}),t=n(new Q(e)),a=s=>{let o=s.message;x(r,new Error(o?`Worker error: ${o}`:"Worker error: the worker crashed or failed to load."))},i=()=>{x(r,new Error("Worker error: a message could not be deserialized."))};e.addEventListener("error",a),e.addEventListener("messageerror",i);let r={worker:e,pendingRejects:new Set,removeListeners:()=>{e.removeEventListener("error",a),e.removeEventListener("messageerror",i)}},m=new Proxy(t,{get(s,o){if(o===R)return e;let c=Reflect.get(s,o);return typeof c!="function"?c:(...u)=>r.failure?Promise.reject(r.failure):new Promise((p,l)=>{r.pendingRejects.add(l),Promise.resolve().then(()=>c(...u)).then(g=>{r.pendingRejects.delete(l),p(g)},g=>{r.pendingRejects.delete(l),l(g)})})}});return S.set(m,r),m}function I(e){let n=S.get(e);n&&(x(n,new Error("Worker was terminated.")),n.removeListeners(),S.delete(e),n.worker.terminate())}var O=`// node_modules/comctx/core/dist/index.js
|
|
var e = { APPLY: \`apply\`, CALLBACK: \`callback\`, PING: \`ping\`, PONG: \`pong\` };
|
|
var t = { PROVIDER: \`provider\`, INJECTOR: \`injector\` };
|
|
var n = (n2) => {
|
|
let r2 = !!n2?.type && Object.values(e).includes(n2.type), i2 = !!n2?.id && typeof n2.id == \`string\`, a2 = !!n2?.path && Array.isArray(n2.path) && n2.path.every((e2) => typeof e2 == \`string\`), o2 = !!n2?.sender && Object.values(t).includes(n2.sender), s2 = n2?.callbackIds === void 0 || Array.isArray(n2.callbackIds) && n2.callbackIds.every((e2) => typeof e2 == \`string\`), c2 = n2?.args === void 0 || Array.isArray(n2.args), l2 = n2?.error === void 0 || typeof n2.error == \`string\`, u2 = !!n2?.meta && n2.meta !== null && typeof n2.meta == \`object\`, d2 = !!n2?.namespace && typeof n2.namespace == \`string\`, f2 = !!n2?.timeStamp && typeof n2.timeStamp == \`number\`;
|
|
return r2 && i2 && a2 && o2 && s2 && c2 && l2 && u2 && d2 && f2;
|
|
};
|
|
var r = () => [...[, , , ,]].map(() => Math.floor(Math.random() * (2 ** 53 - 1)).toString(16)).join(\`-\`);
|
|
var i = r;
|
|
var a = (e2, t2, ...n2) => {
|
|
let r2 = setTimeout(() => {
|
|
clearTimeout(r2), e2(...n2), r2 = setInterval(e2, t2, ...n2);
|
|
}, 0);
|
|
return () => clearInterval(r2);
|
|
};
|
|
var o = a;
|
|
var s = (e2, t2) => t2 !== void 0 && e2 instanceof t2;
|
|
var c = s;
|
|
var l = (e2) => {
|
|
let t2 = /* @__PURE__ */ new WeakSet(), n2 = (e3) => !e3 || typeof e3 != \`object\` || t2.has(e3) ? [] : (t2.add(e3), c(e3, globalThis.ArrayBuffer) || c(e3, globalThis.MessagePort) || c(e3, globalThis.ImageBitmap) || c(e3, globalThis.OffscreenCanvas) || c(e3, globalThis.AudioData) || c(e3, globalThis.VideoFrame) || c(e3, globalThis.RTCDataChannel) || c(e3, globalThis.MediaSourceHandle) || c(e3, globalThis.MIDIAccess) || c(e3, globalThis.MediaStreamTrack) || c(e3, globalThis.ReadableStream) || c(e3, globalThis.WritableStream) || c(e3, globalThis.TransformStream) ? [e3] : Array.isArray(e3) ? e3.flatMap(n2) : Object.values(e3).flatMap(n2));
|
|
return n2(e2);
|
|
};
|
|
var u = l;
|
|
var d = /* @__PURE__ */ Symbol(\`PROXY_MARKER\`);
|
|
var f = async (r2, a2) => {
|
|
let s2, c2, l2 = /* @__PURE__ */ new Set(), d2 = new Promise((c3, d3) => {
|
|
s2 = o(async () => {
|
|
try {
|
|
let o2 = i(), s3 = await r2.onMessage((r3) => {
|
|
if (!n(r3)) return;
|
|
let i2 = r3;
|
|
i2.namespace === a2.namespace && i2.sender === t.PROVIDER && i2.type === e.PONG && i2.id === o2 && c3();
|
|
});
|
|
s3 && l2.add(s3);
|
|
let d4 = { type: e.PING, sender: t.INJECTOR, id: o2, path: [], meta: {}, namespace: a2.namespace, timeStamp: Date.now() };
|
|
r2.sendMessage(d4, a2.transfer ? u(d4) : []);
|
|
} catch (e2) {
|
|
d3(e2);
|
|
}
|
|
}, a2.heartbeatInterval);
|
|
}), f2 = new Promise((e2, t2) => {
|
|
let n2 = setTimeout(() => t2(Error(\`Provider unavailable: heartbeat check timeout \${a2.heartbeatTimeout}ms.\`)), a2.heartbeatTimeout);
|
|
c2 = () => clearTimeout(n2);
|
|
});
|
|
await Promise.race([d2, f2]).finally(() => {
|
|
s2(), c2(), l2.forEach((e2) => e2()), l2.clear();
|
|
});
|
|
};
|
|
var p = (r2, i2, a2) => (i2.onMessage(async (o2) => {
|
|
if (!n(o2)) return;
|
|
let s2 = o2;
|
|
if (s2.namespace === a2.namespace && s2.sender === t.INJECTOR) switch (s2.type) {
|
|
case e.PING: {
|
|
let n2 = { type: e.PONG, sender: t.PROVIDER, id: s2.id, path: s2.path, meta: s2.meta, namespace: a2.namespace, timeStamp: Date.now() };
|
|
i2.sendMessage(n2, a2.transfer ? u(n2) : []);
|
|
break;
|
|
}
|
|
case e.APPLY: {
|
|
try {
|
|
let n3 = s2.args?.map((n4) => s2.callbackIds?.includes(n4) ? (...r3) => {
|
|
let o3 = { type: e.CALLBACK, sender: t.PROVIDER, id: n4, path: s2.path, meta: s2.meta, data: r3, namespace: a2.namespace, timeStamp: Date.now() };
|
|
i2.sendMessage(o3, a2.transfer ? u(o3) : []);
|
|
} : n4);
|
|
s2.data = await (s2.path?.reduce((e2, t2) => e2[t2], r2)).apply(r2, n3 || []);
|
|
} catch (e2) {
|
|
s2.error = e2.message;
|
|
}
|
|
let n2 = { type: e.APPLY, sender: t.PROVIDER, id: s2.id, path: s2.path, data: s2.data, error: s2.error, meta: s2.meta, namespace: a2.namespace, timeStamp: Date.now() };
|
|
i2.sendMessage(n2, a2.transfer ? u(n2) : []);
|
|
break;
|
|
}
|
|
}
|
|
}), r2);
|
|
var m = (r2, a2, o2) => {
|
|
let s2 = (r3, c2) => {
|
|
let l2 = new Proxy(r3, { get(e2, t2, n2) {
|
|
if (e2[d]) return Reflect.get(e2, t2, n2);
|
|
let r4 = () => {
|
|
};
|
|
return r4[d] = true, s2(r4, [...c2, t2]);
|
|
}, apply(r4, s3, l3) {
|
|
return new Promise(async (r5, s4) => {
|
|
try {
|
|
o2.heartbeatCheck && await f(a2, o2);
|
|
let d2 = [], p2 = l3.map((r6) => {
|
|
if (typeof r6 == \`function\`) {
|
|
let s5 = i();
|
|
return d2.push(s5), a2.onMessage((i2) => {
|
|
if (!n(i2)) return;
|
|
let a3 = i2;
|
|
a3.namespace === o2.namespace && a3.sender === t.PROVIDER && a3.type === e.CALLBACK && a3.id === s5 && r6(...a3.data);
|
|
}), s5;
|
|
} else return r6;
|
|
}), m2 = i(), h2 = await a2.onMessage((i2) => {
|
|
if (!n(i2)) return;
|
|
let a3 = i2;
|
|
a3.namespace === o2.namespace && a3.sender === t.PROVIDER && a3.type === e.APPLY && a3.id === m2 && (a3.error ? s4(Error(a3.error)) : r5(a3.data), h2?.());
|
|
}), g2 = { type: e.APPLY, sender: t.INJECTOR, id: m2, path: c2, args: p2, meta: {}, callbackIds: d2, timeStamp: Date.now(), namespace: o2.namespace };
|
|
a2.sendMessage(g2, o2.transfer ? u(g2) : []);
|
|
} catch (e2) {
|
|
s4(e2);
|
|
}
|
|
});
|
|
} });
|
|
return l2;
|
|
};
|
|
return s2(r2, []);
|
|
};
|
|
var h = (e2, t2) => {
|
|
let n2;
|
|
return (r2, ...i2) => n2 ??= p(e2(...i2), r2, t2);
|
|
};
|
|
var g = (e2, t2) => {
|
|
let n2;
|
|
return (r2) => n2 ??= m(t2.backup ? Object.freeze(e2()) : {}, r2, t2);
|
|
};
|
|
var _ = (e2, t2) => {
|
|
let n2 = { namespace: t2?.namespace ?? \`__comctx__\`, heartbeatCheck: t2?.heartbeatCheck ?? true, heartbeatInterval: t2?.heartbeatInterval ?? 300, heartbeatTimeout: t2?.heartbeatTimeout ?? 1e3, transfer: t2?.transfer ?? false, backup: t2?.backup ?? false };
|
|
if (n2.heartbeatTimeout <= n2.heartbeatInterval) throw Error(\`Invalid heartbeat config: timeout (\${n2.heartbeatTimeout}ms) must exceed interval (\${n2.heartbeatInterval}ms).\`);
|
|
return [h(e2, n2), g(e2, n2)];
|
|
};
|
|
|
|
// packages/worker-threads/build-module/worker-thread.mjs
|
|
var WorkerProvideAdapter = class {
|
|
sendMessage = (message, transfer) => {
|
|
self.postMessage(message, { transfer });
|
|
};
|
|
onMessage = (callback) => {
|
|
const handler = (event) => callback(event.data);
|
|
self.addEventListener("message", handler);
|
|
return () => self.removeEventListener("message", handler);
|
|
};
|
|
};
|
|
function normalizeThrowable(error) {
|
|
if (error instanceof Error && error.message) {
|
|
return error;
|
|
}
|
|
let message = "";
|
|
try {
|
|
message = String(error);
|
|
} catch {
|
|
}
|
|
return new Error(message || "Unknown error in worker thread");
|
|
}
|
|
function withNormalizedErrors(target) {
|
|
const wrapped = {};
|
|
for (const key of Object.keys(target)) {
|
|
const value = target[key];
|
|
if (typeof value !== "function") {
|
|
wrapped[key] = value;
|
|
continue;
|
|
}
|
|
wrapped[key] = async (...args) => {
|
|
try {
|
|
return await value.apply(target, args);
|
|
} catch (error) {
|
|
throw normalizeThrowable(error);
|
|
}
|
|
};
|
|
}
|
|
return wrapped;
|
|
}
|
|
function expose(target) {
|
|
const [provide] = _(() => withNormalizedErrors(target), {
|
|
namespace: "__wordpress_worker__",
|
|
heartbeatCheck: false,
|
|
transfer: true
|
|
});
|
|
provide(new WorkerProvideAdapter());
|
|
}
|
|
|
|
// node_modules/mediabunny/dist/modules/src/misc.js
|
|
function assert(x) {
|
|
if (!x) {
|
|
throw new Error("Assertion failed.");
|
|
}
|
|
}
|
|
var normalizeRotation = (rotation) => {
|
|
const mappedRotation = (rotation % 360 + 360) % 360;
|
|
if (mappedRotation === 0 || mappedRotation === 90 || mappedRotation === 180 || mappedRotation === 270) {
|
|
return mappedRotation;
|
|
} else {
|
|
throw new Error(\`Invalid rotation \${rotation}.\`);
|
|
}
|
|
};
|
|
var last = (arr) => {
|
|
return arr && arr[arr.length - 1];
|
|
};
|
|
var isU32 = (value) => {
|
|
return value >= 0 && value < 2 ** 32;
|
|
};
|
|
var readExpGolomb = (bitstream) => {
|
|
let leadingZeroBits = 0;
|
|
while (bitstream.readBits(1) === 0 && leadingZeroBits < 32) {
|
|
leadingZeroBits++;
|
|
}
|
|
if (leadingZeroBits >= 32) {
|
|
throw new Error("Invalid exponential-Golomb code.");
|
|
}
|
|
const result = (1 << leadingZeroBits) - 1 + bitstream.readBits(leadingZeroBits);
|
|
return result;
|
|
};
|
|
var readSignedExpGolomb = (bitstream) => {
|
|
const codeNum = readExpGolomb(bitstream);
|
|
return (codeNum & 1) === 0 ? -(codeNum >> 1) : codeNum + 1 >> 1;
|
|
};
|
|
var writeBits = (bytes2, start, end, value) => {
|
|
for (let i2 = start; i2 < end; i2++) {
|
|
const byteIndex = Math.floor(i2 / 8);
|
|
let byte = bytes2[byteIndex];
|
|
const bitIndex = 7 - (i2 & 7);
|
|
byte &= ~(1 << bitIndex);
|
|
byte |= (value & 1 << end - i2 - 1) >> end - i2 - 1 << bitIndex;
|
|
bytes2[byteIndex] = byte;
|
|
}
|
|
};
|
|
var toUint8Array = (source) => {
|
|
if (source.constructor === Uint8Array) {
|
|
return source;
|
|
} else if (ArrayBuffer.isView(source)) {
|
|
return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
|
|
} else {
|
|
return new Uint8Array(source);
|
|
}
|
|
};
|
|
var toDataView = (source) => {
|
|
if (source.constructor === DataView) {
|
|
return source;
|
|
} else if (ArrayBuffer.isView(source)) {
|
|
return new DataView(source.buffer, source.byteOffset, source.byteLength);
|
|
} else {
|
|
return new DataView(source);
|
|
}
|
|
};
|
|
var textEncoder = /* @__PURE__ */ new TextEncoder();
|
|
var COLOR_PRIMARIES_MAP = {
|
|
bt709: 1,
|
|
// ITU-R BT.709
|
|
bt470bg: 5,
|
|
// ITU-R BT.470BG
|
|
smpte170m: 6,
|
|
// ITU-R BT.601 525 - SMPTE 170M
|
|
bt2020: 9,
|
|
// ITU-R BT.202
|
|
smpte432: 12
|
|
// SMPTE EG 432-1
|
|
};
|
|
var TRANSFER_CHARACTERISTICS_MAP = {
|
|
"bt709": 1,
|
|
// ITU-R BT.709
|
|
"smpte170m": 6,
|
|
// SMPTE 170M
|
|
"linear": 8,
|
|
// Linear transfer characteristics
|
|
"iec61966-2-1": 13,
|
|
// IEC 61966-2-1
|
|
"pq": 16,
|
|
// Rec. ITU-R BT.2100-2 perceptual quantization (PQ) system
|
|
"hlg": 18
|
|
// Rec. ITU-R BT.2100-2 hybrid loggamma (HLG) system
|
|
};
|
|
var MATRIX_COEFFICIENTS_MAP = {
|
|
"rgb": 0,
|
|
// Identity
|
|
"bt709": 1,
|
|
// ITU-R BT.709
|
|
"bt470bg": 5,
|
|
// ITU-R BT.470BG
|
|
"smpte170m": 6,
|
|
// SMPTE 170M
|
|
"bt2020-ncl": 9
|
|
// ITU-R BT.2020-2 (non-constant luminance)
|
|
};
|
|
var colorSpaceIsComplete = (colorSpace) => {
|
|
return !!colorSpace && !!colorSpace.primaries && !!colorSpace.transfer && !!colorSpace.matrix && colorSpace.fullRange !== void 0;
|
|
};
|
|
var isAllowSharedBufferSource = (x) => {
|
|
return x instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && x instanceof SharedArrayBuffer || ArrayBuffer.isView(x);
|
|
};
|
|
var AsyncMutex = class {
|
|
constructor() {
|
|
this.currentPromise = Promise.resolve();
|
|
this.pending = 0;
|
|
}
|
|
async acquire() {
|
|
let resolver;
|
|
const nextPromise = new Promise((resolve) => {
|
|
let resolved = false;
|
|
resolver = () => {
|
|
if (resolved) {
|
|
return;
|
|
}
|
|
resolve();
|
|
this.pending--;
|
|
resolved = true;
|
|
};
|
|
});
|
|
const currentPromiseAlias = this.currentPromise;
|
|
this.currentPromise = nextPromise;
|
|
this.pending++;
|
|
await currentPromiseAlias;
|
|
return resolver;
|
|
}
|
|
};
|
|
var binarySearchLessOrEqual = (arr, key, valueGetter) => {
|
|
let low = 0;
|
|
let high = arr.length - 1;
|
|
let ans = -1;
|
|
while (low <= high) {
|
|
const mid = low + (high - low + 1) / 2 | 0;
|
|
const midVal = valueGetter(arr[mid]);
|
|
if (midVal <= key) {
|
|
ans = mid;
|
|
low = mid + 1;
|
|
} else {
|
|
high = mid - 1;
|
|
}
|
|
}
|
|
return ans;
|
|
};
|
|
var promiseWithResolvers = () => {
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
return { promise, resolve, reject };
|
|
};
|
|
var assertNever = (x) => {
|
|
throw new Error(\`Unexpected value: \${x}\`);
|
|
};
|
|
var getUint24 = (view2, byteOffset, littleEndian) => {
|
|
const byte1 = view2.getUint8(byteOffset);
|
|
const byte2 = view2.getUint8(byteOffset + 1);
|
|
const byte3 = view2.getUint8(byteOffset + 2);
|
|
if (littleEndian) {
|
|
return byte1 | byte2 << 8 | byte3 << 16;
|
|
} else {
|
|
return byte1 << 16 | byte2 << 8 | byte3;
|
|
}
|
|
};
|
|
var setUint24 = (view2, byteOffset, value, littleEndian) => {
|
|
value = value >>> 0;
|
|
value = value & 16777215;
|
|
if (littleEndian) {
|
|
view2.setUint8(byteOffset, value & 255);
|
|
view2.setUint8(byteOffset + 1, value >>> 8 & 255);
|
|
view2.setUint8(byteOffset + 2, value >>> 16 & 255);
|
|
} else {
|
|
view2.setUint8(byteOffset, value >>> 16 & 255);
|
|
view2.setUint8(byteOffset + 1, value >>> 8 & 255);
|
|
view2.setUint8(byteOffset + 2, value & 255);
|
|
}
|
|
};
|
|
var UNDETERMINED_LANGUAGE = "und";
|
|
var roundToMultiple = (value, multiple) => {
|
|
return Math.round(value / multiple) * multiple;
|
|
};
|
|
var roundToDivisor = (value, multiple) => {
|
|
return Math.round(value * multiple) / multiple;
|
|
};
|
|
var floorToDivisor = (value, multiple) => {
|
|
return Math.floor(value * multiple) / multiple;
|
|
};
|
|
var ISO_639_2_REGEX = /^[a-z]{3}$/;
|
|
var isIso639Dash2LanguageCode = (x) => {
|
|
return ISO_639_2_REGEX.test(x);
|
|
};
|
|
var SECOND_TO_MICROSECOND_FACTOR = 1e6 * (1 + Number.EPSILON);
|
|
var computeRationalApproximation = (x, maxDenominator) => {
|
|
const sign = x < 0 ? -1 : 1;
|
|
x = Math.abs(x);
|
|
let prevNumerator = 0, prevDenominator = 1;
|
|
let currNumerator = 1, currDenominator = 0;
|
|
let remainder = x;
|
|
while (true) {
|
|
const integer = Math.floor(remainder);
|
|
const nextNumerator = integer * currNumerator + prevNumerator;
|
|
const nextDenominator = integer * currDenominator + prevDenominator;
|
|
if (nextDenominator > maxDenominator) {
|
|
return {
|
|
num: sign * currNumerator,
|
|
den: currDenominator
|
|
};
|
|
}
|
|
prevNumerator = currNumerator;
|
|
prevDenominator = currDenominator;
|
|
currNumerator = nextNumerator;
|
|
currDenominator = nextDenominator;
|
|
remainder = 1 / (remainder - integer);
|
|
if (!isFinite(remainder)) {
|
|
break;
|
|
}
|
|
}
|
|
return {
|
|
num: sign * currNumerator,
|
|
den: currDenominator
|
|
};
|
|
};
|
|
var CallSerializer = class {
|
|
constructor() {
|
|
this.currentPromise = Promise.resolve();
|
|
}
|
|
call(fn) {
|
|
return this.currentPromise = this.currentPromise.then(fn);
|
|
}
|
|
};
|
|
var isFirefoxCache = null;
|
|
var isFirefox = () => {
|
|
if (isFirefoxCache !== null) {
|
|
return isFirefoxCache;
|
|
}
|
|
return isFirefoxCache = typeof navigator !== "undefined" && navigator.userAgent?.includes("Firefox");
|
|
};
|
|
var isChromiumCache = null;
|
|
var isChromium = () => {
|
|
if (isChromiumCache !== null) {
|
|
return isChromiumCache;
|
|
}
|
|
return isChromiumCache = !!(typeof navigator !== "undefined" && (navigator.vendor?.includes("Google Inc") || /Chrome/.test(navigator.userAgent)));
|
|
};
|
|
var chromiumVersionCache = null;
|
|
var getChromiumVersion = () => {
|
|
if (chromiumVersionCache !== null) {
|
|
return chromiumVersionCache;
|
|
}
|
|
if (typeof navigator === "undefined") {
|
|
return null;
|
|
}
|
|
const match = /\\bChrome\\/(\\d+)/.exec(navigator.userAgent);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
return chromiumVersionCache = Number(match[1]);
|
|
};
|
|
var keyValueIterator = function* (object) {
|
|
for (const key in object) {
|
|
const value = object[key];
|
|
if (value === void 0) {
|
|
continue;
|
|
}
|
|
yield { key, value };
|
|
}
|
|
};
|
|
var imageMimeTypeToExtension = (mimeType) => {
|
|
switch (mimeType.toLowerCase()) {
|
|
case "image/jpeg":
|
|
case "image/jpg":
|
|
return ".jpg";
|
|
case "image/png":
|
|
return ".png";
|
|
case "image/gif":
|
|
return ".gif";
|
|
case "image/webp":
|
|
return ".webp";
|
|
case "image/bmp":
|
|
return ".bmp";
|
|
case "image/svg+xml":
|
|
return ".svg";
|
|
case "image/tiff":
|
|
return ".tiff";
|
|
case "image/avif":
|
|
return ".avif";
|
|
case "image/x-icon":
|
|
case "image/vnd.microsoft.icon":
|
|
return ".ico";
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
var uint8ArraysAreEqual = (a2, b) => {
|
|
if (a2.length !== b.length) {
|
|
return false;
|
|
}
|
|
for (let i2 = 0; i2 < a2.length; i2++) {
|
|
if (a2[i2] !== b[i2]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
var polyfillSymbolDispose = () => {
|
|
Symbol.dispose ??= /* @__PURE__ */ Symbol("Symbol.dispose");
|
|
};
|
|
var arrayArgmin = (array, getValue) => {
|
|
let minIndex = -1;
|
|
let minValue = Infinity;
|
|
for (let i2 = 0; i2 < array.length; i2++) {
|
|
const value = getValue(array[i2]);
|
|
if (value < minValue) {
|
|
minValue = value;
|
|
minIndex = i2;
|
|
}
|
|
}
|
|
return minIndex;
|
|
};
|
|
var simplifyRational = (rational) => {
|
|
assert(Number.isInteger(rational.num));
|
|
assert(Number.isInteger(rational.den));
|
|
assert(rational.den !== 0);
|
|
let a2 = Math.abs(rational.num);
|
|
let b = Math.abs(rational.den);
|
|
while (b !== 0) {
|
|
const t2 = a2 % b;
|
|
a2 = b;
|
|
b = t2;
|
|
}
|
|
const gcd = a2 || 1;
|
|
return {
|
|
num: rational.num / gcd,
|
|
den: rational.den / gcd
|
|
};
|
|
};
|
|
var validateRectangle = (rect, propertyPath) => {
|
|
if (typeof rect !== "object" || !rect) {
|
|
throw new TypeError(\`\${propertyPath} must be an object.\`);
|
|
}
|
|
if (!Number.isInteger(rect.left) || rect.left < 0) {
|
|
throw new TypeError(\`\${propertyPath}.left must be a non-negative integer.\`);
|
|
}
|
|
if (!Number.isInteger(rect.top) || rect.top < 0) {
|
|
throw new TypeError(\`\${propertyPath}.top must be a non-negative integer.\`);
|
|
}
|
|
if (!Number.isInteger(rect.width) || rect.width < 0) {
|
|
throw new TypeError(\`\${propertyPath}.width must be a non-negative integer.\`);
|
|
}
|
|
if (!Number.isInteger(rect.height) || rect.height < 0) {
|
|
throw new TypeError(\`\${propertyPath}.height must be a non-negative integer.\`);
|
|
}
|
|
};
|
|
var toArray = (x) => {
|
|
if (Array.isArray(x)) {
|
|
return x;
|
|
} else {
|
|
return [x];
|
|
}
|
|
};
|
|
var EventEmitter = class {
|
|
constructor() {
|
|
this._listeners = /* @__PURE__ */ new Map();
|
|
}
|
|
/** Registers a listener for the given event. Returns a function that, when called, removes the listener again. */
|
|
on(event, listener, options) {
|
|
if (!this._listeners.has(event)) {
|
|
this._listeners.set(event, /* @__PURE__ */ new Set());
|
|
}
|
|
const entry = { fn: listener, once: options?.once ?? false };
|
|
this._listeners.get(event).add(entry);
|
|
return () => {
|
|
this._listeners.get(event)?.delete(entry);
|
|
};
|
|
}
|
|
/** @internal */
|
|
_emit(...args) {
|
|
const [event, data] = args;
|
|
const listeners = this._listeners.get(event);
|
|
if (!listeners) {
|
|
return;
|
|
}
|
|
for (const entry of listeners) {
|
|
try {
|
|
entry.fn(data);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
if (entry.once) {
|
|
listeners.delete(entry);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
var isRecordStringString = (value) => {
|
|
return value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype && Object.values(value).every((x) => typeof x === "string");
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/logging.js
|
|
var LogLevel;
|
|
(function(LogLevel2) {
|
|
LogLevel2[LogLevel2["Silent"] = 0] = "Silent";
|
|
LogLevel2[LogLevel2["Errors"] = 1] = "Errors";
|
|
LogLevel2[LogLevel2["Warnings"] = 2] = "Warnings";
|
|
LogLevel2[LogLevel2["Info"] = 3] = "Info";
|
|
})(LogLevel || (LogLevel = {}));
|
|
var Logging = class _Logging {
|
|
constructor() {
|
|
}
|
|
/** The current log level. Defaults to {@link LogLevel.Info}. */
|
|
static get level() {
|
|
return _Logging._level;
|
|
}
|
|
static set level(value) {
|
|
if (value !== LogLevel.Silent && value !== LogLevel.Errors && value !== LogLevel.Warnings && value !== LogLevel.Info) {
|
|
throw new TypeError("Invalid log level. Use one of the values of the LogLevel enum.");
|
|
}
|
|
_Logging._level = value;
|
|
}
|
|
/** @internal */
|
|
static get _emitter() {
|
|
return _Logging._emitterInstance ??= new EventEmitter();
|
|
}
|
|
/** Registers a listener for a log event. Returns a function that, when called, removes the listener again. */
|
|
static on(event, listener, options) {
|
|
return _Logging._emitter.on(event, listener, options);
|
|
}
|
|
/** @internal */
|
|
static _error(...args) {
|
|
_Logging._emitter._emit("error", args);
|
|
if (_Logging._level >= LogLevel.Errors) {
|
|
console.error(...args);
|
|
}
|
|
}
|
|
/** @internal */
|
|
static _warn(...args) {
|
|
_Logging._emitter._emit("warn", args);
|
|
if (_Logging._level >= LogLevel.Warnings) {
|
|
console.warn(...args);
|
|
}
|
|
}
|
|
/** @internal */
|
|
static _info(...args) {
|
|
_Logging._emitter._emit("info", args);
|
|
if (_Logging._level >= LogLevel.Info) {
|
|
console.info(...args);
|
|
}
|
|
}
|
|
};
|
|
Logging._level = LogLevel.Info;
|
|
Logging._emitterInstance = null;
|
|
|
|
// node_modules/mediabunny/dist/modules/src/metadata.js
|
|
var RichImageData = class {
|
|
/** Creates a new {@link RichImageData}. */
|
|
constructor(data, mimeType) {
|
|
this.data = data;
|
|
this.mimeType = mimeType;
|
|
if (!(data instanceof Uint8Array)) {
|
|
throw new TypeError("data must be a Uint8Array.");
|
|
}
|
|
if (typeof mimeType !== "string") {
|
|
throw new TypeError("mimeType must be a string.");
|
|
}
|
|
}
|
|
};
|
|
var AttachedFile = class {
|
|
/** Creates a new {@link AttachedFile}. */
|
|
constructor(data, mimeType, name, description) {
|
|
this.data = data;
|
|
this.mimeType = mimeType;
|
|
this.name = name;
|
|
this.description = description;
|
|
if (!(data instanceof Uint8Array)) {
|
|
throw new TypeError("data must be a Uint8Array.");
|
|
}
|
|
if (mimeType !== void 0 && typeof mimeType !== "string") {
|
|
throw new TypeError("mimeType, when provided, must be a string.");
|
|
}
|
|
if (name !== void 0 && typeof name !== "string") {
|
|
throw new TypeError("name, when provided, must be a string.");
|
|
}
|
|
if (description !== void 0 && typeof description !== "string") {
|
|
throw new TypeError("description, when provided, must be a string.");
|
|
}
|
|
}
|
|
};
|
|
var validateMetadataTags = (tags) => {
|
|
if (!tags || typeof tags !== "object") {
|
|
throw new TypeError("tags must be an object.");
|
|
}
|
|
if (tags.title !== void 0 && typeof tags.title !== "string") {
|
|
throw new TypeError("tags.title, when provided, must be a string.");
|
|
}
|
|
if (tags.description !== void 0 && typeof tags.description !== "string") {
|
|
throw new TypeError("tags.description, when provided, must be a string.");
|
|
}
|
|
if (tags.artist !== void 0 && typeof tags.artist !== "string") {
|
|
throw new TypeError("tags.artist, when provided, must be a string.");
|
|
}
|
|
if (tags.album !== void 0 && typeof tags.album !== "string") {
|
|
throw new TypeError("tags.album, when provided, must be a string.");
|
|
}
|
|
if (tags.albumArtist !== void 0 && typeof tags.albumArtist !== "string") {
|
|
throw new TypeError("tags.albumArtist, when provided, must be a string.");
|
|
}
|
|
if (tags.trackNumber !== void 0 && (!Number.isInteger(tags.trackNumber) || tags.trackNumber <= 0)) {
|
|
throw new TypeError("tags.trackNumber, when provided, must be a positive integer.");
|
|
}
|
|
if (tags.tracksTotal !== void 0 && (!Number.isInteger(tags.tracksTotal) || tags.tracksTotal <= 0)) {
|
|
throw new TypeError("tags.tracksTotal, when provided, must be a positive integer.");
|
|
}
|
|
if (tags.discNumber !== void 0 && (!Number.isInteger(tags.discNumber) || tags.discNumber <= 0)) {
|
|
throw new TypeError("tags.discNumber, when provided, must be a positive integer.");
|
|
}
|
|
if (tags.discsTotal !== void 0 && (!Number.isInteger(tags.discsTotal) || tags.discsTotal <= 0)) {
|
|
throw new TypeError("tags.discsTotal, when provided, must be a positive integer.");
|
|
}
|
|
if (tags.genre !== void 0 && typeof tags.genre !== "string") {
|
|
throw new TypeError("tags.genre, when provided, must be a string.");
|
|
}
|
|
if (tags.date !== void 0 && (!(tags.date instanceof Date) || Number.isNaN(tags.date.getTime()))) {
|
|
throw new TypeError("tags.date, when provided, must be a valid Date.");
|
|
}
|
|
if (tags.lyrics !== void 0 && typeof tags.lyrics !== "string") {
|
|
throw new TypeError("tags.lyrics, when provided, must be a string.");
|
|
}
|
|
if (tags.images !== void 0) {
|
|
if (!Array.isArray(tags.images)) {
|
|
throw new TypeError("tags.images, when provided, must be an array.");
|
|
}
|
|
for (const image of tags.images) {
|
|
if (!image || typeof image !== "object") {
|
|
throw new TypeError("Each image in tags.images must be an object.");
|
|
}
|
|
if (!(image.data instanceof Uint8Array)) {
|
|
throw new TypeError("Each image.data must be a Uint8Array.");
|
|
}
|
|
if (typeof image.mimeType !== "string") {
|
|
throw new TypeError("Each image.mimeType must be a string.");
|
|
}
|
|
if (!["coverFront", "coverBack", "unknown"].includes(image.kind)) {
|
|
throw new TypeError("Each image.kind must be 'coverFront', 'coverBack', or 'unknown'.");
|
|
}
|
|
}
|
|
}
|
|
if (tags.comment !== void 0 && typeof tags.comment !== "string") {
|
|
throw new TypeError("tags.comment, when provided, must be a string.");
|
|
}
|
|
if (tags.raw !== void 0) {
|
|
if (!tags.raw || typeof tags.raw !== "object") {
|
|
throw new TypeError("tags.raw, when provided, must be an object.");
|
|
}
|
|
for (const value of Object.values(tags.raw)) {
|
|
if (value !== null && typeof value !== "string" && !(value instanceof Uint8Array) && !(value instanceof RichImageData) && !(value instanceof AttachedFile) && !isRecordStringString(value)) {
|
|
throw new TypeError("Each value in tags.raw must be a string, Uint8Array, RichImageData, AttachedFile, Record<string, string>, or null.");
|
|
}
|
|
}
|
|
}
|
|
};
|
|
var validateTrackDisposition = (disposition) => {
|
|
if (!disposition || typeof disposition !== "object") {
|
|
throw new TypeError("disposition must be an object.");
|
|
}
|
|
if (disposition.default !== void 0 && typeof disposition.default !== "boolean") {
|
|
throw new TypeError("disposition.default must be a boolean.");
|
|
}
|
|
if (disposition.primary !== void 0 && typeof disposition.primary !== "boolean") {
|
|
throw new TypeError("disposition.primary must be a boolean.");
|
|
}
|
|
if (disposition.forced !== void 0 && typeof disposition.forced !== "boolean") {
|
|
throw new TypeError("disposition.forced must be a boolean.");
|
|
}
|
|
if (disposition.original !== void 0 && typeof disposition.original !== "boolean") {
|
|
throw new TypeError("disposition.original must be a boolean.");
|
|
}
|
|
if (disposition.commentary !== void 0 && typeof disposition.commentary !== "boolean") {
|
|
throw new TypeError("disposition.commentary must be a boolean.");
|
|
}
|
|
if (disposition.hearingImpaired !== void 0 && typeof disposition.hearingImpaired !== "boolean") {
|
|
throw new TypeError("disposition.hearingImpaired must be a boolean.");
|
|
}
|
|
if (disposition.visuallyImpaired !== void 0 && typeof disposition.visuallyImpaired !== "boolean") {
|
|
throw new TypeError("disposition.visuallyImpaired must be a boolean.");
|
|
}
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/shared/bitstream.js
|
|
var Bitstream = class _Bitstream {
|
|
constructor(bytes2) {
|
|
this.bytes = bytes2;
|
|
this.pos = 0;
|
|
}
|
|
seekToByte(byteOffset) {
|
|
this.pos = 8 * byteOffset;
|
|
}
|
|
readBit() {
|
|
const byteIndex = Math.floor(this.pos / 8);
|
|
const byte = this.bytes[byteIndex] ?? 0;
|
|
const bitIndex = 7 - (this.pos & 7);
|
|
const bit = (byte & 1 << bitIndex) >> bitIndex;
|
|
this.pos++;
|
|
return bit;
|
|
}
|
|
readBits(n2) {
|
|
if (n2 === 1) {
|
|
return this.readBit();
|
|
}
|
|
let result = 0;
|
|
for (let i2 = 0; i2 < n2; i2++) {
|
|
result <<= 1;
|
|
result |= this.readBit();
|
|
}
|
|
return result;
|
|
}
|
|
writeBits(n2, value) {
|
|
const end = this.pos + n2;
|
|
for (let i2 = this.pos; i2 < end; i2++) {
|
|
const byteIndex = Math.floor(i2 / 8);
|
|
let byte = this.bytes[byteIndex];
|
|
const bitIndex = 7 - (i2 & 7);
|
|
byte &= ~(1 << bitIndex);
|
|
byte |= (value & 1 << end - i2 - 1) >> end - i2 - 1 << bitIndex;
|
|
this.bytes[byteIndex] = byte;
|
|
}
|
|
this.pos = end;
|
|
}
|
|
readAlignedByte() {
|
|
if (this.pos % 8 !== 0) {
|
|
throw new Error("Bitstream is not byte-aligned.");
|
|
}
|
|
const byteIndex = this.pos / 8;
|
|
const byte = this.bytes[byteIndex] ?? 0;
|
|
this.pos += 8;
|
|
return byte;
|
|
}
|
|
skipBits(n2) {
|
|
this.pos += n2;
|
|
}
|
|
getBitsLeft() {
|
|
return this.bytes.length * 8 - this.pos;
|
|
}
|
|
clone() {
|
|
const clone = new _Bitstream(this.bytes);
|
|
clone.pos = this.pos;
|
|
return clone;
|
|
}
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/shared/aac-misc.js
|
|
var aacFrequencyTable = [
|
|
96e3,
|
|
88200,
|
|
64e3,
|
|
48e3,
|
|
44100,
|
|
32e3,
|
|
24e3,
|
|
22050,
|
|
16e3,
|
|
12e3,
|
|
11025,
|
|
8e3,
|
|
7350
|
|
];
|
|
var aacChannelMap = [-1, 1, 2, 3, 4, 5, 6, 8];
|
|
var buildAacAudioSpecificConfig = (config) => {
|
|
let frequencyIndex = aacFrequencyTable.indexOf(config.sampleRate);
|
|
let customSampleRate = null;
|
|
if (frequencyIndex === -1) {
|
|
frequencyIndex = 15;
|
|
customSampleRate = config.sampleRate;
|
|
}
|
|
const channelConfiguration = aacChannelMap.indexOf(config.numberOfChannels);
|
|
if (channelConfiguration === -1) {
|
|
throw new TypeError(\`Unsupported number of channels: \${config.numberOfChannels}\`);
|
|
}
|
|
let bitCount = 5 + 4 + 4;
|
|
if (config.objectType >= 32) {
|
|
bitCount += 6;
|
|
}
|
|
if (frequencyIndex === 15) {
|
|
bitCount += 24;
|
|
}
|
|
const byteCount = Math.ceil(bitCount / 8);
|
|
const bytes2 = new Uint8Array(byteCount);
|
|
const bitstream = new Bitstream(bytes2);
|
|
if (config.objectType < 32) {
|
|
bitstream.writeBits(5, config.objectType);
|
|
} else {
|
|
bitstream.writeBits(5, 31);
|
|
bitstream.writeBits(6, config.objectType - 32);
|
|
}
|
|
bitstream.writeBits(4, frequencyIndex);
|
|
if (frequencyIndex === 15) {
|
|
bitstream.writeBits(24, customSampleRate);
|
|
}
|
|
bitstream.writeBits(4, channelConfiguration);
|
|
return bytes2;
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/codec.js
|
|
var VIDEO_CODECS = [
|
|
"avc",
|
|
"hevc",
|
|
"vp9",
|
|
"av1",
|
|
"vp8"
|
|
];
|
|
var PCM_AUDIO_CODECS = [
|
|
"pcm-s16",
|
|
// We don't prefix 'le' so we're compatible with the WebCodecs-registered PCM codec strings
|
|
"pcm-s16be",
|
|
"pcm-s24",
|
|
"pcm-s24be",
|
|
"pcm-s32",
|
|
"pcm-s32be",
|
|
"pcm-f32",
|
|
"pcm-f32be",
|
|
"pcm-f64",
|
|
"pcm-f64be",
|
|
"pcm-u8",
|
|
"pcm-s8",
|
|
"ulaw",
|
|
"alaw"
|
|
];
|
|
var NON_PCM_AUDIO_CODECS = [
|
|
"aac",
|
|
"opus",
|
|
"mp3",
|
|
"vorbis",
|
|
"flac",
|
|
"ac3",
|
|
"eac3"
|
|
];
|
|
var AUDIO_CODECS = [
|
|
...NON_PCM_AUDIO_CODECS,
|
|
...PCM_AUDIO_CODECS
|
|
];
|
|
var SUBTITLE_CODECS = [
|
|
"webvtt"
|
|
];
|
|
var AVC_LEVEL_TABLE = [
|
|
{ maxMacroblocks: 99, maxBitrate: 64e3, maxDpbMbs: 396, level: 10 },
|
|
// Level 1
|
|
{ maxMacroblocks: 396, maxBitrate: 192e3, maxDpbMbs: 900, level: 11 },
|
|
// Level 1.1
|
|
{ maxMacroblocks: 396, maxBitrate: 384e3, maxDpbMbs: 2376, level: 12 },
|
|
// Level 1.2
|
|
{ maxMacroblocks: 396, maxBitrate: 768e3, maxDpbMbs: 2376, level: 13 },
|
|
// Level 1.3
|
|
{ maxMacroblocks: 396, maxBitrate: 2e6, maxDpbMbs: 2376, level: 20 },
|
|
// Level 2
|
|
{ maxMacroblocks: 792, maxBitrate: 4e6, maxDpbMbs: 4752, level: 21 },
|
|
// Level 2.1
|
|
{ maxMacroblocks: 1620, maxBitrate: 4e6, maxDpbMbs: 8100, level: 22 },
|
|
// Level 2.2
|
|
{ maxMacroblocks: 1620, maxBitrate: 1e7, maxDpbMbs: 8100, level: 30 },
|
|
// Level 3
|
|
{ maxMacroblocks: 3600, maxBitrate: 14e6, maxDpbMbs: 18e3, level: 31 },
|
|
// Level 3.1
|
|
{ maxMacroblocks: 5120, maxBitrate: 2e7, maxDpbMbs: 20480, level: 32 },
|
|
// Level 3.2
|
|
{ maxMacroblocks: 8192, maxBitrate: 2e7, maxDpbMbs: 32768, level: 40 },
|
|
// Level 4
|
|
{ maxMacroblocks: 8192, maxBitrate: 5e7, maxDpbMbs: 32768, level: 41 },
|
|
// Level 4.1
|
|
{ maxMacroblocks: 8704, maxBitrate: 5e7, maxDpbMbs: 34816, level: 42 },
|
|
// Level 4.2
|
|
{ maxMacroblocks: 22080, maxBitrate: 135e6, maxDpbMbs: 110400, level: 50 },
|
|
// Level 5
|
|
{ maxMacroblocks: 36864, maxBitrate: 24e7, maxDpbMbs: 184320, level: 51 },
|
|
// Level 5.1
|
|
{ maxMacroblocks: 36864, maxBitrate: 24e7, maxDpbMbs: 184320, level: 52 },
|
|
// Level 5.2
|
|
{ maxMacroblocks: 139264, maxBitrate: 24e7, maxDpbMbs: 696320, level: 60 },
|
|
// Level 6
|
|
{ maxMacroblocks: 139264, maxBitrate: 48e7, maxDpbMbs: 696320, level: 61 },
|
|
// Level 6.1
|
|
{ maxMacroblocks: 139264, maxBitrate: 8e8, maxDpbMbs: 696320, level: 62 }
|
|
// Level 6.2
|
|
];
|
|
var HEVC_LEVEL_TABLE = [
|
|
{ maxPictureSize: 36864, maxBitrate: 128e3, tier: "L", level: 30 },
|
|
// Level 1 (Low Tier)
|
|
{ maxPictureSize: 122880, maxBitrate: 15e5, tier: "L", level: 60 },
|
|
// Level 2 (Low Tier)
|
|
{ maxPictureSize: 245760, maxBitrate: 3e6, tier: "L", level: 63 },
|
|
// Level 2.1 (Low Tier)
|
|
{ maxPictureSize: 552960, maxBitrate: 6e6, tier: "L", level: 90 },
|
|
// Level 3 (Low Tier)
|
|
{ maxPictureSize: 983040, maxBitrate: 1e7, tier: "L", level: 93 },
|
|
// Level 3.1 (Low Tier)
|
|
{ maxPictureSize: 2228224, maxBitrate: 12e6, tier: "L", level: 120 },
|
|
// Level 4 (Low Tier)
|
|
{ maxPictureSize: 2228224, maxBitrate: 3e7, tier: "H", level: 120 },
|
|
// Level 4 (High Tier)
|
|
{ maxPictureSize: 2228224, maxBitrate: 2e7, tier: "L", level: 123 },
|
|
// Level 4.1 (Low Tier)
|
|
{ maxPictureSize: 2228224, maxBitrate: 5e7, tier: "H", level: 123 },
|
|
// Level 4.1 (High Tier)
|
|
{ maxPictureSize: 8912896, maxBitrate: 25e6, tier: "L", level: 150 },
|
|
// Level 5 (Low Tier)
|
|
{ maxPictureSize: 8912896, maxBitrate: 1e8, tier: "H", level: 150 },
|
|
// Level 5 (High Tier)
|
|
{ maxPictureSize: 8912896, maxBitrate: 4e7, tier: "L", level: 153 },
|
|
// Level 5.1 (Low Tier)
|
|
{ maxPictureSize: 8912896, maxBitrate: 16e7, tier: "H", level: 153 },
|
|
// Level 5.1 (High Tier)
|
|
{ maxPictureSize: 8912896, maxBitrate: 6e7, tier: "L", level: 156 },
|
|
// Level 5.2 (Low Tier)
|
|
{ maxPictureSize: 8912896, maxBitrate: 24e7, tier: "H", level: 156 },
|
|
// Level 5.2 (High Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 6e7, tier: "L", level: 180 },
|
|
// Level 6 (Low Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 24e7, tier: "H", level: 180 },
|
|
// Level 6 (High Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 12e7, tier: "L", level: 183 },
|
|
// Level 6.1 (Low Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 48e7, tier: "H", level: 183 },
|
|
// Level 6.1 (High Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 24e7, tier: "L", level: 186 },
|
|
// Level 6.2 (Low Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 8e8, tier: "H", level: 186 }
|
|
// Level 6.2 (High Tier)
|
|
];
|
|
var VP9_LEVEL_TABLE = [
|
|
{ maxPictureSize: 36864, maxBitrate: 2e5, level: 10 },
|
|
// Level 1
|
|
{ maxPictureSize: 73728, maxBitrate: 8e5, level: 11 },
|
|
// Level 1.1
|
|
{ maxPictureSize: 122880, maxBitrate: 18e5, level: 20 },
|
|
// Level 2
|
|
{ maxPictureSize: 245760, maxBitrate: 36e5, level: 21 },
|
|
// Level 2.1
|
|
{ maxPictureSize: 552960, maxBitrate: 72e5, level: 30 },
|
|
// Level 3
|
|
{ maxPictureSize: 983040, maxBitrate: 12e6, level: 31 },
|
|
// Level 3.1
|
|
{ maxPictureSize: 2228224, maxBitrate: 18e6, level: 40 },
|
|
// Level 4
|
|
{ maxPictureSize: 2228224, maxBitrate: 3e7, level: 41 },
|
|
// Level 4.1
|
|
{ maxPictureSize: 8912896, maxBitrate: 6e7, level: 50 },
|
|
// Level 5
|
|
{ maxPictureSize: 8912896, maxBitrate: 12e7, level: 51 },
|
|
// Level 5.1
|
|
{ maxPictureSize: 8912896, maxBitrate: 18e7, level: 52 },
|
|
// Level 5.2
|
|
{ maxPictureSize: 35651584, maxBitrate: 18e7, level: 60 },
|
|
// Level 6
|
|
{ maxPictureSize: 35651584, maxBitrate: 24e7, level: 61 },
|
|
// Level 6.1
|
|
{ maxPictureSize: 35651584, maxBitrate: 48e7, level: 62 }
|
|
// Level 6.2
|
|
];
|
|
var AV1_LEVEL_TABLE = [
|
|
{ maxPictureSize: 147456, maxBitrate: 15e5, tier: "M", level: 0 },
|
|
// Level 2.0 (Main Tier)
|
|
{ maxPictureSize: 278784, maxBitrate: 3e6, tier: "M", level: 1 },
|
|
// Level 2.1 (Main Tier)
|
|
{ maxPictureSize: 665856, maxBitrate: 6e6, tier: "M", level: 4 },
|
|
// Level 3.0 (Main Tier)
|
|
{ maxPictureSize: 1065024, maxBitrate: 1e7, tier: "M", level: 5 },
|
|
// Level 3.1 (Main Tier)
|
|
{ maxPictureSize: 2359296, maxBitrate: 12e6, tier: "M", level: 8 },
|
|
// Level 4.0 (Main Tier)
|
|
{ maxPictureSize: 2359296, maxBitrate: 3e7, tier: "H", level: 8 },
|
|
// Level 4.0 (High Tier)
|
|
{ maxPictureSize: 2359296, maxBitrate: 2e7, tier: "M", level: 9 },
|
|
// Level 4.1 (Main Tier)
|
|
{ maxPictureSize: 2359296, maxBitrate: 5e7, tier: "H", level: 9 },
|
|
// Level 4.1 (High Tier)
|
|
{ maxPictureSize: 8912896, maxBitrate: 3e7, tier: "M", level: 12 },
|
|
// Level 5.0 (Main Tier)
|
|
{ maxPictureSize: 8912896, maxBitrate: 1e8, tier: "H", level: 12 },
|
|
// Level 5.0 (High Tier)
|
|
{ maxPictureSize: 8912896, maxBitrate: 4e7, tier: "M", level: 13 },
|
|
// Level 5.1 (Main Tier)
|
|
{ maxPictureSize: 8912896, maxBitrate: 16e7, tier: "H", level: 13 },
|
|
// Level 5.1 (High Tier)
|
|
{ maxPictureSize: 8912896, maxBitrate: 6e7, tier: "M", level: 14 },
|
|
// Level 5.2 (Main Tier)
|
|
{ maxPictureSize: 8912896, maxBitrate: 24e7, tier: "H", level: 14 },
|
|
// Level 5.2 (High Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 6e7, tier: "M", level: 15 },
|
|
// Level 5.3 (Main Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 24e7, tier: "H", level: 15 },
|
|
// Level 5.3 (High Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 6e7, tier: "M", level: 16 },
|
|
// Level 6.0 (Main Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 24e7, tier: "H", level: 16 },
|
|
// Level 6.0 (High Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 1e8, tier: "M", level: 17 },
|
|
// Level 6.1 (Main Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 48e7, tier: "H", level: 17 },
|
|
// Level 6.1 (High Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 16e7, tier: "M", level: 18 },
|
|
// Level 6.2 (Main Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 8e8, tier: "H", level: 18 },
|
|
// Level 6.2 (High Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 16e7, tier: "M", level: 19 },
|
|
// Level 6.3 (Main Tier)
|
|
{ maxPictureSize: 35651584, maxBitrate: 8e8, tier: "H", level: 19 }
|
|
// Level 6.3 (High Tier)
|
|
];
|
|
var buildVideoCodecString = (codec, width, height, bitrate) => {
|
|
if (codec === "avc") {
|
|
const profileIndication = 100;
|
|
const totalMacroblocks = Math.ceil(width / 16) * Math.ceil(height / 16);
|
|
const levelInfo = AVC_LEVEL_TABLE.find((level) => totalMacroblocks <= level.maxMacroblocks && bitrate <= level.maxBitrate) ?? last(AVC_LEVEL_TABLE);
|
|
const levelIndication = levelInfo ? levelInfo.level : 0;
|
|
const hexProfileIndication = profileIndication.toString(16).padStart(2, "0");
|
|
const hexProfileCompatibility = "00";
|
|
const hexLevelIndication = levelIndication.toString(16).padStart(2, "0");
|
|
return \`avc1.\${hexProfileIndication}\${hexProfileCompatibility}\${hexLevelIndication}\`;
|
|
} else if (codec === "hevc") {
|
|
const profilePrefix = "";
|
|
const profileIdc = 1;
|
|
const compatibilityFlags = "6";
|
|
const pictureSize = width * height;
|
|
const levelInfo = HEVC_LEVEL_TABLE.find((level) => pictureSize <= level.maxPictureSize && bitrate <= level.maxBitrate) ?? last(HEVC_LEVEL_TABLE);
|
|
const constraintFlags = "B0";
|
|
return \`hev1.\${profilePrefix}\${profileIdc}.\${compatibilityFlags}.\${levelInfo.tier}\${levelInfo.level}.\${constraintFlags}\`;
|
|
} else if (codec === "vp8") {
|
|
return "vp8";
|
|
} else if (codec === "vp9") {
|
|
const profile = "00";
|
|
const pictureSize = width * height;
|
|
const levelInfo = VP9_LEVEL_TABLE.find((level) => pictureSize <= level.maxPictureSize && bitrate <= level.maxBitrate) ?? last(VP9_LEVEL_TABLE);
|
|
const bitDepth = "08";
|
|
return \`vp09.\${profile}.\${levelInfo.level.toString().padStart(2, "0")}.\${bitDepth}\`;
|
|
} else if (codec === "av1") {
|
|
const profile = 0;
|
|
const pictureSize = width * height;
|
|
const levelInfo = AV1_LEVEL_TABLE.find((level2) => pictureSize <= level2.maxPictureSize && bitrate <= level2.maxBitrate) ?? last(AV1_LEVEL_TABLE);
|
|
const level = levelInfo.level.toString().padStart(2, "0");
|
|
const bitDepth = "08";
|
|
return \`av01.\${profile}.\${level}\${levelInfo.tier}.\${bitDepth}\`;
|
|
}
|
|
throw new TypeError(\`Unhandled codec '\${codec}'.\`);
|
|
};
|
|
var generateVp9CodecConfigurationFromCodecString = (codecString) => {
|
|
const parts = codecString.split(".");
|
|
const profile = Number(parts[1]);
|
|
const level = Number(parts[2]);
|
|
const bitDepth = Number(parts[3]);
|
|
const chromaSubsampling = parts[4] ? Number(parts[4]) : 1;
|
|
return [
|
|
1,
|
|
1,
|
|
profile,
|
|
2,
|
|
1,
|
|
level,
|
|
3,
|
|
1,
|
|
bitDepth,
|
|
4,
|
|
1,
|
|
chromaSubsampling
|
|
];
|
|
};
|
|
var generateAv1CodecConfigurationFromCodecString = (codecString) => {
|
|
const parts = codecString.split(".");
|
|
const marker = 1;
|
|
const version = 1;
|
|
const firstByte = (marker << 7) + version;
|
|
const profile = Number(parts[1]);
|
|
const levelAndTier = parts[2];
|
|
const level = Number(levelAndTier.slice(0, -1));
|
|
const secondByte = (profile << 5) + level;
|
|
const tier = levelAndTier.slice(-1) === "H" ? 1 : 0;
|
|
const bitDepth = Number(parts[3]);
|
|
const highBitDepth = bitDepth === 8 ? 0 : 1;
|
|
const twelveBit = 0;
|
|
const monochrome = parts[4] ? Number(parts[4]) : 0;
|
|
const chromaSubsamplingX = parts[5] ? Number(parts[5][0]) : 1;
|
|
const chromaSubsamplingY = parts[5] ? Number(parts[5][1]) : 1;
|
|
const chromaSamplePosition = parts[5] ? Number(parts[5][2]) : 0;
|
|
const thirdByte = (tier << 7) + (highBitDepth << 6) + (twelveBit << 5) + (monochrome << 4) + (chromaSubsamplingX << 3) + (chromaSubsamplingY << 2) + chromaSamplePosition;
|
|
const initialPresentationDelayPresent = 0;
|
|
const fourthByte = initialPresentationDelayPresent;
|
|
return [firstByte, secondByte, thirdByte, fourthByte];
|
|
};
|
|
var OPUS_SAMPLE_RATE = 48e3;
|
|
var PCM_CODEC_REGEX = /^pcm-([usf])(\\d+)(be)?$/;
|
|
var parsePcmCodec = (codec) => {
|
|
assert(PCM_AUDIO_CODECS.includes(codec));
|
|
if (codec === "ulaw") {
|
|
return { dataType: "ulaw", sampleSize: 1, littleEndian: true, silentValue: 255 };
|
|
} else if (codec === "alaw") {
|
|
return { dataType: "alaw", sampleSize: 1, littleEndian: true, silentValue: 213 };
|
|
}
|
|
const match = PCM_CODEC_REGEX.exec(codec);
|
|
assert(match);
|
|
let dataType;
|
|
if (match[1] === "u") {
|
|
dataType = "unsigned";
|
|
} else if (match[1] === "s") {
|
|
dataType = "signed";
|
|
} else {
|
|
dataType = "float";
|
|
}
|
|
const sampleSize = Number(match[2]) / 8;
|
|
const littleEndian = match[3] !== "be";
|
|
const silentValue = codec === "pcm-u8" ? 2 ** 7 : 0;
|
|
return { dataType, sampleSize, littleEndian, silentValue };
|
|
};
|
|
var inferCodecFromCodecString = (codecString) => {
|
|
if (codecString.startsWith("avc1") || codecString.startsWith("avc3")) {
|
|
return "avc";
|
|
} else if (codecString.startsWith("hev1") || codecString.startsWith("hvc1")) {
|
|
return "hevc";
|
|
} else if (codecString === "vp8") {
|
|
return "vp8";
|
|
} else if (codecString.startsWith("vp09")) {
|
|
return "vp9";
|
|
} else if (codecString.startsWith("av01")) {
|
|
return "av1";
|
|
}
|
|
if (codecString === "mp3" || codecString === "mp4a.69" || codecString === "mp4a.6B" || codecString === "mp4a.6b" || codecString === "mp4a.40.34") {
|
|
return "mp3";
|
|
} else if (codecString.startsWith("mp4a.40.") || codecString === "mp4a.67") {
|
|
return "aac";
|
|
} else if (codecString === "opus") {
|
|
return "opus";
|
|
} else if (codecString === "vorbis") {
|
|
return "vorbis";
|
|
} else if (codecString === "flac") {
|
|
return "flac";
|
|
} else if (codecString === "ac-3" || codecString === "ac3") {
|
|
return "ac3";
|
|
} else if (codecString === "ec-3" || codecString === "eac3") {
|
|
return "eac3";
|
|
} else if (codecString === "ulaw") {
|
|
return "ulaw";
|
|
} else if (codecString === "alaw") {
|
|
return "alaw";
|
|
} else if (PCM_CODEC_REGEX.test(codecString)) {
|
|
return codecString;
|
|
}
|
|
if (codecString === "webvtt") {
|
|
return "webvtt";
|
|
}
|
|
return null;
|
|
};
|
|
var getVideoEncoderConfigExtension = (codec) => {
|
|
if (codec === "avc") {
|
|
return {
|
|
avc: {
|
|
format: "avc"
|
|
// Ensure the format is not Annex B
|
|
}
|
|
};
|
|
} else if (codec === "hevc") {
|
|
return {
|
|
hevc: {
|
|
format: "hevc"
|
|
// Ensure the format is not Annex B
|
|
}
|
|
};
|
|
}
|
|
return {};
|
|
};
|
|
var VALID_VIDEO_CODEC_STRING_PREFIXES = ["avc1", "avc3", "hev1", "hvc1", "vp8", "vp09", "av01"];
|
|
var AVC_CODEC_STRING_REGEX = /^(avc1|avc3)\\.[0-9a-fA-F]{6}$/;
|
|
var HEVC_CODEC_STRING_REGEX = /^(hev1|hvc1)\\.(?:[ABC]?\\d+)\\.[0-9a-fA-F]{1,8}\\.[LH]\\d+(?:\\.[0-9a-fA-F]{1,2}){0,6}$/;
|
|
var VP9_CODEC_STRING_REGEX = /^vp09(?:\\.\\d{2}){3}(?:(?:\\.\\d{2}){5})?$/;
|
|
var AV1_CODEC_STRING_REGEX = /^av01\\.\\d\\.\\d{2}[MH]\\.\\d{2}(?:\\.\\d\\.\\d{3}\\.\\d{2}\\.\\d{2}\\.\\d{2}\\.\\d)?$/;
|
|
var validateVideoChunkMetadata = (metadata) => {
|
|
if (!metadata) {
|
|
throw new TypeError("Video chunk metadata must be provided.");
|
|
}
|
|
if (typeof metadata !== "object") {
|
|
throw new TypeError("Video chunk metadata must be an object.");
|
|
}
|
|
if (!metadata.decoderConfig) {
|
|
throw new TypeError("Video chunk metadata must include a decoder configuration.");
|
|
}
|
|
if (typeof metadata.decoderConfig !== "object") {
|
|
throw new TypeError("Video chunk metadata decoder configuration must be an object.");
|
|
}
|
|
if (typeof metadata.decoderConfig.codec !== "string") {
|
|
throw new TypeError("Video chunk metadata decoder configuration must specify a codec string.");
|
|
}
|
|
if (!VALID_VIDEO_CODEC_STRING_PREFIXES.some((prefix) => metadata.decoderConfig.codec.startsWith(prefix))) {
|
|
throw new TypeError("Video chunk metadata decoder configuration codec string must be a valid video codec string as specified in the Mediabunny Codec Registry.");
|
|
}
|
|
if (!Number.isInteger(metadata.decoderConfig.codedWidth) || metadata.decoderConfig.codedWidth <= 0) {
|
|
throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).");
|
|
}
|
|
if (!Number.isInteger(metadata.decoderConfig.codedHeight) || metadata.decoderConfig.codedHeight <= 0) {
|
|
throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedHeight (positive integer).");
|
|
}
|
|
if (metadata.decoderConfig.displayAspectWidth !== void 0 && (!Number.isInteger(metadata.decoderConfig.displayAspectWidth) || metadata.decoderConfig.displayAspectWidth <= 0)) {
|
|
throw new TypeError("Video chunk metadata decoder configuration displayAspectWidth, when defined, must be a positive integer.");
|
|
}
|
|
if (metadata.decoderConfig.displayAspectHeight !== void 0 && (!Number.isInteger(metadata.decoderConfig.displayAspectHeight) || metadata.decoderConfig.displayAspectHeight <= 0)) {
|
|
throw new TypeError("Video chunk metadata decoder configuration displayAspectHeight, when defined, must be a positive integer.");
|
|
}
|
|
if (metadata.decoderConfig.displayAspectWidth !== void 0 !== (metadata.decoderConfig.displayAspectHeight !== void 0)) {
|
|
throw new TypeError("Video chunk metadata decoder configuration must specify both displayAspectWidth and displayAspectHeight, or neither.");
|
|
}
|
|
if (metadata.decoderConfig.description !== void 0) {
|
|
if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
|
|
throw new TypeError("Video chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");
|
|
}
|
|
}
|
|
if (metadata.decoderConfig.colorSpace !== void 0) {
|
|
const { colorSpace } = metadata.decoderConfig;
|
|
if (typeof colorSpace !== "object") {
|
|
throw new TypeError("Video chunk metadata decoder configuration colorSpace, when provided, must be an object.");
|
|
}
|
|
const primariesValues = Object.keys(COLOR_PRIMARIES_MAP);
|
|
if (colorSpace.primaries != null && !primariesValues.includes(colorSpace.primaries)) {
|
|
throw new TypeError(\`Video chunk metadata decoder configuration colorSpace primaries, when defined, must be one of \${primariesValues.join(", ")}.\`);
|
|
}
|
|
const transferValues = Object.keys(TRANSFER_CHARACTERISTICS_MAP);
|
|
if (colorSpace.transfer != null && !transferValues.includes(colorSpace.transfer)) {
|
|
throw new TypeError(\`Video chunk metadata decoder configuration colorSpace transfer, when defined, must be one of \${transferValues.join(", ")}.\`);
|
|
}
|
|
const matrixValues = Object.keys(MATRIX_COEFFICIENTS_MAP);
|
|
if (colorSpace.matrix != null && !matrixValues.includes(colorSpace.matrix)) {
|
|
throw new TypeError(\`Video chunk metadata decoder configuration colorSpace matrix, when defined, must be one of \${matrixValues.join(", ")}.\`);
|
|
}
|
|
if (colorSpace.fullRange != null && typeof colorSpace.fullRange !== "boolean") {
|
|
throw new TypeError("Video chunk metadata decoder configuration colorSpace fullRange, when defined, must be a boolean.");
|
|
}
|
|
}
|
|
if (metadata.decoderConfig.codec.startsWith("avc1") || metadata.decoderConfig.codec.startsWith("avc3")) {
|
|
if (!AVC_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
|
|
throw new TypeError("Video chunk metadata decoder configuration codec string for AVC must be a valid AVC codec string as specified in Section 3.4 of RFC 6381.");
|
|
}
|
|
} else if (metadata.decoderConfig.codec.startsWith("hev1") || metadata.decoderConfig.codec.startsWith("hvc1")) {
|
|
if (!HEVC_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
|
|
throw new TypeError("Video chunk metadata decoder configuration codec string for HEVC must be a valid HEVC codec string as specified in Section E.3 of ISO 14496-15.");
|
|
}
|
|
} else if (metadata.decoderConfig.codec.startsWith("vp8")) {
|
|
if (metadata.decoderConfig.codec !== "vp8") {
|
|
throw new TypeError('Video chunk metadata decoder configuration codec string for VP8 must be "vp8".');
|
|
}
|
|
} else if (metadata.decoderConfig.codec.startsWith("vp09")) {
|
|
if (!VP9_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
|
|
throw new TypeError('Video chunk metadata decoder configuration codec string for VP9 must be a valid VP9 codec string as specified in Section "Codecs Parameter String" of https://www.webmproject.org/vp9/mp4/.');
|
|
}
|
|
} else if (metadata.decoderConfig.codec.startsWith("av01")) {
|
|
if (!AV1_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
|
|
throw new TypeError('Video chunk metadata decoder configuration codec string for AV1 must be a valid AV1 codec string as specified in Section "Codecs Parameter String" of https://aomediacodec.github.io/av1-isobmff/.');
|
|
}
|
|
}
|
|
};
|
|
var VALID_AUDIO_CODEC_STRING_PREFIXES = [
|
|
"mp4a",
|
|
"mp3",
|
|
"opus",
|
|
"vorbis",
|
|
"flac",
|
|
"ulaw",
|
|
"alaw",
|
|
"pcm",
|
|
"ac-3",
|
|
"ec-3"
|
|
];
|
|
var validateAudioChunkMetadata = (metadata) => {
|
|
if (!metadata) {
|
|
throw new TypeError("Audio chunk metadata must be provided.");
|
|
}
|
|
if (typeof metadata !== "object") {
|
|
throw new TypeError("Audio chunk metadata must be an object.");
|
|
}
|
|
if (!metadata.decoderConfig) {
|
|
throw new TypeError("Audio chunk metadata must include a decoder configuration.");
|
|
}
|
|
if (typeof metadata.decoderConfig !== "object") {
|
|
throw new TypeError("Audio chunk metadata decoder configuration must be an object.");
|
|
}
|
|
if (typeof metadata.decoderConfig.codec !== "string") {
|
|
throw new TypeError("Audio chunk metadata decoder configuration must specify a codec string.");
|
|
}
|
|
if (!VALID_AUDIO_CODEC_STRING_PREFIXES.some((prefix) => metadata.decoderConfig.codec.startsWith(prefix))) {
|
|
throw new TypeError("Audio chunk metadata decoder configuration codec string must be a valid audio codec string as specified in the Mediabunny Codec Registry.");
|
|
}
|
|
if (!Number.isInteger(metadata.decoderConfig.sampleRate) || metadata.decoderConfig.sampleRate <= 0) {
|
|
throw new TypeError("Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).");
|
|
}
|
|
if (!Number.isInteger(metadata.decoderConfig.numberOfChannels) || metadata.decoderConfig.numberOfChannels <= 0) {
|
|
throw new TypeError("Audio chunk metadata decoder configuration must specify a valid numberOfChannels (positive integer).");
|
|
}
|
|
if (metadata.decoderConfig.description !== void 0) {
|
|
if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
|
|
throw new TypeError("Audio chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");
|
|
}
|
|
}
|
|
if (metadata.decoderConfig.codec.startsWith("mp4a") && metadata.decoderConfig.codec !== "mp4a.69" && metadata.decoderConfig.codec !== "mp4a.6B" && metadata.decoderConfig.codec !== "mp4a.6b") {
|
|
const validStrings = ["mp4a.40.2", "mp4a.40.02", "mp4a.40.5", "mp4a.40.05", "mp4a.40.29", "mp4a.67"];
|
|
if (!validStrings.includes(metadata.decoderConfig.codec)) {
|
|
throw new TypeError("Audio chunk metadata decoder configuration codec string for AAC must be a valid AAC codec string as specified in https://www.w3.org/TR/webcodecs-aac-codec-registration/.");
|
|
}
|
|
} else if (metadata.decoderConfig.codec.startsWith("mp3") || metadata.decoderConfig.codec.startsWith("mp4a")) {
|
|
if (metadata.decoderConfig.codec !== "mp3" && metadata.decoderConfig.codec !== "mp4a.69" && metadata.decoderConfig.codec !== "mp4a.6B" && metadata.decoderConfig.codec !== "mp4a.6b") {
|
|
throw new TypeError('Audio chunk metadata decoder configuration codec string for MP3 must be "mp3", "mp4a.69" or "mp4a.6B".');
|
|
}
|
|
} else if (metadata.decoderConfig.codec.startsWith("opus")) {
|
|
if (metadata.decoderConfig.codec !== "opus") {
|
|
throw new TypeError('Audio chunk metadata decoder configuration codec string for Opus must be "opus".');
|
|
}
|
|
if (metadata.decoderConfig.description && metadata.decoderConfig.description.byteLength < 18) {
|
|
throw new TypeError("Audio chunk metadata decoder configuration description, when specified, is expected to be an Identification Header as specified in Section 5.1 of RFC 7845.");
|
|
}
|
|
} else if (metadata.decoderConfig.codec.startsWith("vorbis")) {
|
|
if (metadata.decoderConfig.codec !== "vorbis") {
|
|
throw new TypeError('Audio chunk metadata decoder configuration codec string for Vorbis must be "vorbis".');
|
|
}
|
|
if (!metadata.decoderConfig.description) {
|
|
throw new TypeError("Audio chunk metadata decoder configuration for Vorbis must include a description, which is expected to adhere to the format described in https://www.w3.org/TR/webcodecs-vorbis-codec-registration/.");
|
|
}
|
|
} else if (metadata.decoderConfig.codec.startsWith("flac")) {
|
|
if (metadata.decoderConfig.codec !== "flac") {
|
|
throw new TypeError('Audio chunk metadata decoder configuration codec string for FLAC must be "flac".');
|
|
}
|
|
const minDescriptionSize = 4 + 4 + 34;
|
|
if (!metadata.decoderConfig.description || metadata.decoderConfig.description.byteLength < minDescriptionSize) {
|
|
throw new TypeError("Audio chunk metadata decoder configuration for FLAC must include a description, which is expected to adhere to the format described in https://www.w3.org/TR/webcodecs-flac-codec-registration/.");
|
|
}
|
|
} else if (metadata.decoderConfig.codec.startsWith("ac-3") || metadata.decoderConfig.codec.startsWith("ac3")) {
|
|
if (metadata.decoderConfig.codec !== "ac-3") {
|
|
throw new TypeError('Audio chunk metadata decoder configuration codec string for AC-3 must be "ac-3".');
|
|
}
|
|
} else if (metadata.decoderConfig.codec.startsWith("ec-3") || metadata.decoderConfig.codec.startsWith("eac3")) {
|
|
if (metadata.decoderConfig.codec !== "ec-3") {
|
|
throw new TypeError('Audio chunk metadata decoder configuration codec string for EC-3 must be "ec-3".');
|
|
}
|
|
} else if (metadata.decoderConfig.codec.startsWith("pcm") || metadata.decoderConfig.codec.startsWith("ulaw") || metadata.decoderConfig.codec.startsWith("alaw")) {
|
|
if (!PCM_AUDIO_CODECS.includes(metadata.decoderConfig.codec)) {
|
|
throw new TypeError(\`Audio chunk metadata decoder configuration codec string for PCM must be one of the supported PCM codecs (\${PCM_AUDIO_CODECS.join(", ")}).\`);
|
|
}
|
|
}
|
|
};
|
|
var validateSubtitleMetadata = (metadata) => {
|
|
if (!metadata) {
|
|
throw new TypeError("Subtitle metadata must be provided.");
|
|
}
|
|
if (typeof metadata !== "object") {
|
|
throw new TypeError("Subtitle metadata must be an object.");
|
|
}
|
|
if (!metadata.config) {
|
|
throw new TypeError("Subtitle metadata must include a config object.");
|
|
}
|
|
if (typeof metadata.config !== "object") {
|
|
throw new TypeError("Subtitle metadata config must be an object.");
|
|
}
|
|
if (typeof metadata.config.description !== "string") {
|
|
throw new TypeError("Subtitle metadata config description must be a string.");
|
|
}
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/shared/ac3-misc.js
|
|
var AC3_SAMPLE_RATES = [48e3, 44100, 32e3];
|
|
var EAC3_REDUCED_SAMPLE_RATES = [24e3, 22050, 16e3];
|
|
|
|
// node_modules/mediabunny/dist/modules/src/codec-data.js
|
|
var AvcNalUnitType;
|
|
(function(AvcNalUnitType2) {
|
|
AvcNalUnitType2[AvcNalUnitType2["NON_IDR_SLICE"] = 1] = "NON_IDR_SLICE";
|
|
AvcNalUnitType2[AvcNalUnitType2["SLICE_DPA"] = 2] = "SLICE_DPA";
|
|
AvcNalUnitType2[AvcNalUnitType2["SLICE_DPB"] = 3] = "SLICE_DPB";
|
|
AvcNalUnitType2[AvcNalUnitType2["SLICE_DPC"] = 4] = "SLICE_DPC";
|
|
AvcNalUnitType2[AvcNalUnitType2["IDR"] = 5] = "IDR";
|
|
AvcNalUnitType2[AvcNalUnitType2["SEI"] = 6] = "SEI";
|
|
AvcNalUnitType2[AvcNalUnitType2["SPS"] = 7] = "SPS";
|
|
AvcNalUnitType2[AvcNalUnitType2["PPS"] = 8] = "PPS";
|
|
AvcNalUnitType2[AvcNalUnitType2["AUD"] = 9] = "AUD";
|
|
AvcNalUnitType2[AvcNalUnitType2["SPS_EXT"] = 13] = "SPS_EXT";
|
|
})(AvcNalUnitType || (AvcNalUnitType = {}));
|
|
var HevcNalUnitType;
|
|
(function(HevcNalUnitType2) {
|
|
HevcNalUnitType2[HevcNalUnitType2["RASL_N"] = 8] = "RASL_N";
|
|
HevcNalUnitType2[HevcNalUnitType2["RASL_R"] = 9] = "RASL_R";
|
|
HevcNalUnitType2[HevcNalUnitType2["BLA_W_LP"] = 16] = "BLA_W_LP";
|
|
HevcNalUnitType2[HevcNalUnitType2["RSV_IRAP_VCL23"] = 23] = "RSV_IRAP_VCL23";
|
|
HevcNalUnitType2[HevcNalUnitType2["VPS_NUT"] = 32] = "VPS_NUT";
|
|
HevcNalUnitType2[HevcNalUnitType2["SPS_NUT"] = 33] = "SPS_NUT";
|
|
HevcNalUnitType2[HevcNalUnitType2["PPS_NUT"] = 34] = "PPS_NUT";
|
|
HevcNalUnitType2[HevcNalUnitType2["AUD_NUT"] = 35] = "AUD_NUT";
|
|
HevcNalUnitType2[HevcNalUnitType2["PREFIX_SEI_NUT"] = 39] = "PREFIX_SEI_NUT";
|
|
HevcNalUnitType2[HevcNalUnitType2["SUFFIX_SEI_NUT"] = 40] = "SUFFIX_SEI_NUT";
|
|
})(HevcNalUnitType || (HevcNalUnitType = {}));
|
|
var iterateNalUnitsInAnnexB = function* (packetData) {
|
|
let i2 = 0;
|
|
let nalStart = -1;
|
|
while (i2 < packetData.length - 2) {
|
|
const zeroIndex = packetData.indexOf(0, i2);
|
|
if (zeroIndex === -1 || zeroIndex >= packetData.length - 2) {
|
|
break;
|
|
}
|
|
i2 = zeroIndex;
|
|
let startCodeLength = 0;
|
|
if (i2 + 3 < packetData.length && packetData[i2 + 1] === 0 && packetData[i2 + 2] === 0 && packetData[i2 + 3] === 1) {
|
|
startCodeLength = 4;
|
|
} else if (packetData[i2 + 1] === 0 && packetData[i2 + 2] === 1) {
|
|
startCodeLength = 3;
|
|
}
|
|
if (startCodeLength === 0) {
|
|
i2++;
|
|
continue;
|
|
}
|
|
if (nalStart !== -1 && i2 > nalStart) {
|
|
yield {
|
|
offset: nalStart,
|
|
length: i2 - nalStart
|
|
};
|
|
}
|
|
nalStart = i2 + startCodeLength;
|
|
i2 = nalStart;
|
|
}
|
|
if (nalStart !== -1 && nalStart < packetData.length) {
|
|
yield {
|
|
offset: nalStart,
|
|
length: packetData.length - nalStart
|
|
};
|
|
}
|
|
};
|
|
var iterateNalUnitsInLengthPrefixed = function* (packetData, lengthSize) {
|
|
let offset = 0;
|
|
const dataView = new DataView(packetData.buffer, packetData.byteOffset, packetData.byteLength);
|
|
while (offset + lengthSize <= packetData.length) {
|
|
let nalUnitLength;
|
|
if (lengthSize === 1) {
|
|
nalUnitLength = dataView.getUint8(offset);
|
|
} else if (lengthSize === 2) {
|
|
nalUnitLength = dataView.getUint16(offset, false);
|
|
} else if (lengthSize === 3) {
|
|
nalUnitLength = getUint24(dataView, offset, false);
|
|
} else {
|
|
assert(lengthSize === 4);
|
|
nalUnitLength = dataView.getUint32(offset, false);
|
|
}
|
|
offset += lengthSize;
|
|
yield {
|
|
offset,
|
|
length: nalUnitLength
|
|
};
|
|
offset += nalUnitLength;
|
|
}
|
|
};
|
|
var iterateAvcNalUnits = (packetData, decoderConfig) => {
|
|
if (decoderConfig.description) {
|
|
const bytes2 = toUint8Array(decoderConfig.description);
|
|
const lengthSizeMinusOne = bytes2[4] & 3;
|
|
const lengthSize = lengthSizeMinusOne + 1;
|
|
return iterateNalUnitsInLengthPrefixed(packetData, lengthSize);
|
|
} else {
|
|
return iterateNalUnitsInAnnexB(packetData);
|
|
}
|
|
};
|
|
var extractNalUnitTypeForAvc = (byte) => {
|
|
return byte & 31;
|
|
};
|
|
var removeEmulationPreventionBytes = (data) => {
|
|
const result = [];
|
|
const len = data.length;
|
|
for (let i2 = 0; i2 < len; i2++) {
|
|
if (i2 + 2 < len && data[i2] === 0 && data[i2 + 1] === 0 && data[i2 + 2] === 3) {
|
|
result.push(0, 0);
|
|
i2 += 2;
|
|
} else {
|
|
result.push(data[i2]);
|
|
}
|
|
}
|
|
return new Uint8Array(result);
|
|
};
|
|
var ANNEX_B_START_CODE = new Uint8Array([0, 0, 0, 1]);
|
|
var concatNalUnitsInLengthPrefixed = (nalUnits, lengthSize) => {
|
|
const totalLength = nalUnits.reduce((a2, b) => a2 + lengthSize + b.byteLength, 0);
|
|
const result = new Uint8Array(totalLength);
|
|
let offset = 0;
|
|
for (const nalUnit of nalUnits) {
|
|
const dataView = new DataView(result.buffer, result.byteOffset, result.byteLength);
|
|
switch (lengthSize) {
|
|
case 1:
|
|
dataView.setUint8(offset, nalUnit.byteLength);
|
|
break;
|
|
case 2:
|
|
dataView.setUint16(offset, nalUnit.byteLength, false);
|
|
break;
|
|
case 3:
|
|
setUint24(dataView, offset, nalUnit.byteLength, false);
|
|
break;
|
|
case 4:
|
|
dataView.setUint32(offset, nalUnit.byteLength, false);
|
|
break;
|
|
}
|
|
offset += lengthSize;
|
|
result.set(nalUnit, offset);
|
|
offset += nalUnit.byteLength;
|
|
}
|
|
return result;
|
|
};
|
|
var extractAvcDecoderConfigurationRecord = (packetData) => {
|
|
try {
|
|
const spsUnits = [];
|
|
const ppsUnits = [];
|
|
const spsExtUnits = [];
|
|
for (const loc of iterateNalUnitsInAnnexB(packetData)) {
|
|
const nalUnit = packetData.subarray(loc.offset, loc.offset + loc.length);
|
|
const type = extractNalUnitTypeForAvc(nalUnit[0]);
|
|
if (type === AvcNalUnitType.SPS) {
|
|
spsUnits.push(nalUnit);
|
|
} else if (type === AvcNalUnitType.PPS) {
|
|
ppsUnits.push(nalUnit);
|
|
} else if (type === AvcNalUnitType.SPS_EXT) {
|
|
spsExtUnits.push(nalUnit);
|
|
}
|
|
}
|
|
if (spsUnits.length === 0) {
|
|
return null;
|
|
}
|
|
if (ppsUnits.length === 0) {
|
|
return null;
|
|
}
|
|
const spsData = spsUnits[0];
|
|
const spsInfo = parseAvcSps(spsData);
|
|
assert(spsInfo !== null);
|
|
const hasExtendedData = spsInfo.profileIdc === 100 || spsInfo.profileIdc === 110 || spsInfo.profileIdc === 122 || spsInfo.profileIdc === 144;
|
|
return {
|
|
configurationVersion: 1,
|
|
avcProfileIndication: spsInfo.profileIdc,
|
|
profileCompatibility: spsInfo.constraintFlags,
|
|
avcLevelIndication: spsInfo.levelIdc,
|
|
lengthSizeMinusOne: 3,
|
|
// Typically 4 bytes for length field
|
|
sequenceParameterSets: spsUnits,
|
|
pictureParameterSets: ppsUnits,
|
|
chromaFormat: hasExtendedData ? spsInfo.chromaFormatIdc : null,
|
|
bitDepthLumaMinus8: hasExtendedData ? spsInfo.bitDepthLumaMinus8 : null,
|
|
bitDepthChromaMinus8: hasExtendedData ? spsInfo.bitDepthChromaMinus8 : null,
|
|
sequenceParameterSetExt: hasExtendedData ? spsExtUnits : null
|
|
};
|
|
} catch (error) {
|
|
Logging._error("Error building AVC Decoder Configuration Record:", error);
|
|
return null;
|
|
}
|
|
};
|
|
var serializeAvcDecoderConfigurationRecord = (record) => {
|
|
const bytes2 = [];
|
|
bytes2.push(record.configurationVersion);
|
|
bytes2.push(record.avcProfileIndication);
|
|
bytes2.push(record.profileCompatibility);
|
|
bytes2.push(record.avcLevelIndication);
|
|
bytes2.push(252 | record.lengthSizeMinusOne & 3);
|
|
bytes2.push(224 | record.sequenceParameterSets.length & 31);
|
|
for (const sps of record.sequenceParameterSets) {
|
|
const length = sps.byteLength;
|
|
bytes2.push(length >> 8);
|
|
bytes2.push(length & 255);
|
|
for (let i2 = 0; i2 < length; i2++) {
|
|
bytes2.push(sps[i2]);
|
|
}
|
|
}
|
|
bytes2.push(record.pictureParameterSets.length);
|
|
for (const pps of record.pictureParameterSets) {
|
|
const length = pps.byteLength;
|
|
bytes2.push(length >> 8);
|
|
bytes2.push(length & 255);
|
|
for (let i2 = 0; i2 < length; i2++) {
|
|
bytes2.push(pps[i2]);
|
|
}
|
|
}
|
|
if (record.avcProfileIndication === 100 || record.avcProfileIndication === 110 || record.avcProfileIndication === 122 || record.avcProfileIndication === 144) {
|
|
assert(record.chromaFormat !== null);
|
|
assert(record.bitDepthLumaMinus8 !== null);
|
|
assert(record.bitDepthChromaMinus8 !== null);
|
|
assert(record.sequenceParameterSetExt !== null);
|
|
bytes2.push(252 | record.chromaFormat & 3);
|
|
bytes2.push(248 | record.bitDepthLumaMinus8 & 7);
|
|
bytes2.push(248 | record.bitDepthChromaMinus8 & 7);
|
|
bytes2.push(record.sequenceParameterSetExt.length);
|
|
for (const spsExt of record.sequenceParameterSetExt) {
|
|
const length = spsExt.byteLength;
|
|
bytes2.push(length >> 8);
|
|
bytes2.push(length & 255);
|
|
for (let i2 = 0; i2 < length; i2++) {
|
|
bytes2.push(spsExt[i2]);
|
|
}
|
|
}
|
|
}
|
|
return new Uint8Array(bytes2);
|
|
};
|
|
var AVC_HEVC_ASPECT_RATIO_IDC_TABLE = {
|
|
1: { num: 1, den: 1 },
|
|
2: { num: 12, den: 11 },
|
|
3: { num: 10, den: 11 },
|
|
4: { num: 16, den: 11 },
|
|
5: { num: 40, den: 33 },
|
|
6: { num: 24, den: 11 },
|
|
7: { num: 20, den: 11 },
|
|
8: { num: 32, den: 11 },
|
|
9: { num: 80, den: 33 },
|
|
10: { num: 18, den: 11 },
|
|
11: { num: 15, den: 11 },
|
|
12: { num: 64, den: 33 },
|
|
13: { num: 160, den: 99 },
|
|
14: { num: 4, den: 3 },
|
|
15: { num: 3, den: 2 },
|
|
16: { num: 2, den: 1 }
|
|
};
|
|
var parseAvcSps = (sps) => {
|
|
try {
|
|
const bitstream = new Bitstream(removeEmulationPreventionBytes(sps));
|
|
bitstream.skipBits(1);
|
|
bitstream.skipBits(2);
|
|
const nalUnitType = bitstream.readBits(5);
|
|
if (nalUnitType !== 7) {
|
|
return null;
|
|
}
|
|
const profileIdc = bitstream.readAlignedByte();
|
|
const constraintFlags = bitstream.readAlignedByte();
|
|
const levelIdc = bitstream.readAlignedByte();
|
|
readExpGolomb(bitstream);
|
|
let chromaFormatIdc = 1;
|
|
let bitDepthLumaMinus8 = 0;
|
|
let bitDepthChromaMinus8 = 0;
|
|
let separateColourPlaneFlag = 0;
|
|
if (profileIdc === 100 || profileIdc === 110 || profileIdc === 122 || profileIdc === 244 || profileIdc === 44 || profileIdc === 83 || profileIdc === 86 || profileIdc === 118 || profileIdc === 128) {
|
|
chromaFormatIdc = readExpGolomb(bitstream);
|
|
if (chromaFormatIdc === 3) {
|
|
separateColourPlaneFlag = bitstream.readBits(1);
|
|
}
|
|
bitDepthLumaMinus8 = readExpGolomb(bitstream);
|
|
bitDepthChromaMinus8 = readExpGolomb(bitstream);
|
|
bitstream.skipBits(1);
|
|
const seqScalingMatrixPresentFlag = bitstream.readBits(1);
|
|
if (seqScalingMatrixPresentFlag) {
|
|
for (let i2 = 0; i2 < (chromaFormatIdc !== 3 ? 8 : 12); i2++) {
|
|
const seqScalingListPresentFlag = bitstream.readBits(1);
|
|
if (seqScalingListPresentFlag) {
|
|
const sizeOfScalingList = i2 < 6 ? 16 : 64;
|
|
let lastScale = 8;
|
|
let nextScale = 8;
|
|
for (let j = 0; j < sizeOfScalingList; j++) {
|
|
if (nextScale !== 0) {
|
|
const deltaScale = readSignedExpGolomb(bitstream);
|
|
nextScale = (lastScale + deltaScale + 256) % 256;
|
|
}
|
|
lastScale = nextScale === 0 ? lastScale : nextScale;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
readExpGolomb(bitstream);
|
|
const picOrderCntType = readExpGolomb(bitstream);
|
|
if (picOrderCntType === 0) {
|
|
readExpGolomb(bitstream);
|
|
} else if (picOrderCntType === 1) {
|
|
bitstream.skipBits(1);
|
|
readSignedExpGolomb(bitstream);
|
|
readSignedExpGolomb(bitstream);
|
|
const numRefFramesInPicOrderCntCycle = readExpGolomb(bitstream);
|
|
for (let i2 = 0; i2 < numRefFramesInPicOrderCntCycle; i2++) {
|
|
readSignedExpGolomb(bitstream);
|
|
}
|
|
}
|
|
readExpGolomb(bitstream);
|
|
bitstream.skipBits(1);
|
|
const picWidthInMbsMinus1 = readExpGolomb(bitstream);
|
|
const picHeightInMapUnitsMinus1 = readExpGolomb(bitstream);
|
|
const codedWidth = 16 * (picWidthInMbsMinus1 + 1);
|
|
const codedHeight = 16 * (picHeightInMapUnitsMinus1 + 1);
|
|
let displayWidth = codedWidth;
|
|
let displayHeight = codedHeight;
|
|
const frameMbsOnlyFlag = bitstream.readBits(1);
|
|
if (!frameMbsOnlyFlag) {
|
|
bitstream.skipBits(1);
|
|
}
|
|
bitstream.skipBits(1);
|
|
const frameCroppingFlag = bitstream.readBits(1);
|
|
if (frameCroppingFlag) {
|
|
const frameCropLeftOffset = readExpGolomb(bitstream);
|
|
const frameCropRightOffset = readExpGolomb(bitstream);
|
|
const frameCropTopOffset = readExpGolomb(bitstream);
|
|
const frameCropBottomOffset = readExpGolomb(bitstream);
|
|
let cropUnitX;
|
|
let cropUnitY;
|
|
const chromaArrayType = separateColourPlaneFlag === 0 ? chromaFormatIdc : 0;
|
|
if (chromaArrayType === 0) {
|
|
cropUnitX = 1;
|
|
cropUnitY = 2 - frameMbsOnlyFlag;
|
|
} else {
|
|
const subWidthC = chromaFormatIdc === 3 ? 1 : 2;
|
|
const subHeightC = chromaFormatIdc === 1 ? 2 : 1;
|
|
cropUnitX = subWidthC;
|
|
cropUnitY = subHeightC * (2 - frameMbsOnlyFlag);
|
|
}
|
|
displayWidth -= cropUnitX * (frameCropLeftOffset + frameCropRightOffset);
|
|
displayHeight -= cropUnitY * (frameCropTopOffset + frameCropBottomOffset);
|
|
}
|
|
let colourPrimaries = 2;
|
|
let transferCharacteristics = 2;
|
|
let matrixCoefficients = 2;
|
|
let fullRangeFlag = 0;
|
|
let pixelAspectRatio = { num: 1, den: 1 };
|
|
let numReorderFrames = null;
|
|
let maxDecFrameBuffering = null;
|
|
const vuiParametersPresentFlag = bitstream.readBits(1);
|
|
if (vuiParametersPresentFlag) {
|
|
const aspectRatioInfoPresentFlag = bitstream.readBits(1);
|
|
if (aspectRatioInfoPresentFlag) {
|
|
const aspectRatioIdc = bitstream.readBits(8);
|
|
if (aspectRatioIdc === 255) {
|
|
pixelAspectRatio = {
|
|
num: bitstream.readBits(16),
|
|
den: bitstream.readBits(16)
|
|
};
|
|
} else {
|
|
const aspectRatio = AVC_HEVC_ASPECT_RATIO_IDC_TABLE[aspectRatioIdc];
|
|
if (aspectRatio) {
|
|
pixelAspectRatio = aspectRatio;
|
|
}
|
|
}
|
|
}
|
|
const overscanInfoPresentFlag = bitstream.readBits(1);
|
|
if (overscanInfoPresentFlag) {
|
|
bitstream.skipBits(1);
|
|
}
|
|
const videoSignalTypePresentFlag = bitstream.readBits(1);
|
|
if (videoSignalTypePresentFlag) {
|
|
bitstream.skipBits(3);
|
|
fullRangeFlag = bitstream.readBits(1);
|
|
const colourDescriptionPresentFlag = bitstream.readBits(1);
|
|
if (colourDescriptionPresentFlag) {
|
|
colourPrimaries = bitstream.readBits(8);
|
|
transferCharacteristics = bitstream.readBits(8);
|
|
matrixCoefficients = bitstream.readBits(8);
|
|
}
|
|
}
|
|
const chromaLocInfoPresentFlag = bitstream.readBits(1);
|
|
if (chromaLocInfoPresentFlag) {
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
}
|
|
const timingInfoPresentFlag = bitstream.readBits(1);
|
|
if (timingInfoPresentFlag) {
|
|
bitstream.skipBits(32);
|
|
bitstream.skipBits(32);
|
|
bitstream.skipBits(1);
|
|
}
|
|
const nalHrdParametersPresentFlag = bitstream.readBits(1);
|
|
if (nalHrdParametersPresentFlag) {
|
|
skipAvcHrdParameters(bitstream);
|
|
}
|
|
const vclHrdParametersPresentFlag = bitstream.readBits(1);
|
|
if (vclHrdParametersPresentFlag) {
|
|
skipAvcHrdParameters(bitstream);
|
|
}
|
|
if (nalHrdParametersPresentFlag || vclHrdParametersPresentFlag) {
|
|
bitstream.skipBits(1);
|
|
}
|
|
bitstream.skipBits(1);
|
|
const bitstreamRestrictionFlag = bitstream.readBits(1);
|
|
if (bitstreamRestrictionFlag) {
|
|
bitstream.skipBits(1);
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
numReorderFrames = readExpGolomb(bitstream);
|
|
maxDecFrameBuffering = readExpGolomb(bitstream);
|
|
}
|
|
}
|
|
if (numReorderFrames === null) {
|
|
assert(maxDecFrameBuffering === null);
|
|
const constraintSet3Flag = constraintFlags & 16;
|
|
if ((profileIdc === 44 || profileIdc === 86 || profileIdc === 100 || profileIdc === 110 || profileIdc === 122 || profileIdc === 244) && constraintSet3Flag) {
|
|
numReorderFrames = 0;
|
|
maxDecFrameBuffering = 0;
|
|
} else {
|
|
const picWidthInMbs = picWidthInMbsMinus1 + 1;
|
|
const picHeightInMapUnits = picHeightInMapUnitsMinus1 + 1;
|
|
const frameHeightInMbs = (2 - frameMbsOnlyFlag) * picHeightInMapUnits;
|
|
const levelInfo = AVC_LEVEL_TABLE.find((x) => x.level >= levelIdc) ?? last(AVC_LEVEL_TABLE);
|
|
const maxDpbFrames = Math.min(Math.floor(levelInfo.maxDpbMbs / (picWidthInMbs * frameHeightInMbs)), 16);
|
|
numReorderFrames = maxDpbFrames;
|
|
maxDecFrameBuffering = maxDpbFrames;
|
|
}
|
|
}
|
|
assert(maxDecFrameBuffering !== null);
|
|
return {
|
|
profileIdc,
|
|
constraintFlags,
|
|
levelIdc,
|
|
frameMbsOnlyFlag,
|
|
chromaFormatIdc,
|
|
bitDepthLumaMinus8,
|
|
bitDepthChromaMinus8,
|
|
codedWidth,
|
|
codedHeight,
|
|
displayWidth,
|
|
displayHeight,
|
|
pixelAspectRatio,
|
|
colourPrimaries,
|
|
matrixCoefficients,
|
|
transferCharacteristics,
|
|
fullRangeFlag,
|
|
numReorderFrames,
|
|
maxDecFrameBuffering
|
|
};
|
|
} catch (error) {
|
|
Logging._error("Error parsing AVC SPS:", error);
|
|
return null;
|
|
}
|
|
};
|
|
var skipAvcHrdParameters = (bitstream) => {
|
|
const cpb_cnt_minus1 = readExpGolomb(bitstream);
|
|
bitstream.skipBits(4);
|
|
bitstream.skipBits(4);
|
|
for (let i2 = 0; i2 <= cpb_cnt_minus1; i2++) {
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
bitstream.skipBits(1);
|
|
}
|
|
bitstream.skipBits(5);
|
|
bitstream.skipBits(5);
|
|
bitstream.skipBits(5);
|
|
bitstream.skipBits(5);
|
|
};
|
|
var iterateHevcNalUnits = (packetData, decoderConfig) => {
|
|
if (decoderConfig.description) {
|
|
const bytes2 = toUint8Array(decoderConfig.description);
|
|
const lengthSizeMinusOne = bytes2[21] & 3;
|
|
const lengthSize = lengthSizeMinusOne + 1;
|
|
return iterateNalUnitsInLengthPrefixed(packetData, lengthSize);
|
|
} else {
|
|
return iterateNalUnitsInAnnexB(packetData);
|
|
}
|
|
};
|
|
var extractNalUnitTypeForHevc = (byte) => {
|
|
return byte >> 1 & 63;
|
|
};
|
|
var parseHevcSps = (sps) => {
|
|
try {
|
|
const bitstream = new Bitstream(removeEmulationPreventionBytes(sps));
|
|
bitstream.skipBits(16);
|
|
bitstream.readBits(4);
|
|
const spsMaxSubLayersMinus1 = bitstream.readBits(3);
|
|
const spsTemporalIdNestingFlag = bitstream.readBits(1);
|
|
const { general_profile_space, general_tier_flag, general_profile_idc, general_profile_compatibility_flags, general_constraint_indicator_flags, general_level_idc } = parseProfileTierLevel(bitstream, spsMaxSubLayersMinus1);
|
|
readExpGolomb(bitstream);
|
|
const chromaFormatIdc = readExpGolomb(bitstream);
|
|
let separateColourPlaneFlag = 0;
|
|
if (chromaFormatIdc === 3) {
|
|
separateColourPlaneFlag = bitstream.readBits(1);
|
|
}
|
|
const picWidthInLumaSamples = readExpGolomb(bitstream);
|
|
const picHeightInLumaSamples = readExpGolomb(bitstream);
|
|
let displayWidth = picWidthInLumaSamples;
|
|
let displayHeight = picHeightInLumaSamples;
|
|
if (bitstream.readBits(1)) {
|
|
const confWinLeftOffset = readExpGolomb(bitstream);
|
|
const confWinRightOffset = readExpGolomb(bitstream);
|
|
const confWinTopOffset = readExpGolomb(bitstream);
|
|
const confWinBottomOffset = readExpGolomb(bitstream);
|
|
let subWidthC = 1;
|
|
let subHeightC = 1;
|
|
const chromaArrayType = separateColourPlaneFlag === 0 ? chromaFormatIdc : 0;
|
|
if (chromaArrayType === 1) {
|
|
subWidthC = 2;
|
|
subHeightC = 2;
|
|
} else if (chromaArrayType === 2) {
|
|
subWidthC = 2;
|
|
subHeightC = 1;
|
|
}
|
|
displayWidth -= (confWinLeftOffset + confWinRightOffset) * subWidthC;
|
|
displayHeight -= (confWinTopOffset + confWinBottomOffset) * subHeightC;
|
|
}
|
|
const bitDepthLumaMinus8 = readExpGolomb(bitstream);
|
|
const bitDepthChromaMinus8 = readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
const spsSubLayerOrderingInfoPresentFlag = bitstream.readBits(1);
|
|
const startI = spsSubLayerOrderingInfoPresentFlag ? 0 : spsMaxSubLayersMinus1;
|
|
let spsMaxNumReorderPics = 0;
|
|
for (let i2 = startI; i2 <= spsMaxSubLayersMinus1; i2++) {
|
|
readExpGolomb(bitstream);
|
|
spsMaxNumReorderPics = readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
}
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
if (bitstream.readBits(1)) {
|
|
if (bitstream.readBits(1)) {
|
|
skipScalingListData(bitstream);
|
|
}
|
|
}
|
|
bitstream.skipBits(1);
|
|
bitstream.skipBits(1);
|
|
if (bitstream.readBits(1)) {
|
|
bitstream.skipBits(4);
|
|
bitstream.skipBits(4);
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
bitstream.skipBits(1);
|
|
}
|
|
const numShortTermRefPicSets = readExpGolomb(bitstream);
|
|
skipAllStRefPicSets(bitstream, numShortTermRefPicSets);
|
|
if (bitstream.readBits(1)) {
|
|
const numLongTermRefPicsSps = readExpGolomb(bitstream);
|
|
for (let i2 = 0; i2 < numLongTermRefPicsSps; i2++) {
|
|
readExpGolomb(bitstream);
|
|
bitstream.skipBits(1);
|
|
}
|
|
}
|
|
bitstream.skipBits(1);
|
|
bitstream.skipBits(1);
|
|
let colourPrimaries = 2;
|
|
let transferCharacteristics = 2;
|
|
let matrixCoefficients = 2;
|
|
let fullRangeFlag = 0;
|
|
let minSpatialSegmentationIdc = 0;
|
|
let pixelAspectRatio = { num: 1, den: 1 };
|
|
if (bitstream.readBits(1)) {
|
|
const vui = parseHevcVui(bitstream, spsMaxSubLayersMinus1);
|
|
pixelAspectRatio = vui.pixelAspectRatio;
|
|
colourPrimaries = vui.colourPrimaries;
|
|
transferCharacteristics = vui.transferCharacteristics;
|
|
matrixCoefficients = vui.matrixCoefficients;
|
|
fullRangeFlag = vui.fullRangeFlag;
|
|
minSpatialSegmentationIdc = vui.minSpatialSegmentationIdc;
|
|
}
|
|
return {
|
|
displayWidth,
|
|
displayHeight,
|
|
pixelAspectRatio,
|
|
colourPrimaries,
|
|
transferCharacteristics,
|
|
matrixCoefficients,
|
|
fullRangeFlag,
|
|
maxDecFrameBuffering: spsMaxNumReorderPics + 1,
|
|
spsMaxSubLayersMinus1,
|
|
spsTemporalIdNestingFlag,
|
|
generalProfileSpace: general_profile_space,
|
|
generalTierFlag: general_tier_flag,
|
|
generalProfileIdc: general_profile_idc,
|
|
generalProfileCompatibilityFlags: general_profile_compatibility_flags,
|
|
generalConstraintIndicatorFlags: general_constraint_indicator_flags,
|
|
generalLevelIdc: general_level_idc,
|
|
chromaFormatIdc,
|
|
bitDepthLumaMinus8,
|
|
bitDepthChromaMinus8,
|
|
minSpatialSegmentationIdc
|
|
};
|
|
} catch (error) {
|
|
Logging._error("Error parsing HEVC SPS:", error);
|
|
return null;
|
|
}
|
|
};
|
|
var extractHevcDecoderConfigurationRecord = (packetData) => {
|
|
try {
|
|
const vpsUnits = [];
|
|
const spsUnits = [];
|
|
const ppsUnits = [];
|
|
const seiUnits = [];
|
|
for (const loc of iterateNalUnitsInAnnexB(packetData)) {
|
|
const nalUnit = packetData.subarray(loc.offset, loc.offset + loc.length);
|
|
const type = extractNalUnitTypeForHevc(nalUnit[0]);
|
|
if (type === HevcNalUnitType.VPS_NUT) {
|
|
vpsUnits.push(nalUnit);
|
|
} else if (type === HevcNalUnitType.SPS_NUT) {
|
|
spsUnits.push(nalUnit);
|
|
} else if (type === HevcNalUnitType.PPS_NUT) {
|
|
ppsUnits.push(nalUnit);
|
|
} else if (type === HevcNalUnitType.PREFIX_SEI_NUT || type === HevcNalUnitType.SUFFIX_SEI_NUT) {
|
|
seiUnits.push(nalUnit);
|
|
}
|
|
}
|
|
if (spsUnits.length === 0 || ppsUnits.length === 0)
|
|
return null;
|
|
const spsInfo = parseHevcSps(spsUnits[0]);
|
|
if (!spsInfo)
|
|
return null;
|
|
let parallelismType = 0;
|
|
if (ppsUnits.length > 0) {
|
|
const pps = ppsUnits[0];
|
|
const ppsBitstream = new Bitstream(removeEmulationPreventionBytes(pps));
|
|
ppsBitstream.skipBits(16);
|
|
readExpGolomb(ppsBitstream);
|
|
readExpGolomb(ppsBitstream);
|
|
ppsBitstream.skipBits(1);
|
|
ppsBitstream.skipBits(1);
|
|
ppsBitstream.skipBits(3);
|
|
ppsBitstream.skipBits(1);
|
|
ppsBitstream.skipBits(1);
|
|
readExpGolomb(ppsBitstream);
|
|
readExpGolomb(ppsBitstream);
|
|
readSignedExpGolomb(ppsBitstream);
|
|
ppsBitstream.skipBits(1);
|
|
ppsBitstream.skipBits(1);
|
|
if (ppsBitstream.readBits(1)) {
|
|
readExpGolomb(ppsBitstream);
|
|
}
|
|
readSignedExpGolomb(ppsBitstream);
|
|
readSignedExpGolomb(ppsBitstream);
|
|
ppsBitstream.skipBits(1);
|
|
ppsBitstream.skipBits(1);
|
|
ppsBitstream.skipBits(1);
|
|
ppsBitstream.skipBits(1);
|
|
const tiles_enabled_flag = ppsBitstream.readBits(1);
|
|
const entropy_coding_sync_enabled_flag = ppsBitstream.readBits(1);
|
|
if (!tiles_enabled_flag && !entropy_coding_sync_enabled_flag)
|
|
parallelismType = 0;
|
|
else if (tiles_enabled_flag && !entropy_coding_sync_enabled_flag)
|
|
parallelismType = 2;
|
|
else if (!tiles_enabled_flag && entropy_coding_sync_enabled_flag)
|
|
parallelismType = 3;
|
|
else
|
|
parallelismType = 0;
|
|
}
|
|
const arrays = [
|
|
...vpsUnits.length ? [
|
|
{
|
|
arrayCompleteness: 1,
|
|
nalUnitType: HevcNalUnitType.VPS_NUT,
|
|
nalUnits: vpsUnits
|
|
}
|
|
] : [],
|
|
...spsUnits.length ? [
|
|
{
|
|
arrayCompleteness: 1,
|
|
nalUnitType: HevcNalUnitType.SPS_NUT,
|
|
nalUnits: spsUnits
|
|
}
|
|
] : [],
|
|
...ppsUnits.length ? [
|
|
{
|
|
arrayCompleteness: 1,
|
|
nalUnitType: HevcNalUnitType.PPS_NUT,
|
|
nalUnits: ppsUnits
|
|
}
|
|
] : [],
|
|
...seiUnits.length ? [
|
|
{
|
|
arrayCompleteness: 1,
|
|
nalUnitType: extractNalUnitTypeForHevc(seiUnits[0][0]),
|
|
nalUnits: seiUnits
|
|
}
|
|
] : []
|
|
];
|
|
const record = {
|
|
configurationVersion: 1,
|
|
generalProfileSpace: spsInfo.generalProfileSpace,
|
|
generalTierFlag: spsInfo.generalTierFlag,
|
|
generalProfileIdc: spsInfo.generalProfileIdc,
|
|
generalProfileCompatibilityFlags: spsInfo.generalProfileCompatibilityFlags,
|
|
generalConstraintIndicatorFlags: spsInfo.generalConstraintIndicatorFlags,
|
|
generalLevelIdc: spsInfo.generalLevelIdc,
|
|
minSpatialSegmentationIdc: spsInfo.minSpatialSegmentationIdc,
|
|
parallelismType,
|
|
chromaFormatIdc: spsInfo.chromaFormatIdc,
|
|
bitDepthLumaMinus8: spsInfo.bitDepthLumaMinus8,
|
|
bitDepthChromaMinus8: spsInfo.bitDepthChromaMinus8,
|
|
avgFrameRate: 0,
|
|
constantFrameRate: 0,
|
|
numTemporalLayers: spsInfo.spsMaxSubLayersMinus1 + 1,
|
|
temporalIdNested: spsInfo.spsTemporalIdNestingFlag,
|
|
lengthSizeMinusOne: 3,
|
|
arrays
|
|
};
|
|
return record;
|
|
} catch (error) {
|
|
Logging._error("Error building HEVC Decoder Configuration Record:", error);
|
|
return null;
|
|
}
|
|
};
|
|
var parseProfileTierLevel = (bitstream, maxNumSubLayersMinus1) => {
|
|
const general_profile_space = bitstream.readBits(2);
|
|
const general_tier_flag = bitstream.readBits(1);
|
|
const general_profile_idc = bitstream.readBits(5);
|
|
let general_profile_compatibility_flags = 0;
|
|
for (let i2 = 0; i2 < 32; i2++) {
|
|
general_profile_compatibility_flags = general_profile_compatibility_flags << 1 | bitstream.readBits(1);
|
|
}
|
|
const general_constraint_indicator_flags = new Uint8Array(6);
|
|
for (let i2 = 0; i2 < 6; i2++) {
|
|
general_constraint_indicator_flags[i2] = bitstream.readBits(8);
|
|
}
|
|
const general_level_idc = bitstream.readBits(8);
|
|
const sub_layer_profile_present_flag = [];
|
|
const sub_layer_level_present_flag = [];
|
|
for (let i2 = 0; i2 < maxNumSubLayersMinus1; i2++) {
|
|
sub_layer_profile_present_flag.push(bitstream.readBits(1));
|
|
sub_layer_level_present_flag.push(bitstream.readBits(1));
|
|
}
|
|
if (maxNumSubLayersMinus1 > 0) {
|
|
for (let i2 = maxNumSubLayersMinus1; i2 < 8; i2++) {
|
|
bitstream.skipBits(2);
|
|
}
|
|
}
|
|
for (let i2 = 0; i2 < maxNumSubLayersMinus1; i2++) {
|
|
if (sub_layer_profile_present_flag[i2])
|
|
bitstream.skipBits(88);
|
|
if (sub_layer_level_present_flag[i2])
|
|
bitstream.skipBits(8);
|
|
}
|
|
return {
|
|
general_profile_space,
|
|
general_tier_flag,
|
|
general_profile_idc,
|
|
general_profile_compatibility_flags,
|
|
general_constraint_indicator_flags,
|
|
general_level_idc
|
|
};
|
|
};
|
|
var skipScalingListData = (bitstream) => {
|
|
for (let sizeId = 0; sizeId < 4; sizeId++) {
|
|
for (let matrixId = 0; matrixId < (sizeId === 3 ? 2 : 6); matrixId++) {
|
|
const scaling_list_pred_mode_flag = bitstream.readBits(1);
|
|
if (!scaling_list_pred_mode_flag) {
|
|
readExpGolomb(bitstream);
|
|
} else {
|
|
const coefNum = Math.min(64, 1 << 4 + (sizeId << 1));
|
|
if (sizeId > 1) {
|
|
readSignedExpGolomb(bitstream);
|
|
}
|
|
for (let i2 = 0; i2 < coefNum; i2++) {
|
|
readSignedExpGolomb(bitstream);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
var skipAllStRefPicSets = (bitstream, num_short_term_ref_pic_sets) => {
|
|
const NumDeltaPocs = [];
|
|
for (let stRpsIdx = 0; stRpsIdx < num_short_term_ref_pic_sets; stRpsIdx++) {
|
|
NumDeltaPocs[stRpsIdx] = skipStRefPicSet(bitstream, stRpsIdx, num_short_term_ref_pic_sets, NumDeltaPocs);
|
|
}
|
|
};
|
|
var skipStRefPicSet = (bitstream, stRpsIdx, num_short_term_ref_pic_sets, NumDeltaPocs) => {
|
|
let NumDeltaPocsThis = 0;
|
|
let inter_ref_pic_set_prediction_flag = 0;
|
|
let RefRpsIdx = 0;
|
|
if (stRpsIdx !== 0) {
|
|
inter_ref_pic_set_prediction_flag = bitstream.readBits(1);
|
|
}
|
|
if (inter_ref_pic_set_prediction_flag) {
|
|
if (stRpsIdx === num_short_term_ref_pic_sets) {
|
|
const delta_idx_minus1 = readExpGolomb(bitstream);
|
|
RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1);
|
|
} else {
|
|
RefRpsIdx = stRpsIdx - 1;
|
|
}
|
|
bitstream.readBits(1);
|
|
readExpGolomb(bitstream);
|
|
const numDelta = NumDeltaPocs[RefRpsIdx] ?? 0;
|
|
for (let j = 0; j <= numDelta; j++) {
|
|
const used_by_curr_pic_flag = bitstream.readBits(1);
|
|
if (!used_by_curr_pic_flag) {
|
|
bitstream.readBits(1);
|
|
}
|
|
}
|
|
NumDeltaPocsThis = NumDeltaPocs[RefRpsIdx];
|
|
} else {
|
|
const num_negative_pics = readExpGolomb(bitstream);
|
|
const num_positive_pics = readExpGolomb(bitstream);
|
|
for (let i2 = 0; i2 < num_negative_pics; i2++) {
|
|
readExpGolomb(bitstream);
|
|
bitstream.readBits(1);
|
|
}
|
|
for (let i2 = 0; i2 < num_positive_pics; i2++) {
|
|
readExpGolomb(bitstream);
|
|
bitstream.readBits(1);
|
|
}
|
|
NumDeltaPocsThis = num_negative_pics + num_positive_pics;
|
|
}
|
|
return NumDeltaPocsThis;
|
|
};
|
|
var parseHevcVui = (bitstream, sps_max_sub_layers_minus1) => {
|
|
let colourPrimaries = 2;
|
|
let transferCharacteristics = 2;
|
|
let matrixCoefficients = 2;
|
|
let fullRangeFlag = 0;
|
|
let minSpatialSegmentationIdc = 0;
|
|
let pixelAspectRatio = { num: 1, den: 1 };
|
|
if (bitstream.readBits(1)) {
|
|
const aspect_ratio_idc = bitstream.readBits(8);
|
|
if (aspect_ratio_idc === 255) {
|
|
pixelAspectRatio = {
|
|
num: bitstream.readBits(16),
|
|
den: bitstream.readBits(16)
|
|
};
|
|
} else {
|
|
const aspectRatio = AVC_HEVC_ASPECT_RATIO_IDC_TABLE[aspect_ratio_idc];
|
|
if (aspectRatio) {
|
|
pixelAspectRatio = aspectRatio;
|
|
}
|
|
}
|
|
}
|
|
if (bitstream.readBits(1)) {
|
|
bitstream.readBits(1);
|
|
}
|
|
if (bitstream.readBits(1)) {
|
|
bitstream.readBits(3);
|
|
fullRangeFlag = bitstream.readBits(1);
|
|
if (bitstream.readBits(1)) {
|
|
colourPrimaries = bitstream.readBits(8);
|
|
transferCharacteristics = bitstream.readBits(8);
|
|
matrixCoefficients = bitstream.readBits(8);
|
|
}
|
|
}
|
|
if (bitstream.readBits(1)) {
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
}
|
|
bitstream.readBits(1);
|
|
bitstream.readBits(1);
|
|
bitstream.readBits(1);
|
|
if (bitstream.readBits(1)) {
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
}
|
|
if (bitstream.readBits(1)) {
|
|
bitstream.readBits(32);
|
|
bitstream.readBits(32);
|
|
if (bitstream.readBits(1)) {
|
|
readExpGolomb(bitstream);
|
|
}
|
|
if (bitstream.readBits(1)) {
|
|
skipHevcHrdParameters(bitstream, true, sps_max_sub_layers_minus1);
|
|
}
|
|
}
|
|
if (bitstream.readBits(1)) {
|
|
bitstream.readBits(1);
|
|
bitstream.readBits(1);
|
|
bitstream.readBits(1);
|
|
minSpatialSegmentationIdc = readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
}
|
|
return {
|
|
pixelAspectRatio,
|
|
colourPrimaries,
|
|
transferCharacteristics,
|
|
matrixCoefficients,
|
|
fullRangeFlag,
|
|
minSpatialSegmentationIdc
|
|
};
|
|
};
|
|
var skipHevcHrdParameters = (bitstream, commonInfPresentFlag, maxNumSubLayersMinus1) => {
|
|
let nal_hrd_parameters_present_flag = false;
|
|
let vcl_hrd_parameters_present_flag = false;
|
|
let sub_pic_hrd_params_present_flag = false;
|
|
if (commonInfPresentFlag) {
|
|
nal_hrd_parameters_present_flag = bitstream.readBits(1) === 1;
|
|
vcl_hrd_parameters_present_flag = bitstream.readBits(1) === 1;
|
|
if (nal_hrd_parameters_present_flag || vcl_hrd_parameters_present_flag) {
|
|
sub_pic_hrd_params_present_flag = bitstream.readBits(1) === 1;
|
|
if (sub_pic_hrd_params_present_flag) {
|
|
bitstream.readBits(8);
|
|
bitstream.readBits(5);
|
|
bitstream.readBits(1);
|
|
bitstream.readBits(5);
|
|
}
|
|
bitstream.readBits(4);
|
|
bitstream.readBits(4);
|
|
if (sub_pic_hrd_params_present_flag) {
|
|
bitstream.readBits(4);
|
|
}
|
|
bitstream.readBits(5);
|
|
bitstream.readBits(5);
|
|
bitstream.readBits(5);
|
|
}
|
|
}
|
|
for (let i2 = 0; i2 <= maxNumSubLayersMinus1; i2++) {
|
|
const fixed_pic_rate_general_flag = bitstream.readBits(1) === 1;
|
|
let fixed_pic_rate_within_cvs_flag = true;
|
|
if (!fixed_pic_rate_general_flag) {
|
|
fixed_pic_rate_within_cvs_flag = bitstream.readBits(1) === 1;
|
|
}
|
|
let low_delay_hrd_flag = false;
|
|
if (fixed_pic_rate_within_cvs_flag) {
|
|
readExpGolomb(bitstream);
|
|
} else {
|
|
low_delay_hrd_flag = bitstream.readBits(1) === 1;
|
|
}
|
|
let CpbCnt = 1;
|
|
if (!low_delay_hrd_flag) {
|
|
const cpb_cnt_minus1 = readExpGolomb(bitstream);
|
|
CpbCnt = cpb_cnt_minus1 + 1;
|
|
}
|
|
if (nal_hrd_parameters_present_flag) {
|
|
skipSubLayerHrdParameters(bitstream, CpbCnt, sub_pic_hrd_params_present_flag);
|
|
}
|
|
if (vcl_hrd_parameters_present_flag) {
|
|
skipSubLayerHrdParameters(bitstream, CpbCnt, sub_pic_hrd_params_present_flag);
|
|
}
|
|
}
|
|
};
|
|
var skipSubLayerHrdParameters = (bitstream, CpbCnt, sub_pic_hrd_params_present_flag) => {
|
|
for (let i2 = 0; i2 < CpbCnt; i2++) {
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
if (sub_pic_hrd_params_present_flag) {
|
|
readExpGolomb(bitstream);
|
|
readExpGolomb(bitstream);
|
|
}
|
|
bitstream.readBits(1);
|
|
}
|
|
};
|
|
var serializeHevcDecoderConfigurationRecord = (record) => {
|
|
const bytes2 = [];
|
|
bytes2.push(record.configurationVersion);
|
|
bytes2.push((record.generalProfileSpace & 3) << 6 | (record.generalTierFlag & 1) << 5 | record.generalProfileIdc & 31);
|
|
bytes2.push(record.generalProfileCompatibilityFlags >>> 24 & 255);
|
|
bytes2.push(record.generalProfileCompatibilityFlags >>> 16 & 255);
|
|
bytes2.push(record.generalProfileCompatibilityFlags >>> 8 & 255);
|
|
bytes2.push(record.generalProfileCompatibilityFlags & 255);
|
|
bytes2.push(...record.generalConstraintIndicatorFlags);
|
|
bytes2.push(record.generalLevelIdc & 255);
|
|
bytes2.push(240 | record.minSpatialSegmentationIdc >> 8 & 15);
|
|
bytes2.push(record.minSpatialSegmentationIdc & 255);
|
|
bytes2.push(252 | record.parallelismType & 3);
|
|
bytes2.push(252 | record.chromaFormatIdc & 3);
|
|
bytes2.push(248 | record.bitDepthLumaMinus8 & 7);
|
|
bytes2.push(248 | record.bitDepthChromaMinus8 & 7);
|
|
bytes2.push(record.avgFrameRate >> 8 & 255);
|
|
bytes2.push(record.avgFrameRate & 255);
|
|
bytes2.push((record.constantFrameRate & 3) << 6 | (record.numTemporalLayers & 7) << 3 | (record.temporalIdNested & 1) << 2 | record.lengthSizeMinusOne & 3);
|
|
bytes2.push(record.arrays.length & 255);
|
|
for (const arr of record.arrays) {
|
|
bytes2.push((arr.arrayCompleteness & 1) << 7 | 0 << 6 | arr.nalUnitType & 63);
|
|
bytes2.push(arr.nalUnits.length >> 8 & 255);
|
|
bytes2.push(arr.nalUnits.length & 255);
|
|
for (const nal of arr.nalUnits) {
|
|
bytes2.push(nal.length >> 8 & 255);
|
|
bytes2.push(nal.length & 255);
|
|
for (let i2 = 0; i2 < nal.length; i2++) {
|
|
bytes2.push(nal[i2]);
|
|
}
|
|
}
|
|
}
|
|
return new Uint8Array(bytes2);
|
|
};
|
|
var HevcNaluOrderState;
|
|
(function(HevcNaluOrderState2) {
|
|
HevcNaluOrderState2[HevcNaluOrderState2["audAllowed"] = 0] = "audAllowed";
|
|
HevcNaluOrderState2[HevcNaluOrderState2["beforeFirstVcl"] = 1] = "beforeFirstVcl";
|
|
HevcNaluOrderState2[HevcNaluOrderState2["afterFirstVcl"] = 2] = "afterFirstVcl";
|
|
HevcNaluOrderState2[HevcNaluOrderState2["eoBitstreamAllowed"] = 3] = "eoBitstreamAllowed";
|
|
HevcNaluOrderState2[HevcNaluOrderState2["noMoreDataAllowed"] = 4] = "noMoreDataAllowed";
|
|
})(HevcNaluOrderState || (HevcNaluOrderState = {}));
|
|
var iterateAv1PacketObus = function* (packet) {
|
|
const bitstream = new Bitstream(packet);
|
|
const readLeb128 = () => {
|
|
let value = 0;
|
|
for (let i2 = 0; i2 < 8; i2++) {
|
|
const byte = bitstream.readAlignedByte();
|
|
value |= (byte & 127) << i2 * 7;
|
|
if (!(byte & 128)) {
|
|
break;
|
|
}
|
|
if (i2 === 7 && byte & 128) {
|
|
return null;
|
|
}
|
|
}
|
|
if (value >= 2 ** 32 - 1) {
|
|
return null;
|
|
}
|
|
return value;
|
|
};
|
|
while (bitstream.getBitsLeft() >= 8) {
|
|
bitstream.skipBits(1);
|
|
const obuType = bitstream.readBits(4);
|
|
const obuExtension = bitstream.readBits(1);
|
|
const obuHasSizeField = bitstream.readBits(1);
|
|
bitstream.skipBits(1);
|
|
if (obuExtension) {
|
|
bitstream.skipBits(8);
|
|
}
|
|
let obuSize;
|
|
if (obuHasSizeField) {
|
|
const obuSizeValue = readLeb128();
|
|
if (obuSizeValue === null)
|
|
return;
|
|
obuSize = obuSizeValue;
|
|
} else {
|
|
obuSize = Math.floor(bitstream.getBitsLeft() / 8);
|
|
}
|
|
assert(bitstream.pos % 8 === 0);
|
|
yield {
|
|
type: obuType,
|
|
data: packet.subarray(bitstream.pos / 8, bitstream.pos / 8 + obuSize)
|
|
};
|
|
bitstream.skipBits(obuSize * 8);
|
|
}
|
|
};
|
|
var parseOpusIdentificationHeader = (bytes2) => {
|
|
const view2 = toDataView(bytes2);
|
|
const outputChannelCount = view2.getUint8(9);
|
|
const preSkip = view2.getUint16(10, true);
|
|
const inputSampleRate = view2.getUint32(12, true);
|
|
const outputGain = view2.getInt16(16, true);
|
|
const channelMappingFamily = view2.getUint8(18);
|
|
let channelMappingTable = null;
|
|
if (channelMappingFamily) {
|
|
channelMappingTable = bytes2.subarray(19, 19 + 2 + outputChannelCount);
|
|
}
|
|
return {
|
|
outputChannelCount,
|
|
preSkip,
|
|
inputSampleRate,
|
|
outputGain,
|
|
channelMappingFamily,
|
|
channelMappingTable
|
|
};
|
|
};
|
|
var determineVideoPacketType = (codec, decoderConfig, packetData) => {
|
|
switch (codec) {
|
|
case "avc":
|
|
{
|
|
for (const loc of iterateAvcNalUnits(packetData, decoderConfig)) {
|
|
const nalTypeByte = packetData[loc.offset];
|
|
const type = extractNalUnitTypeForAvc(nalTypeByte);
|
|
if (type >= AvcNalUnitType.NON_IDR_SLICE && type <= AvcNalUnitType.SLICE_DPC) {
|
|
return "delta";
|
|
}
|
|
if (type === AvcNalUnitType.IDR) {
|
|
return "key";
|
|
}
|
|
if (type === AvcNalUnitType.SEI && (!isChromium() || getChromiumVersion() >= 144)) {
|
|
const nalUnit = packetData.subarray(loc.offset, loc.offset + loc.length);
|
|
const bytes2 = removeEmulationPreventionBytes(nalUnit);
|
|
let pos = 1;
|
|
do {
|
|
let payloadType = 0;
|
|
while (true) {
|
|
const nextByte = bytes2[pos++];
|
|
if (nextByte === void 0)
|
|
break;
|
|
payloadType += nextByte;
|
|
if (nextByte < 255) {
|
|
break;
|
|
}
|
|
}
|
|
let payloadSize = 0;
|
|
while (true) {
|
|
const nextByte = bytes2[pos++];
|
|
if (nextByte === void 0)
|
|
break;
|
|
payloadSize += nextByte;
|
|
if (nextByte < 255) {
|
|
break;
|
|
}
|
|
}
|
|
const PAYLOAD_TYPE_RECOVERY_POINT = 6;
|
|
if (payloadType === PAYLOAD_TYPE_RECOVERY_POINT) {
|
|
const bitstream = new Bitstream(bytes2);
|
|
bitstream.pos = 8 * pos;
|
|
const recoveryFrameCount = readExpGolomb(bitstream);
|
|
const exactMatchFlag = bitstream.readBits(1);
|
|
if (recoveryFrameCount === 0 && exactMatchFlag === 1) {
|
|
return "key";
|
|
}
|
|
}
|
|
pos += payloadSize;
|
|
} while (pos < bytes2.length - 1);
|
|
}
|
|
}
|
|
return "delta";
|
|
}
|
|
;
|
|
case "hevc":
|
|
{
|
|
for (const loc of iterateHevcNalUnits(packetData, decoderConfig)) {
|
|
const type = extractNalUnitTypeForHevc(packetData[loc.offset]);
|
|
if (type < HevcNalUnitType.BLA_W_LP) {
|
|
return "delta";
|
|
}
|
|
if (type <= HevcNalUnitType.RSV_IRAP_VCL23) {
|
|
return "key";
|
|
}
|
|
}
|
|
return "delta";
|
|
}
|
|
;
|
|
case "vp8":
|
|
{
|
|
const frameType = packetData[0] & 1;
|
|
return frameType === 0 ? "key" : "delta";
|
|
}
|
|
;
|
|
case "vp9":
|
|
{
|
|
const bitstream = new Bitstream(packetData);
|
|
if (bitstream.readBits(2) !== 2) {
|
|
return null;
|
|
}
|
|
;
|
|
const profileLowBit = bitstream.readBits(1);
|
|
const profileHighBit = bitstream.readBits(1);
|
|
const profile = (profileHighBit << 1) + profileLowBit;
|
|
if (profile === 3) {
|
|
bitstream.skipBits(1);
|
|
}
|
|
const showExistingFrame = bitstream.readBits(1);
|
|
if (showExistingFrame) {
|
|
return null;
|
|
}
|
|
const frameType = bitstream.readBits(1);
|
|
return frameType === 0 ? "key" : "delta";
|
|
}
|
|
;
|
|
case "av1":
|
|
{
|
|
let reducedStillPictureHeader = false;
|
|
for (const { type, data } of iterateAv1PacketObus(packetData)) {
|
|
if (type === 1) {
|
|
const bitstream = new Bitstream(data);
|
|
bitstream.skipBits(4);
|
|
reducedStillPictureHeader = !!bitstream.readBits(1);
|
|
} else if (type === 3 || type === 6 || type === 7) {
|
|
if (reducedStillPictureHeader) {
|
|
return "key";
|
|
}
|
|
const bitstream = new Bitstream(data);
|
|
const showExistingFrame = bitstream.readBits(1);
|
|
if (showExistingFrame) {
|
|
return null;
|
|
}
|
|
const frameType = bitstream.readBits(2);
|
|
return frameType === 0 ? "key" : "delta";
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
;
|
|
default:
|
|
{
|
|
assertNever(codec);
|
|
assert(false);
|
|
}
|
|
;
|
|
}
|
|
};
|
|
var FlacBlockType;
|
|
(function(FlacBlockType2) {
|
|
FlacBlockType2[FlacBlockType2["STREAMINFO"] = 0] = "STREAMINFO";
|
|
FlacBlockType2[FlacBlockType2["VORBIS_COMMENT"] = 4] = "VORBIS_COMMENT";
|
|
FlacBlockType2[FlacBlockType2["PICTURE"] = 6] = "PICTURE";
|
|
})(FlacBlockType || (FlacBlockType = {}));
|
|
var parseAc3SyncFrame = (data) => {
|
|
if (data.length < 7) {
|
|
return null;
|
|
}
|
|
if (data[0] !== 11 || data[1] !== 119) {
|
|
return null;
|
|
}
|
|
const bitstream = new Bitstream(data);
|
|
bitstream.skipBits(16);
|
|
bitstream.skipBits(16);
|
|
const fscod = bitstream.readBits(2);
|
|
if (fscod === 3) {
|
|
return null;
|
|
}
|
|
const frmsizecod = bitstream.readBits(6);
|
|
const bsid = bitstream.readBits(5);
|
|
if (bsid > 8) {
|
|
return null;
|
|
}
|
|
const bsmod = bitstream.readBits(3);
|
|
const acmod = bitstream.readBits(3);
|
|
if ((acmod & 1) !== 0 && acmod !== 1) {
|
|
bitstream.skipBits(2);
|
|
}
|
|
if ((acmod & 4) !== 0) {
|
|
bitstream.skipBits(2);
|
|
}
|
|
if (acmod === 2) {
|
|
bitstream.skipBits(2);
|
|
}
|
|
const lfeon = bitstream.readBits(1);
|
|
const bitRateCode = Math.floor(frmsizecod / 2);
|
|
return { fscod, bsid, bsmod, acmod, lfeon, bitRateCode };
|
|
};
|
|
var AC3_FRAME_SIZES = [
|
|
// frmsizecod, [48kHz, 44.1kHz, 32kHz] in bytes
|
|
64 * 2,
|
|
69 * 2,
|
|
96 * 2,
|
|
64 * 2,
|
|
70 * 2,
|
|
96 * 2,
|
|
80 * 2,
|
|
87 * 2,
|
|
120 * 2,
|
|
80 * 2,
|
|
88 * 2,
|
|
120 * 2,
|
|
96 * 2,
|
|
104 * 2,
|
|
144 * 2,
|
|
96 * 2,
|
|
105 * 2,
|
|
144 * 2,
|
|
112 * 2,
|
|
121 * 2,
|
|
168 * 2,
|
|
112 * 2,
|
|
122 * 2,
|
|
168 * 2,
|
|
128 * 2,
|
|
139 * 2,
|
|
192 * 2,
|
|
128 * 2,
|
|
140 * 2,
|
|
192 * 2,
|
|
160 * 2,
|
|
174 * 2,
|
|
240 * 2,
|
|
160 * 2,
|
|
175 * 2,
|
|
240 * 2,
|
|
192 * 2,
|
|
208 * 2,
|
|
288 * 2,
|
|
192 * 2,
|
|
209 * 2,
|
|
288 * 2,
|
|
224 * 2,
|
|
243 * 2,
|
|
336 * 2,
|
|
224 * 2,
|
|
244 * 2,
|
|
336 * 2,
|
|
256 * 2,
|
|
278 * 2,
|
|
384 * 2,
|
|
256 * 2,
|
|
279 * 2,
|
|
384 * 2,
|
|
320 * 2,
|
|
348 * 2,
|
|
480 * 2,
|
|
320 * 2,
|
|
349 * 2,
|
|
480 * 2,
|
|
384 * 2,
|
|
417 * 2,
|
|
576 * 2,
|
|
384 * 2,
|
|
418 * 2,
|
|
576 * 2,
|
|
448 * 2,
|
|
487 * 2,
|
|
672 * 2,
|
|
448 * 2,
|
|
488 * 2,
|
|
672 * 2,
|
|
512 * 2,
|
|
557 * 2,
|
|
768 * 2,
|
|
512 * 2,
|
|
558 * 2,
|
|
768 * 2,
|
|
640 * 2,
|
|
696 * 2,
|
|
960 * 2,
|
|
640 * 2,
|
|
697 * 2,
|
|
960 * 2,
|
|
768 * 2,
|
|
835 * 2,
|
|
1152 * 2,
|
|
768 * 2,
|
|
836 * 2,
|
|
1152 * 2,
|
|
896 * 2,
|
|
975 * 2,
|
|
1344 * 2,
|
|
896 * 2,
|
|
976 * 2,
|
|
1344 * 2,
|
|
1024 * 2,
|
|
1114 * 2,
|
|
1536 * 2,
|
|
1024 * 2,
|
|
1115 * 2,
|
|
1536 * 2,
|
|
1152 * 2,
|
|
1253 * 2,
|
|
1728 * 2,
|
|
1152 * 2,
|
|
1254 * 2,
|
|
1728 * 2,
|
|
1280 * 2,
|
|
1393 * 2,
|
|
1920 * 2,
|
|
1280 * 2,
|
|
1394 * 2,
|
|
1920 * 2
|
|
];
|
|
var AC3_REGISTRATION_DESCRIPTOR = new Uint8Array([5, 4, 65, 67, 45, 51]);
|
|
var EAC3_REGISTRATION_DESCRIPTOR = new Uint8Array([5, 4, 69, 65, 67, 51]);
|
|
var EAC3_NUMBLKS_TABLE = [1, 2, 3, 6];
|
|
var parseEac3SyncFrame = (data) => {
|
|
if (data.length < 6) {
|
|
return null;
|
|
}
|
|
if (data[0] !== 11 || data[1] !== 119) {
|
|
return null;
|
|
}
|
|
const bitstream = new Bitstream(data);
|
|
bitstream.skipBits(16);
|
|
const strmtyp = bitstream.readBits(2);
|
|
bitstream.skipBits(3);
|
|
if (strmtyp !== 0 && strmtyp !== 2) {
|
|
return null;
|
|
}
|
|
const frmsiz = bitstream.readBits(11);
|
|
const fscod = bitstream.readBits(2);
|
|
let fscod2 = 0;
|
|
let numblkscod;
|
|
if (fscod === 3) {
|
|
fscod2 = bitstream.readBits(2);
|
|
numblkscod = 3;
|
|
} else {
|
|
numblkscod = bitstream.readBits(2);
|
|
}
|
|
const acmod = bitstream.readBits(3);
|
|
const lfeon = bitstream.readBits(1);
|
|
const bsid = bitstream.readBits(5);
|
|
if (bsid < 11 || bsid > 16) {
|
|
return null;
|
|
}
|
|
const numblks = EAC3_NUMBLKS_TABLE[numblkscod];
|
|
let fs;
|
|
if (fscod < 3) {
|
|
fs = AC3_SAMPLE_RATES[fscod] / 1e3;
|
|
} else {
|
|
fs = EAC3_REDUCED_SAMPLE_RATES[fscod2] / 1e3;
|
|
}
|
|
const dataRate = Math.round((frmsiz + 1) * fs / (numblks * 16));
|
|
const bsmod = 0;
|
|
const numDepSub = 0;
|
|
const chanLoc = 0;
|
|
const substream = {
|
|
fscod,
|
|
fscod2,
|
|
bsid,
|
|
bsmod,
|
|
acmod,
|
|
lfeon,
|
|
numDepSub,
|
|
chanLoc
|
|
};
|
|
return {
|
|
dataRate,
|
|
substreams: [substream]
|
|
};
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/packet.js
|
|
var PLACEHOLDER_DATA = /* @__PURE__ */ new Uint8Array(0);
|
|
var EncodedPacket = class _EncodedPacket {
|
|
/** Creates a new {@link EncodedPacket} from raw bytes and timing information. */
|
|
constructor(data, type, timestamp, duration, sequenceNumber = -1, byteLength, sideData) {
|
|
this.data = data;
|
|
this.type = type;
|
|
this.timestamp = timestamp;
|
|
this.duration = duration;
|
|
this.sequenceNumber = sequenceNumber;
|
|
if (data === PLACEHOLDER_DATA && byteLength === void 0) {
|
|
throw new Error("Internal error: byteLength must be explicitly provided when constructing metadata-only packets.");
|
|
}
|
|
if (byteLength === void 0) {
|
|
byteLength = data.byteLength;
|
|
}
|
|
if (!(data instanceof Uint8Array)) {
|
|
throw new TypeError("data must be a Uint8Array.");
|
|
}
|
|
if (type !== "key" && type !== "delta") {
|
|
throw new TypeError('type must be either "key" or "delta".');
|
|
}
|
|
if (!Number.isFinite(timestamp)) {
|
|
throw new TypeError("timestamp must be a number.");
|
|
}
|
|
if (!Number.isFinite(duration) || duration < 0) {
|
|
throw new TypeError("duration must be a non-negative number.");
|
|
}
|
|
if (!Number.isFinite(sequenceNumber)) {
|
|
throw new TypeError("sequenceNumber must be a number.");
|
|
}
|
|
if (!Number.isInteger(byteLength) || byteLength < 0) {
|
|
throw new TypeError("byteLength must be a non-negative integer.");
|
|
}
|
|
if (sideData !== void 0 && (typeof sideData !== "object" || !sideData)) {
|
|
throw new TypeError("sideData, when provided, must be an object.");
|
|
}
|
|
if (sideData?.alpha !== void 0 && !(sideData.alpha instanceof Uint8Array)) {
|
|
throw new TypeError("sideData.alpha, when provided, must be a Uint8Array.");
|
|
}
|
|
if (sideData?.alphaByteLength !== void 0 && (!Number.isInteger(sideData.alphaByteLength) || sideData.alphaByteLength < 0)) {
|
|
throw new TypeError("sideData.alphaByteLength, when provided, must be a non-negative integer.");
|
|
}
|
|
this.byteLength = byteLength;
|
|
this.sideData = sideData ?? {};
|
|
if (this.sideData.alpha && this.sideData.alphaByteLength === void 0) {
|
|
this.sideData.alphaByteLength = this.sideData.alpha.byteLength;
|
|
}
|
|
}
|
|
/**
|
|
* If this packet is a metadata-only packet. Metadata-only packets don't contain their packet data. They are the
|
|
* result of retrieving packets with {@link PacketRetrievalOptions.metadataOnly} set to \`true\`.
|
|
*/
|
|
get isMetadataOnly() {
|
|
return this.data === PLACEHOLDER_DATA;
|
|
}
|
|
/** The timestamp of this packet in microseconds. */
|
|
get microsecondTimestamp() {
|
|
return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.timestamp);
|
|
}
|
|
/** The duration of this packet in microseconds. */
|
|
get microsecondDuration() {
|
|
return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.duration);
|
|
}
|
|
/** Converts this packet to an
|
|
* [\`EncodedVideoChunk\`](https://developer.mozilla.org/en-US/docs/Web/API/EncodedVideoChunk) for use with the
|
|
* WebCodecs API. */
|
|
toEncodedVideoChunk() {
|
|
if (this.isMetadataOnly) {
|
|
throw new TypeError("Metadata-only packets cannot be converted to a video chunk.");
|
|
}
|
|
if (typeof EncodedVideoChunk === "undefined") {
|
|
throw new Error("Your browser does not support EncodedVideoChunk.");
|
|
}
|
|
return new EncodedVideoChunk({
|
|
data: this.data,
|
|
type: this.type,
|
|
timestamp: this.microsecondTimestamp,
|
|
duration: this.microsecondDuration
|
|
});
|
|
}
|
|
/**
|
|
* Converts this packet to an
|
|
* [\`EncodedVideoChunk\`](https://developer.mozilla.org/en-US/docs/Web/API/EncodedVideoChunk) for use with the
|
|
* WebCodecs API, using the alpha side data instead of the color data. Throws if no alpha side data is defined.
|
|
*/
|
|
alphaToEncodedVideoChunk(type = this.type) {
|
|
if (!this.sideData.alpha) {
|
|
throw new TypeError("This packet does not contain alpha side data.");
|
|
}
|
|
if (this.isMetadataOnly) {
|
|
throw new TypeError("Metadata-only packets cannot be converted to a video chunk.");
|
|
}
|
|
if (typeof EncodedVideoChunk === "undefined") {
|
|
throw new Error("Your browser does not support EncodedVideoChunk.");
|
|
}
|
|
return new EncodedVideoChunk({
|
|
data: this.sideData.alpha,
|
|
type,
|
|
timestamp: this.microsecondTimestamp,
|
|
duration: this.microsecondDuration
|
|
});
|
|
}
|
|
/** Converts this packet to an
|
|
* [\`EncodedAudioChunk\`](https://developer.mozilla.org/en-US/docs/Web/API/EncodedAudioChunk) for use with the
|
|
* WebCodecs API. */
|
|
toEncodedAudioChunk() {
|
|
if (this.isMetadataOnly) {
|
|
throw new TypeError("Metadata-only packets cannot be converted to an audio chunk.");
|
|
}
|
|
if (typeof EncodedAudioChunk === "undefined") {
|
|
throw new Error("Your browser does not support EncodedAudioChunk.");
|
|
}
|
|
return new EncodedAudioChunk({
|
|
data: this.data,
|
|
type: this.type,
|
|
timestamp: this.microsecondTimestamp,
|
|
duration: this.microsecondDuration
|
|
});
|
|
}
|
|
/**
|
|
* Creates an {@link EncodedPacket} from an
|
|
* [\`EncodedVideoChunk\`](https://developer.mozilla.org/en-US/docs/Web/API/EncodedVideoChunk) or
|
|
* [\`EncodedAudioChunk\`](https://developer.mozilla.org/en-US/docs/Web/API/EncodedAudioChunk). This method is useful
|
|
* for converting chunks from the WebCodecs API to \`EncodedPacket\` instances.
|
|
*/
|
|
static fromEncodedChunk(chunk, sideData) {
|
|
if (!(chunk instanceof EncodedVideoChunk || chunk instanceof EncodedAudioChunk)) {
|
|
throw new TypeError("chunk must be an EncodedVideoChunk or EncodedAudioChunk.");
|
|
}
|
|
const data = new Uint8Array(chunk.byteLength);
|
|
chunk.copyTo(data);
|
|
return new _EncodedPacket(data, chunk.type, chunk.timestamp / 1e6, (chunk.duration ?? 0) / 1e6, void 0, void 0, sideData);
|
|
}
|
|
/** Clones this packet while optionally modifying the new packet's data. */
|
|
clone(options) {
|
|
if (options !== void 0 && (typeof options !== "object" || options === null)) {
|
|
throw new TypeError("options, when provided, must be an object.");
|
|
}
|
|
if (options?.data !== void 0 && !(options.data instanceof Uint8Array)) {
|
|
throw new TypeError("options.data, when provided, must be a Uint8Array.");
|
|
}
|
|
if (options?.type !== void 0 && options.type !== "key" && options.type !== "delta") {
|
|
throw new TypeError('options.type, when provided, must be either "key" or "delta".');
|
|
}
|
|
if (options?.timestamp !== void 0 && !Number.isFinite(options.timestamp)) {
|
|
throw new TypeError("options.timestamp, when provided, must be a number.");
|
|
}
|
|
if (options?.duration !== void 0 && !Number.isFinite(options.duration)) {
|
|
throw new TypeError("options.duration, when provided, must be a number.");
|
|
}
|
|
if (options?.sequenceNumber !== void 0 && !Number.isFinite(options.sequenceNumber)) {
|
|
throw new TypeError("options.sequenceNumber, when provided, must be a number.");
|
|
}
|
|
if (options?.sideData !== void 0 && (typeof options.sideData !== "object" || options.sideData === null)) {
|
|
throw new TypeError("options.sideData, when provided, must be an object.");
|
|
}
|
|
return new _EncodedPacket(options?.data ?? this.data, options?.type ?? this.type, options?.timestamp ?? this.timestamp, options?.duration ?? this.duration, options?.sequenceNumber ?? this.sequenceNumber, this.byteLength, options?.sideData ?? this.sideData);
|
|
}
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/isobmff/isobmff-misc.js
|
|
var buildIsobmffMimeType = (info) => {
|
|
const base = info.hasVideo ? "video/" : info.hasAudio ? "audio/" : "application/";
|
|
let string = base + (info.isQuickTime ? "quicktime" : "mp4");
|
|
if (info.codecStrings.length > 0) {
|
|
const uniqueCodecMimeTypes = [...new Set(info.codecStrings)];
|
|
string += \`; codecs="\${uniqueCodecMimeTypes.join(", ")}"\`;
|
|
}
|
|
return string;
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/isobmff/isobmff-reader.js
|
|
var MIN_BOX_HEADER_SIZE = 8;
|
|
var MAX_BOX_HEADER_SIZE = 16;
|
|
|
|
// node_modules/mediabunny/dist/modules/src/matroska/ebml.js
|
|
var EBMLFloat32 = class {
|
|
constructor(value) {
|
|
this.value = value;
|
|
}
|
|
};
|
|
var EBMLFloat64 = class {
|
|
constructor(value) {
|
|
this.value = value;
|
|
}
|
|
};
|
|
var EBMLSignedInt = class {
|
|
constructor(value) {
|
|
this.value = value;
|
|
}
|
|
};
|
|
var EBMLUnicodeString = class {
|
|
constructor(value) {
|
|
this.value = value;
|
|
}
|
|
};
|
|
var EBMLId;
|
|
(function(EBMLId2) {
|
|
EBMLId2[EBMLId2["EBML"] = 440786851] = "EBML";
|
|
EBMLId2[EBMLId2["EBMLVersion"] = 17030] = "EBMLVersion";
|
|
EBMLId2[EBMLId2["EBMLReadVersion"] = 17143] = "EBMLReadVersion";
|
|
EBMLId2[EBMLId2["EBMLMaxIDLength"] = 17138] = "EBMLMaxIDLength";
|
|
EBMLId2[EBMLId2["EBMLMaxSizeLength"] = 17139] = "EBMLMaxSizeLength";
|
|
EBMLId2[EBMLId2["DocType"] = 17026] = "DocType";
|
|
EBMLId2[EBMLId2["DocTypeVersion"] = 17031] = "DocTypeVersion";
|
|
EBMLId2[EBMLId2["DocTypeReadVersion"] = 17029] = "DocTypeReadVersion";
|
|
EBMLId2[EBMLId2["Void"] = 236] = "Void";
|
|
EBMLId2[EBMLId2["Segment"] = 408125543] = "Segment";
|
|
EBMLId2[EBMLId2["SeekHead"] = 290298740] = "SeekHead";
|
|
EBMLId2[EBMLId2["Seek"] = 19899] = "Seek";
|
|
EBMLId2[EBMLId2["SeekID"] = 21419] = "SeekID";
|
|
EBMLId2[EBMLId2["SeekPosition"] = 21420] = "SeekPosition";
|
|
EBMLId2[EBMLId2["Duration"] = 17545] = "Duration";
|
|
EBMLId2[EBMLId2["Info"] = 357149030] = "Info";
|
|
EBMLId2[EBMLId2["TimestampScale"] = 2807729] = "TimestampScale";
|
|
EBMLId2[EBMLId2["MuxingApp"] = 19840] = "MuxingApp";
|
|
EBMLId2[EBMLId2["WritingApp"] = 22337] = "WritingApp";
|
|
EBMLId2[EBMLId2["Tracks"] = 374648427] = "Tracks";
|
|
EBMLId2[EBMLId2["TrackEntry"] = 174] = "TrackEntry";
|
|
EBMLId2[EBMLId2["TrackNumber"] = 215] = "TrackNumber";
|
|
EBMLId2[EBMLId2["TrackUID"] = 29637] = "TrackUID";
|
|
EBMLId2[EBMLId2["TrackType"] = 131] = "TrackType";
|
|
EBMLId2[EBMLId2["FlagEnabled"] = 185] = "FlagEnabled";
|
|
EBMLId2[EBMLId2["FlagDefault"] = 136] = "FlagDefault";
|
|
EBMLId2[EBMLId2["FlagForced"] = 21930] = "FlagForced";
|
|
EBMLId2[EBMLId2["FlagOriginal"] = 21934] = "FlagOriginal";
|
|
EBMLId2[EBMLId2["FlagHearingImpaired"] = 21931] = "FlagHearingImpaired";
|
|
EBMLId2[EBMLId2["FlagVisualImpaired"] = 21932] = "FlagVisualImpaired";
|
|
EBMLId2[EBMLId2["FlagCommentary"] = 21935] = "FlagCommentary";
|
|
EBMLId2[EBMLId2["FlagLacing"] = 156] = "FlagLacing";
|
|
EBMLId2[EBMLId2["Name"] = 21358] = "Name";
|
|
EBMLId2[EBMLId2["Language"] = 2274716] = "Language";
|
|
EBMLId2[EBMLId2["LanguageBCP47"] = 2274717] = "LanguageBCP47";
|
|
EBMLId2[EBMLId2["CodecID"] = 134] = "CodecID";
|
|
EBMLId2[EBMLId2["CodecPrivate"] = 25506] = "CodecPrivate";
|
|
EBMLId2[EBMLId2["CodecDelay"] = 22186] = "CodecDelay";
|
|
EBMLId2[EBMLId2["SeekPreRoll"] = 22203] = "SeekPreRoll";
|
|
EBMLId2[EBMLId2["DefaultDuration"] = 2352003] = "DefaultDuration";
|
|
EBMLId2[EBMLId2["Video"] = 224] = "Video";
|
|
EBMLId2[EBMLId2["PixelWidth"] = 176] = "PixelWidth";
|
|
EBMLId2[EBMLId2["PixelHeight"] = 186] = "PixelHeight";
|
|
EBMLId2[EBMLId2["DisplayWidth"] = 21680] = "DisplayWidth";
|
|
EBMLId2[EBMLId2["DisplayHeight"] = 21690] = "DisplayHeight";
|
|
EBMLId2[EBMLId2["DisplayUnit"] = 21682] = "DisplayUnit";
|
|
EBMLId2[EBMLId2["AlphaMode"] = 21440] = "AlphaMode";
|
|
EBMLId2[EBMLId2["Audio"] = 225] = "Audio";
|
|
EBMLId2[EBMLId2["SamplingFrequency"] = 181] = "SamplingFrequency";
|
|
EBMLId2[EBMLId2["Channels"] = 159] = "Channels";
|
|
EBMLId2[EBMLId2["BitDepth"] = 25188] = "BitDepth";
|
|
EBMLId2[EBMLId2["SimpleBlock"] = 163] = "SimpleBlock";
|
|
EBMLId2[EBMLId2["BlockGroup"] = 160] = "BlockGroup";
|
|
EBMLId2[EBMLId2["Block"] = 161] = "Block";
|
|
EBMLId2[EBMLId2["BlockAdditions"] = 30113] = "BlockAdditions";
|
|
EBMLId2[EBMLId2["BlockMore"] = 166] = "BlockMore";
|
|
EBMLId2[EBMLId2["BlockAdditional"] = 165] = "BlockAdditional";
|
|
EBMLId2[EBMLId2["BlockAddID"] = 238] = "BlockAddID";
|
|
EBMLId2[EBMLId2["BlockDuration"] = 155] = "BlockDuration";
|
|
EBMLId2[EBMLId2["ReferenceBlock"] = 251] = "ReferenceBlock";
|
|
EBMLId2[EBMLId2["Cluster"] = 524531317] = "Cluster";
|
|
EBMLId2[EBMLId2["Timestamp"] = 231] = "Timestamp";
|
|
EBMLId2[EBMLId2["Cues"] = 475249515] = "Cues";
|
|
EBMLId2[EBMLId2["CuePoint"] = 187] = "CuePoint";
|
|
EBMLId2[EBMLId2["CueTime"] = 179] = "CueTime";
|
|
EBMLId2[EBMLId2["CueTrackPositions"] = 183] = "CueTrackPositions";
|
|
EBMLId2[EBMLId2["CueTrack"] = 247] = "CueTrack";
|
|
EBMLId2[EBMLId2["CueClusterPosition"] = 241] = "CueClusterPosition";
|
|
EBMLId2[EBMLId2["Colour"] = 21936] = "Colour";
|
|
EBMLId2[EBMLId2["MatrixCoefficients"] = 21937] = "MatrixCoefficients";
|
|
EBMLId2[EBMLId2["TransferCharacteristics"] = 21946] = "TransferCharacteristics";
|
|
EBMLId2[EBMLId2["Primaries"] = 21947] = "Primaries";
|
|
EBMLId2[EBMLId2["Range"] = 21945] = "Range";
|
|
EBMLId2[EBMLId2["Projection"] = 30320] = "Projection";
|
|
EBMLId2[EBMLId2["ProjectionType"] = 30321] = "ProjectionType";
|
|
EBMLId2[EBMLId2["ProjectionPoseRoll"] = 30325] = "ProjectionPoseRoll";
|
|
EBMLId2[EBMLId2["Attachments"] = 423732329] = "Attachments";
|
|
EBMLId2[EBMLId2["AttachedFile"] = 24999] = "AttachedFile";
|
|
EBMLId2[EBMLId2["FileDescription"] = 18046] = "FileDescription";
|
|
EBMLId2[EBMLId2["FileName"] = 18030] = "FileName";
|
|
EBMLId2[EBMLId2["FileMediaType"] = 18016] = "FileMediaType";
|
|
EBMLId2[EBMLId2["FileData"] = 18012] = "FileData";
|
|
EBMLId2[EBMLId2["FileUID"] = 18094] = "FileUID";
|
|
EBMLId2[EBMLId2["Chapters"] = 272869232] = "Chapters";
|
|
EBMLId2[EBMLId2["Tags"] = 307544935] = "Tags";
|
|
EBMLId2[EBMLId2["Tag"] = 29555] = "Tag";
|
|
EBMLId2[EBMLId2["Targets"] = 25536] = "Targets";
|
|
EBMLId2[EBMLId2["TargetTypeValue"] = 26826] = "TargetTypeValue";
|
|
EBMLId2[EBMLId2["TargetType"] = 25546] = "TargetType";
|
|
EBMLId2[EBMLId2["TagTrackUID"] = 25541] = "TagTrackUID";
|
|
EBMLId2[EBMLId2["TagEditionUID"] = 25545] = "TagEditionUID";
|
|
EBMLId2[EBMLId2["TagChapterUID"] = 25540] = "TagChapterUID";
|
|
EBMLId2[EBMLId2["TagAttachmentUID"] = 25542] = "TagAttachmentUID";
|
|
EBMLId2[EBMLId2["SimpleTag"] = 26568] = "SimpleTag";
|
|
EBMLId2[EBMLId2["TagName"] = 17827] = "TagName";
|
|
EBMLId2[EBMLId2["TagLanguage"] = 17530] = "TagLanguage";
|
|
EBMLId2[EBMLId2["TagString"] = 17543] = "TagString";
|
|
EBMLId2[EBMLId2["TagBinary"] = 17541] = "TagBinary";
|
|
EBMLId2[EBMLId2["ContentEncodings"] = 28032] = "ContentEncodings";
|
|
EBMLId2[EBMLId2["ContentEncoding"] = 25152] = "ContentEncoding";
|
|
EBMLId2[EBMLId2["ContentEncodingOrder"] = 20529] = "ContentEncodingOrder";
|
|
EBMLId2[EBMLId2["ContentEncodingScope"] = 20530] = "ContentEncodingScope";
|
|
EBMLId2[EBMLId2["ContentCompression"] = 20532] = "ContentCompression";
|
|
EBMLId2[EBMLId2["ContentCompAlgo"] = 16980] = "ContentCompAlgo";
|
|
EBMLId2[EBMLId2["ContentCompSettings"] = 16981] = "ContentCompSettings";
|
|
EBMLId2[EBMLId2["ContentEncryption"] = 20533] = "ContentEncryption";
|
|
})(EBMLId || (EBMLId = {}));
|
|
var LEVEL_0_EBML_IDS = [
|
|
EBMLId.EBML,
|
|
EBMLId.Segment
|
|
];
|
|
var LEVEL_1_EBML_IDS = [
|
|
EBMLId.SeekHead,
|
|
EBMLId.Info,
|
|
EBMLId.Cluster,
|
|
EBMLId.Tracks,
|
|
EBMLId.Cues,
|
|
EBMLId.Attachments,
|
|
EBMLId.Chapters,
|
|
EBMLId.Tags
|
|
];
|
|
var LEVEL_0_AND_1_EBML_IDS = [
|
|
...LEVEL_0_EBML_IDS,
|
|
...LEVEL_1_EBML_IDS
|
|
];
|
|
var measureUnsignedInt = (value) => {
|
|
if (value < 1 << 8) {
|
|
return 1;
|
|
} else if (value < 1 << 16) {
|
|
return 2;
|
|
} else if (value < 1 << 24) {
|
|
return 3;
|
|
} else if (value < 2 ** 32) {
|
|
return 4;
|
|
} else if (value < 2 ** 40) {
|
|
return 5;
|
|
} else {
|
|
return 6;
|
|
}
|
|
};
|
|
var measureUnsignedBigInt = (value) => {
|
|
if (value < 1n << 8n) {
|
|
return 1;
|
|
} else if (value < 1n << 16n) {
|
|
return 2;
|
|
} else if (value < 1n << 24n) {
|
|
return 3;
|
|
} else if (value < 1n << 32n) {
|
|
return 4;
|
|
} else if (value < 1n << 40n) {
|
|
return 5;
|
|
} else if (value < 1n << 48n) {
|
|
return 6;
|
|
} else if (value < 1n << 56n) {
|
|
return 7;
|
|
} else {
|
|
return 8;
|
|
}
|
|
};
|
|
var measureSignedInt = (value) => {
|
|
if (value >= -(1 << 6) && value < 1 << 6) {
|
|
return 1;
|
|
} else if (value >= -(1 << 13) && value < 1 << 13) {
|
|
return 2;
|
|
} else if (value >= -(1 << 20) && value < 1 << 20) {
|
|
return 3;
|
|
} else if (value >= -(1 << 27) && value < 1 << 27) {
|
|
return 4;
|
|
} else if (value >= -(2 ** 34) && value < 2 ** 34) {
|
|
return 5;
|
|
} else {
|
|
return 6;
|
|
}
|
|
};
|
|
var measureVarInt = (value) => {
|
|
if (value < (1 << 7) - 1) {
|
|
return 1;
|
|
} else if (value < (1 << 14) - 1) {
|
|
return 2;
|
|
} else if (value < (1 << 21) - 1) {
|
|
return 3;
|
|
} else if (value < (1 << 28) - 1) {
|
|
return 4;
|
|
} else if (value < 2 ** 35 - 1) {
|
|
return 5;
|
|
} else if (value < 2 ** 42 - 1) {
|
|
return 6;
|
|
} else {
|
|
throw new Error("EBML varint size not supported " + value);
|
|
}
|
|
};
|
|
var EBMLWriter = class {
|
|
constructor(writer) {
|
|
this.writer = writer;
|
|
this.helper = new Uint8Array(8);
|
|
this.helperView = new DataView(this.helper.buffer);
|
|
this.offsets = /* @__PURE__ */ new WeakMap();
|
|
this.dataOffsets = /* @__PURE__ */ new WeakMap();
|
|
}
|
|
writeByte(value) {
|
|
this.helperView.setUint8(0, value);
|
|
this.writer.write(this.helper.subarray(0, 1));
|
|
}
|
|
writeFloat32(value) {
|
|
this.helperView.setFloat32(0, value, false);
|
|
this.writer.write(this.helper.subarray(0, 4));
|
|
}
|
|
writeFloat64(value) {
|
|
this.helperView.setFloat64(0, value, false);
|
|
this.writer.write(this.helper);
|
|
}
|
|
writeUnsignedInt(value, width = measureUnsignedInt(value)) {
|
|
let pos = 0;
|
|
switch (width) {
|
|
case 6:
|
|
this.helperView.setUint8(pos++, value / 2 ** 40 | 0);
|
|
// eslint-disable-next-line no-fallthrough
|
|
case 5:
|
|
this.helperView.setUint8(pos++, value / 2 ** 32 | 0);
|
|
// eslint-disable-next-line no-fallthrough
|
|
case 4:
|
|
this.helperView.setUint8(pos++, value >> 24);
|
|
// eslint-disable-next-line no-fallthrough
|
|
case 3:
|
|
this.helperView.setUint8(pos++, value >> 16);
|
|
// eslint-disable-next-line no-fallthrough
|
|
case 2:
|
|
this.helperView.setUint8(pos++, value >> 8);
|
|
// eslint-disable-next-line no-fallthrough
|
|
case 1:
|
|
this.helperView.setUint8(pos++, value);
|
|
break;
|
|
default:
|
|
throw new Error("Bad unsigned int size " + width);
|
|
}
|
|
this.writer.write(this.helper.subarray(0, pos));
|
|
}
|
|
writeUnsignedBigInt(value, width = measureUnsignedBigInt(value)) {
|
|
let pos = 0;
|
|
for (let i2 = width - 1; i2 >= 0; i2--) {
|
|
this.helperView.setUint8(pos++, Number(value >> BigInt(i2 * 8) & 0xffn));
|
|
}
|
|
this.writer.write(this.helper.subarray(0, pos));
|
|
}
|
|
writeSignedInt(value, width = measureSignedInt(value)) {
|
|
if (value < 0) {
|
|
value += 2 ** (width * 8);
|
|
}
|
|
this.writeUnsignedInt(value, width);
|
|
}
|
|
writeVarInt(value, width = measureVarInt(value)) {
|
|
let pos = 0;
|
|
switch (width) {
|
|
case 1:
|
|
this.helperView.setUint8(pos++, 1 << 7 | value);
|
|
break;
|
|
case 2:
|
|
this.helperView.setUint8(pos++, 1 << 6 | value >> 8);
|
|
this.helperView.setUint8(pos++, value);
|
|
break;
|
|
case 3:
|
|
this.helperView.setUint8(pos++, 1 << 5 | value >> 16);
|
|
this.helperView.setUint8(pos++, value >> 8);
|
|
this.helperView.setUint8(pos++, value);
|
|
break;
|
|
case 4:
|
|
this.helperView.setUint8(pos++, 1 << 4 | value >> 24);
|
|
this.helperView.setUint8(pos++, value >> 16);
|
|
this.helperView.setUint8(pos++, value >> 8);
|
|
this.helperView.setUint8(pos++, value);
|
|
break;
|
|
case 5:
|
|
this.helperView.setUint8(pos++, 1 << 3 | value / 2 ** 32 & 7);
|
|
this.helperView.setUint8(pos++, value >> 24);
|
|
this.helperView.setUint8(pos++, value >> 16);
|
|
this.helperView.setUint8(pos++, value >> 8);
|
|
this.helperView.setUint8(pos++, value);
|
|
break;
|
|
case 6:
|
|
this.helperView.setUint8(pos++, 1 << 2 | value / 2 ** 40 & 3);
|
|
this.helperView.setUint8(pos++, value / 2 ** 32 | 0);
|
|
this.helperView.setUint8(pos++, value >> 24);
|
|
this.helperView.setUint8(pos++, value >> 16);
|
|
this.helperView.setUint8(pos++, value >> 8);
|
|
this.helperView.setUint8(pos++, value);
|
|
break;
|
|
default:
|
|
throw new Error("Bad EBML varint size " + width);
|
|
}
|
|
this.writer.write(this.helper.subarray(0, pos));
|
|
}
|
|
writeAsciiString(str) {
|
|
this.writer.write(new Uint8Array(str.split("").map((x) => x.charCodeAt(0))));
|
|
}
|
|
writeEBML(data) {
|
|
if (data === null)
|
|
return;
|
|
if (data instanceof Uint8Array) {
|
|
this.writer.write(data);
|
|
} else if (Array.isArray(data)) {
|
|
for (const elem of data) {
|
|
this.writeEBML(elem);
|
|
}
|
|
} else {
|
|
this.offsets.set(data, this.writer.getPos());
|
|
this.writeUnsignedInt(data.id);
|
|
if (Array.isArray(data.data)) {
|
|
const sizePos = this.writer.getPos();
|
|
const sizeSize = data.size === -1 ? 1 : data.size ?? 4;
|
|
if (data.size === -1) {
|
|
this.writeByte(255);
|
|
} else {
|
|
this.writer.seek(this.writer.getPos() + sizeSize);
|
|
}
|
|
const startPos = this.writer.getPos();
|
|
this.dataOffsets.set(data, startPos);
|
|
this.writeEBML(data.data);
|
|
if (data.size !== -1) {
|
|
const size = this.writer.getPos() - startPos;
|
|
const endPos = this.writer.getPos();
|
|
this.writer.seek(sizePos);
|
|
this.writeVarInt(size, sizeSize);
|
|
this.writer.seek(endPos);
|
|
}
|
|
} else if (typeof data.data === "number") {
|
|
const size = data.size ?? measureUnsignedInt(data.data);
|
|
this.writeVarInt(size);
|
|
this.writeUnsignedInt(data.data, size);
|
|
} else if (typeof data.data === "bigint") {
|
|
const size = data.size ?? measureUnsignedBigInt(data.data);
|
|
this.writeVarInt(size);
|
|
this.writeUnsignedBigInt(data.data, size);
|
|
} else if (typeof data.data === "string") {
|
|
this.writeVarInt(data.data.length);
|
|
this.writeAsciiString(data.data);
|
|
} else if (data.data instanceof Uint8Array) {
|
|
this.writeVarInt(data.data.byteLength, data.size);
|
|
this.writer.write(data.data);
|
|
} else if (data.data instanceof EBMLFloat32) {
|
|
this.writeVarInt(4);
|
|
this.writeFloat32(data.data.value);
|
|
} else if (data.data instanceof EBMLFloat64) {
|
|
this.writeVarInt(8);
|
|
this.writeFloat64(data.data.value);
|
|
} else if (data.data instanceof EBMLSignedInt) {
|
|
const size = data.size ?? measureSignedInt(data.data.value);
|
|
this.writeVarInt(size);
|
|
this.writeSignedInt(data.data.value, size);
|
|
} else if (data.data instanceof EBMLUnicodeString) {
|
|
const bytes2 = textEncoder.encode(data.data.value);
|
|
this.writeVarInt(bytes2.length);
|
|
this.writer.write(bytes2);
|
|
} else {
|
|
assertNever(data.data);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
var MAX_VAR_INT_SIZE = 8;
|
|
var MAX_HEADER_SIZE = 2 * MAX_VAR_INT_SIZE;
|
|
var CODEC_STRING_MAP = {
|
|
"avc": "V_MPEG4/ISO/AVC",
|
|
"hevc": "V_MPEGH/ISO/HEVC",
|
|
"vp8": "V_VP8",
|
|
"vp9": "V_VP9",
|
|
"av1": "V_AV1",
|
|
"aac": "A_AAC",
|
|
"mp3": "A_MPEG/L3",
|
|
"opus": "A_OPUS",
|
|
"vorbis": "A_VORBIS",
|
|
"flac": "A_FLAC",
|
|
"ac3": "A_AC3",
|
|
"eac3": "A_EAC3",
|
|
"pcm-u8": "A_PCM/INT/LIT",
|
|
"pcm-s16": "A_PCM/INT/LIT",
|
|
"pcm-s16be": "A_PCM/INT/BIG",
|
|
"pcm-s24": "A_PCM/INT/LIT",
|
|
"pcm-s24be": "A_PCM/INT/BIG",
|
|
"pcm-s32": "A_PCM/INT/LIT",
|
|
"pcm-s32be": "A_PCM/INT/BIG",
|
|
"pcm-f32": "A_PCM/FLOAT/IEEE",
|
|
"pcm-f64": "A_PCM/FLOAT/IEEE",
|
|
"webvtt": "S_TEXT/WEBVTT"
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/matroska/matroska-misc.js
|
|
var buildMatroskaMimeType = (info) => {
|
|
const base = info.hasVideo ? "video/" : info.hasAudio ? "audio/" : "application/";
|
|
let string = base + (info.isWebM ? "webm" : "x-matroska");
|
|
if (info.codecStrings.length > 0) {
|
|
const uniqueCodecMimeTypes = [...new Set(info.codecStrings.filter(Boolean))];
|
|
string += \`; codecs="\${uniqueCodecMimeTypes.join(", ")}"\`;
|
|
}
|
|
return string;
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/adts/adts-reader.js
|
|
var MIN_ADTS_FRAME_HEADER_SIZE = 7;
|
|
var MAX_ADTS_FRAME_HEADER_SIZE = 9;
|
|
var readAdtsFrameHeader = (slice) => {
|
|
const startPos = slice.filePos;
|
|
const bytes2 = readBytes(slice, 9);
|
|
const bitstream = new Bitstream(bytes2);
|
|
const syncword = bitstream.readBits(12);
|
|
if (syncword !== 4095) {
|
|
return null;
|
|
}
|
|
bitstream.skipBits(1);
|
|
const layer = bitstream.readBits(2);
|
|
if (layer !== 0) {
|
|
return null;
|
|
}
|
|
const protectionAbsence = bitstream.readBits(1);
|
|
const objectType = bitstream.readBits(2) + 1;
|
|
const samplingFrequencyIndex = bitstream.readBits(4);
|
|
if (samplingFrequencyIndex === 15) {
|
|
return null;
|
|
}
|
|
bitstream.skipBits(1);
|
|
const channelConfiguration = bitstream.readBits(3);
|
|
if (channelConfiguration === 0) {
|
|
throw new Error("ADTS frames with channel configuration 0 are not supported.");
|
|
}
|
|
bitstream.skipBits(1);
|
|
bitstream.skipBits(1);
|
|
bitstream.skipBits(1);
|
|
bitstream.skipBits(1);
|
|
const frameLength = bitstream.readBits(13);
|
|
bitstream.skipBits(11);
|
|
const numberOfAacFrames = bitstream.readBits(2) + 1;
|
|
if (numberOfAacFrames !== 1) {
|
|
throw new Error("ADTS frames with more than one AAC frame are not supported.");
|
|
}
|
|
let crcCheck = null;
|
|
if (protectionAbsence === 1) {
|
|
slice.filePos -= 2;
|
|
} else {
|
|
crcCheck = bitstream.readBits(16);
|
|
}
|
|
return {
|
|
objectType,
|
|
samplingFrequencyIndex,
|
|
channelConfiguration,
|
|
frameLength,
|
|
numberOfAacFrames,
|
|
crcCheck,
|
|
startPos
|
|
};
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/sample.js
|
|
var __addDisposableResource = function(env, value, async) {
|
|
if (value !== null && value !== void 0) {
|
|
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
var dispose, inner;
|
|
if (async) {
|
|
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
dispose = value[Symbol.asyncDispose];
|
|
}
|
|
if (dispose === void 0) {
|
|
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
dispose = value[Symbol.dispose];
|
|
if (async) inner = dispose;
|
|
}
|
|
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
if (inner) dispose = function() {
|
|
try {
|
|
inner.call(this);
|
|
} catch (e2) {
|
|
return Promise.reject(e2);
|
|
}
|
|
};
|
|
env.stack.push({ value, dispose, async });
|
|
} else if (async) {
|
|
env.stack.push({ async: true });
|
|
}
|
|
return value;
|
|
};
|
|
var __disposeResources = /* @__PURE__ */ (function(SuppressedError2) {
|
|
return function(env) {
|
|
function fail(e2) {
|
|
env.error = env.hasError ? new SuppressedError2(e2, env.error, "An error was suppressed during disposal.") : e2;
|
|
env.hasError = true;
|
|
}
|
|
var r2, s2 = 0;
|
|
function next() {
|
|
while (r2 = env.stack.pop()) {
|
|
try {
|
|
if (!r2.async && s2 === 1) return s2 = 0, env.stack.push(r2), Promise.resolve().then(next);
|
|
if (r2.dispose) {
|
|
var result = r2.dispose.call(r2.value);
|
|
if (r2.async) return s2 |= 2, Promise.resolve(result).then(next, function(e2) {
|
|
fail(e2);
|
|
return next();
|
|
});
|
|
} else s2 |= 1;
|
|
} catch (e2) {
|
|
fail(e2);
|
|
}
|
|
}
|
|
if (s2 === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
if (env.hasError) throw env.error;
|
|
}
|
|
return next();
|
|
};
|
|
})(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
|
|
var e2 = new Error(message);
|
|
return e2.name = "SuppressedError", e2.error = error, e2.suppressed = suppressed, e2;
|
|
});
|
|
polyfillSymbolDispose();
|
|
var lastVideoGcErrorLog = -Infinity;
|
|
var lastAudioGcErrorLog = -Infinity;
|
|
var finalizationRegistry = null;
|
|
if (typeof FinalizationRegistry !== "undefined") {
|
|
finalizationRegistry = new FinalizationRegistry((value) => {
|
|
const now = performance.now();
|
|
if (value.type === "video") {
|
|
if (now - lastVideoGcErrorLog >= 1e3) {
|
|
Logging._error(\`A VideoSample was garbage collected without first being closed. For proper resource management, make sure to call close() on all your VideoSamples as soon as you're done using them.\`);
|
|
lastVideoGcErrorLog = now;
|
|
}
|
|
if (typeof VideoFrame !== "undefined" && value.data instanceof VideoFrame) {
|
|
value.data.close();
|
|
}
|
|
} else {
|
|
if (now - lastAudioGcErrorLog >= 1e3) {
|
|
Logging._error(\`An AudioSample was garbage collected without first being closed. For proper resource management, make sure to call close() on all your AudioSamples as soon as you're done using them.\`);
|
|
lastAudioGcErrorLog = now;
|
|
}
|
|
if (typeof AudioData !== "undefined" && value.data instanceof AudioData) {
|
|
value.data.close();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
var VideoSampleResource = class {
|
|
constructor() {
|
|
this._referenceCount = 0;
|
|
this._lastAllocationBuffer = null;
|
|
}
|
|
};
|
|
var VIDEO_SAMPLE_PIXEL_FORMATS = [
|
|
// 4:2:0 Y, U, V
|
|
"I420",
|
|
"I420P10",
|
|
"I420P12",
|
|
// 4:2:0 Y, U, V, A
|
|
"I420A",
|
|
"I420AP10",
|
|
"I420AP12",
|
|
// 4:2:2 Y, U, V
|
|
"I422",
|
|
"I422P10",
|
|
"I422P12",
|
|
// 4:2:2 Y, U, V, A
|
|
"I422A",
|
|
"I422AP10",
|
|
"I422AP12",
|
|
// 4:4:4 Y, U, V
|
|
"I444",
|
|
"I444P10",
|
|
"I444P12",
|
|
// 4:4:4 Y, U, V, A
|
|
"I444A",
|
|
"I444AP10",
|
|
"I444AP12",
|
|
// 4:2:0 Y, UV
|
|
"NV12",
|
|
// 4:4:4 RGBA
|
|
"RGBA",
|
|
// 4:4:4 RGBX (opaque)
|
|
"RGBX",
|
|
// 4:4:4 BGRA
|
|
"BGRA",
|
|
// 4:4:4 BGRX (opaque)
|
|
"BGRX"
|
|
];
|
|
var VIDEO_SAMPLE_PIXEL_FORMATS_SET = new Set(VIDEO_SAMPLE_PIXEL_FORMATS);
|
|
var VideoSample = class _VideoSample {
|
|
/** The width of the frame in pixels. */
|
|
get codedWidth() {
|
|
return this.visibleRect.width;
|
|
}
|
|
/** The height of the frame in pixels. */
|
|
get codedHeight() {
|
|
return this.visibleRect.height;
|
|
}
|
|
/** The display width of the frame in pixels, after aspect ratio adjustment and rotation. */
|
|
get displayWidth() {
|
|
return this.rotation % 180 === 0 ? this.squarePixelWidth : this.squarePixelHeight;
|
|
}
|
|
/** The display height of the frame in pixels, after aspect ratio adjustment and rotation. */
|
|
get displayHeight() {
|
|
return this.rotation % 180 === 0 ? this.squarePixelHeight : this.squarePixelWidth;
|
|
}
|
|
/** The presentation timestamp of the frame in microseconds. */
|
|
get microsecondTimestamp() {
|
|
return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.timestamp);
|
|
}
|
|
/** The duration of the frame in microseconds. */
|
|
get microsecondDuration() {
|
|
return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.duration);
|
|
}
|
|
/**
|
|
* Whether this sample uses a pixel format that can hold transparency data. Note that this doesn't necessarily mean
|
|
* that the sample is transparent.
|
|
*/
|
|
get hasAlpha() {
|
|
return this.format && this.format.includes("A");
|
|
}
|
|
constructor(data, init) {
|
|
this._closed = false;
|
|
if (data instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && data instanceof SharedArrayBuffer || ArrayBuffer.isView(data)) {
|
|
if (!init || typeof init !== "object") {
|
|
throw new TypeError("init must be an object.");
|
|
}
|
|
if (init.format === void 0 || !VIDEO_SAMPLE_PIXEL_FORMATS_SET.has(init.format)) {
|
|
throw new TypeError("init.format must be one of: " + VIDEO_SAMPLE_PIXEL_FORMATS.join(", "));
|
|
}
|
|
if (!Number.isInteger(init.codedWidth) || init.codedWidth <= 0) {
|
|
throw new TypeError("init.codedWidth must be a positive integer.");
|
|
}
|
|
if (!Number.isInteger(init.codedHeight) || init.codedHeight <= 0) {
|
|
throw new TypeError("init.codedHeight must be a positive integer.");
|
|
}
|
|
if (init.rotation !== void 0 && ![0, 90, 180, 270].includes(init.rotation)) {
|
|
throw new TypeError("init.rotation, when provided, must be 0, 90, 180, or 270.");
|
|
}
|
|
if (!Number.isFinite(init.timestamp)) {
|
|
throw new TypeError("init.timestamp must be a number.");
|
|
}
|
|
if (init.duration !== void 0 && (!Number.isFinite(init.duration) || init.duration < 0)) {
|
|
throw new TypeError("init.duration, when provided, must be a non-negative number.");
|
|
}
|
|
if (init.layout !== void 0) {
|
|
if (!Array.isArray(init.layout)) {
|
|
throw new TypeError("init.layout, when provided, must be an array.");
|
|
}
|
|
for (const plane of init.layout) {
|
|
if (!plane || typeof plane !== "object" || Array.isArray(plane)) {
|
|
throw new TypeError("Each entry in init.layout must be an object.");
|
|
}
|
|
if (!Number.isInteger(plane.offset) || plane.offset < 0) {
|
|
throw new TypeError("plane.offset must be a non-negative integer.");
|
|
}
|
|
if (!Number.isInteger(plane.stride) || plane.stride < 0) {
|
|
throw new TypeError("plane.stride must be a non-negative integer.");
|
|
}
|
|
}
|
|
}
|
|
if (init.visibleRect !== void 0) {
|
|
validateRectangle(init.visibleRect, "init.visibleRect");
|
|
}
|
|
if (init.displayWidth !== void 0 && (!Number.isInteger(init.displayWidth) || init.displayWidth <= 0)) {
|
|
throw new TypeError("init.displayWidth, when provided, must be a positive integer.");
|
|
}
|
|
if (init.displayHeight !== void 0 && (!Number.isInteger(init.displayHeight) || init.displayHeight <= 0)) {
|
|
throw new TypeError("init.displayHeight, when provided, must be a positive integer.");
|
|
}
|
|
if (init.displayWidth !== void 0 !== (init.displayHeight !== void 0)) {
|
|
throw new TypeError("init.displayWidth and init.displayHeight must be either both provided or both omitted.");
|
|
}
|
|
this._data = init._doNotCopy ? toUint8Array(data) : toUint8Array(data).slice();
|
|
this._layout = init.layout ?? createDefaultPlaneLayout(init.format, init.codedWidth, init.codedHeight);
|
|
this.format = init.format;
|
|
this.rotation = init.rotation ?? 0;
|
|
this.timestamp = init.timestamp;
|
|
this.duration = init.duration ?? 0;
|
|
let colorSpaceInit = init.colorSpace ?? null;
|
|
if (colorSpaceInit === null) {
|
|
if (this.format === "RGBA" || this.format === "RGBX" || this.format === "BGRA" || this.format === "BGRX") {
|
|
colorSpaceInit = {
|
|
primaries: "bt709",
|
|
transfer: "iec61966-2-1",
|
|
matrix: "rgb",
|
|
fullRange: true
|
|
};
|
|
} else {
|
|
colorSpaceInit = {
|
|
primaries: "bt709",
|
|
transfer: "bt709",
|
|
matrix: "bt709",
|
|
fullRange: false
|
|
};
|
|
}
|
|
}
|
|
this.colorSpace = new VideoSampleColorSpace(colorSpaceInit);
|
|
this.visibleRect = {
|
|
left: init.visibleRect?.left ?? 0,
|
|
top: init.visibleRect?.top ?? 0,
|
|
width: init.visibleRect?.width ?? init.codedWidth,
|
|
height: init.visibleRect?.height ?? init.codedHeight
|
|
};
|
|
if (init.displayWidth !== void 0) {
|
|
this.squarePixelWidth = this.rotation % 180 === 0 ? init.displayWidth : init.displayHeight;
|
|
this.squarePixelHeight = this.rotation % 180 === 0 ? init.displayHeight : init.displayWidth;
|
|
} else {
|
|
this.squarePixelWidth = this.visibleRect.width;
|
|
this.squarePixelHeight = this.visibleRect.height;
|
|
}
|
|
} else if (typeof VideoFrame !== "undefined" && data instanceof VideoFrame) {
|
|
if (init?.rotation !== void 0 && ![0, 90, 180, 270].includes(init.rotation)) {
|
|
throw new TypeError("init.rotation, when provided, must be 0, 90, 180, or 270.");
|
|
}
|
|
if (init?.timestamp !== void 0 && !Number.isFinite(init?.timestamp)) {
|
|
throw new TypeError("init.timestamp, when provided, must be a number.");
|
|
}
|
|
if (init?.duration !== void 0 && (!Number.isFinite(init.duration) || init.duration < 0)) {
|
|
throw new TypeError("init.duration, when provided, must be a non-negative number.");
|
|
}
|
|
if (init?.visibleRect !== void 0) {
|
|
validateRectangle(init.visibleRect, "init.visibleRect");
|
|
}
|
|
this._data = data;
|
|
this._layout = null;
|
|
this.format = data.format;
|
|
this.visibleRect = {
|
|
left: data.visibleRect?.x ?? 0,
|
|
top: data.visibleRect?.y ?? 0,
|
|
width: data.visibleRect?.width ?? data.codedWidth,
|
|
height: data.visibleRect?.height ?? data.codedHeight
|
|
};
|
|
this.rotation = init?.rotation ?? 0;
|
|
this.squarePixelWidth = data.displayWidth;
|
|
this.squarePixelHeight = data.displayHeight;
|
|
this.timestamp = init?.timestamp ?? data.timestamp / 1e6;
|
|
this.duration = init?.duration ?? (data.duration ?? 0) / 1e6;
|
|
this.colorSpace = new VideoSampleColorSpace(data.colorSpace);
|
|
} else if (typeof HTMLImageElement !== "undefined" && data instanceof HTMLImageElement || typeof SVGImageElement !== "undefined" && data instanceof SVGImageElement || typeof ImageBitmap !== "undefined" && data instanceof ImageBitmap || typeof HTMLVideoElement !== "undefined" && data instanceof HTMLVideoElement || typeof HTMLCanvasElement !== "undefined" && data instanceof HTMLCanvasElement || typeof OffscreenCanvas !== "undefined" && data instanceof OffscreenCanvas) {
|
|
if (!init || typeof init !== "object") {
|
|
throw new TypeError("init must be an object.");
|
|
}
|
|
if (init.rotation !== void 0 && ![0, 90, 180, 270].includes(init.rotation)) {
|
|
throw new TypeError("init.rotation, when provided, must be 0, 90, 180, or 270.");
|
|
}
|
|
if (!Number.isFinite(init.timestamp)) {
|
|
throw new TypeError("init.timestamp must be a number.");
|
|
}
|
|
if (init.duration !== void 0 && (!Number.isFinite(init.duration) || init.duration < 0)) {
|
|
throw new TypeError("init.duration, when provided, must be a non-negative number.");
|
|
}
|
|
if (typeof VideoFrame !== "undefined") {
|
|
return new _VideoSample(new VideoFrame(data, {
|
|
timestamp: Math.trunc(init.timestamp * SECOND_TO_MICROSECOND_FACTOR),
|
|
// Drag 0 to undefined
|
|
duration: Math.trunc((init.duration ?? 0) * SECOND_TO_MICROSECOND_FACTOR) || void 0
|
|
}), init);
|
|
}
|
|
let width = 0;
|
|
let height = 0;
|
|
if ("naturalWidth" in data) {
|
|
width = data.naturalWidth;
|
|
height = data.naturalHeight;
|
|
} else if ("videoWidth" in data) {
|
|
width = data.videoWidth;
|
|
height = data.videoHeight;
|
|
} else if ("width" in data) {
|
|
width = Number(data.width);
|
|
height = Number(data.height);
|
|
}
|
|
if (!width || !height) {
|
|
throw new TypeError("Could not determine dimensions.");
|
|
}
|
|
const canvas = new OffscreenCanvas(width, height);
|
|
const context = canvas.getContext("2d", {
|
|
alpha: isFirefox(),
|
|
// Firefox has VideoFrame glitches with opaque canvases
|
|
willReadFrequently: true
|
|
});
|
|
if (!context) {
|
|
throw new Error("OffscreenCanvas must have support for the '2d' context in order to create a VideoSample from this data.");
|
|
}
|
|
context.drawImage(data, 0, 0);
|
|
this._data = canvas;
|
|
this._layout = null;
|
|
this.format = "RGBX";
|
|
this.visibleRect = { left: 0, top: 0, width, height };
|
|
this.squarePixelWidth = width;
|
|
this.squarePixelHeight = height;
|
|
this.rotation = init.rotation ?? 0;
|
|
this.timestamp = init.timestamp;
|
|
this.duration = init.duration ?? 0;
|
|
this.colorSpace = new VideoSampleColorSpace({
|
|
matrix: "rgb",
|
|
primaries: "bt709",
|
|
transfer: "iec61966-2-1",
|
|
fullRange: true
|
|
});
|
|
} else if (data instanceof VideoSampleResource) {
|
|
if (!init || typeof init !== "object") {
|
|
throw new TypeError("init must be an object.");
|
|
}
|
|
if (init.rotation !== void 0 && ![0, 90, 180, 270].includes(init.rotation)) {
|
|
throw new TypeError("init.rotation, when provided, must be 0, 90, 180, or 270.");
|
|
}
|
|
if (!Number.isFinite(init.timestamp)) {
|
|
throw new TypeError("init.timestamp must be a number.");
|
|
}
|
|
if (init.duration !== void 0 && (!Number.isFinite(init.duration) || init.duration < 0)) {
|
|
throw new TypeError("init.duration, when provided, must be a non-negative number.");
|
|
}
|
|
this._data = data;
|
|
data._referenceCount++;
|
|
this.format = data.getFormat();
|
|
if (this.format !== null && !VIDEO_SAMPLE_PIXEL_FORMATS.includes(this.format)) {
|
|
throw new TypeError("getFormat() must return a VideoSamplePixelFormat or null.");
|
|
}
|
|
this.visibleRect = {
|
|
left: 0,
|
|
top: 0,
|
|
width: data.getCodedWidth(),
|
|
height: data.getCodedHeight()
|
|
};
|
|
if (!Number.isInteger(this.visibleRect.width) || this.visibleRect.width <= 0) {
|
|
throw new TypeError("getCodedWidth() must return a positive integer.");
|
|
}
|
|
if (!Number.isInteger(this.visibleRect.height) || this.visibleRect.height <= 0) {
|
|
throw new TypeError("getCodedHeight() must return a positive integer.");
|
|
}
|
|
this.squarePixelWidth = data.getSquarePixelWidth();
|
|
if (!Number.isInteger(this.squarePixelWidth) || this.squarePixelWidth <= 0) {
|
|
throw new TypeError("getSquarePixelWidth() must return a positive integer.");
|
|
}
|
|
this.squarePixelHeight = data.getSquarePixelHeight();
|
|
if (!Number.isInteger(this.squarePixelHeight) || this.squarePixelHeight <= 0) {
|
|
throw new TypeError("getSquarePixelHeight() must return a positive integer.");
|
|
}
|
|
this.rotation = init.rotation ?? 0;
|
|
this.timestamp = init.timestamp;
|
|
this.duration = init.duration ?? 0;
|
|
this.colorSpace = data.getColorSpace();
|
|
} else {
|
|
throw new TypeError("Invalid data type: Must be a BufferSource, CanvasImageSource, or VideoSampleResource.");
|
|
}
|
|
this.encodeOptions = init?.encodeOptions ?? {};
|
|
this.pixelAspectRatio = simplifyRational({
|
|
num: this.squarePixelWidth * this.codedHeight,
|
|
den: this.squarePixelHeight * this.codedWidth
|
|
});
|
|
finalizationRegistry?.register(this, { type: "video", data: this._data }, this);
|
|
}
|
|
/** Clones this video sample. */
|
|
clone() {
|
|
if (this._closed) {
|
|
throw new Error("VideoSample is closed.");
|
|
}
|
|
assert(this._data !== null);
|
|
if (this._data instanceof VideoSampleResource) {
|
|
return new _VideoSample(this._data, {
|
|
timestamp: this.timestamp,
|
|
duration: this.duration,
|
|
rotation: this.rotation,
|
|
encodeOptions: this.encodeOptions
|
|
});
|
|
} else if (isVideoFrame(this._data)) {
|
|
return new _VideoSample(this._data.clone(), {
|
|
timestamp: this.timestamp,
|
|
duration: this.duration,
|
|
rotation: this.rotation,
|
|
encodeOptions: this.encodeOptions
|
|
});
|
|
} else if (this._data instanceof Uint8Array) {
|
|
assert(this._layout);
|
|
return new _VideoSample(this._data, {
|
|
format: this.format,
|
|
layout: this._layout,
|
|
codedWidth: this.codedWidth,
|
|
codedHeight: this.codedHeight,
|
|
timestamp: this.timestamp,
|
|
duration: this.duration,
|
|
colorSpace: this.colorSpace,
|
|
rotation: this.rotation,
|
|
visibleRect: this.visibleRect,
|
|
displayWidth: this.displayWidth,
|
|
displayHeight: this.displayHeight,
|
|
encodeOptions: this.encodeOptions,
|
|
// It's already been copied, if we copy it again we make the clone unnecessarily expensive
|
|
_doNotCopy: true
|
|
});
|
|
} else {
|
|
return new _VideoSample(this._data, {
|
|
format: this.format,
|
|
codedWidth: this.codedWidth,
|
|
codedHeight: this.codedHeight,
|
|
timestamp: this.timestamp,
|
|
duration: this.duration,
|
|
colorSpace: this.colorSpace,
|
|
rotation: this.rotation,
|
|
visibleRect: this.visibleRect,
|
|
displayWidth: this.displayWidth,
|
|
displayHeight: this.displayHeight,
|
|
encodeOptions: this.encodeOptions
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Closes this video sample, releasing held resources. Video samples should be closed as soon as they are not
|
|
* needed anymore.
|
|
*/
|
|
close() {
|
|
if (this._closed) {
|
|
return;
|
|
}
|
|
finalizationRegistry?.unregister(this);
|
|
if (this._data instanceof VideoSampleResource) {
|
|
this._data._referenceCount--;
|
|
if (this._data._referenceCount === 0) {
|
|
this._data.close();
|
|
}
|
|
} else if (isVideoFrame(this._data)) {
|
|
this._data.close();
|
|
} else {
|
|
this._data = null;
|
|
}
|
|
this._closed = true;
|
|
}
|
|
/**
|
|
* Returns the number of bytes required to hold this video sample's pixel data.
|
|
*/
|
|
allocationSize(options = {}) {
|
|
validateVideoFrameCopyToOptions(options);
|
|
if (this._closed) {
|
|
throw new Error("VideoSample is closed.");
|
|
}
|
|
if ((options.format ?? this.format) == null) {
|
|
throw new Error("Cannot get allocation size when format is null.");
|
|
}
|
|
if (isVideoFrame(this._data)) {
|
|
return this._data.allocationSize(options);
|
|
}
|
|
const combinedLayout = ParseVideoFrameCopyToOptions(this, options);
|
|
return combinedLayout.allocationSize;
|
|
}
|
|
/**
|
|
* Copies this video sample's pixel data to an ArrayBuffer or ArrayBufferView.
|
|
* @returns The byte layout of the planes of the copied data.
|
|
*/
|
|
async copyTo(destination, options = {}) {
|
|
if (!isAllowSharedBufferSource(destination)) {
|
|
throw new TypeError("destination must be an ArrayBuffer or an ArrayBuffer view.");
|
|
}
|
|
validateVideoFrameCopyToOptions(options);
|
|
if (this._closed) {
|
|
throw new Error("VideoSample is closed.");
|
|
}
|
|
if ((options.format ?? this.format) == null) {
|
|
throw new Error("Cannot copy video sample data when format is null.");
|
|
}
|
|
assert(this._data !== null);
|
|
if (isVideoFrame(this._data)) {
|
|
return this._data.copyTo(destination, options);
|
|
}
|
|
if (options.format && !["RGBA", "RGBX", "BGRA", "BGRX"].includes(this.format) && ["RGBA", "RGBX", "BGRA", "BGRX"].includes(options.format)) {
|
|
if (this._data instanceof VideoSampleResource) {
|
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
try {
|
|
const rgbSample = __addDisposableResource(env_1, await this._data.toRgbSample({
|
|
timestamp: this.timestamp,
|
|
duration: this.duration,
|
|
rotation: this.rotation
|
|
}, options.colorSpace ?? "srgb"), false);
|
|
if (!(rgbSample instanceof _VideoSample)) {
|
|
throw new TypeError("toRgbSample() must return a VideoSample.");
|
|
}
|
|
if (!["RGBA", "RGBX", "BGRA", "BGRX"].includes(rgbSample.format)) {
|
|
throw new Error(\`Sample returned by toRgbSample was expected to have an RGB format, got '\${rgbSample.format}' instead.\`);
|
|
}
|
|
return await rgbSample.copyTo(destination, options);
|
|
} catch (e_1) {
|
|
env_1.error = e_1;
|
|
env_1.hasError = true;
|
|
} finally {
|
|
__disposeResources(env_1);
|
|
}
|
|
} else {
|
|
if (typeof VideoFrame === "undefined") {
|
|
throw new Error("For this sample, converting from a non-RGB to an RGB format requires VideoFrame to be defined.");
|
|
}
|
|
const tempFrame = this.toVideoFrame();
|
|
const result = await tempFrame.copyTo(destination, options);
|
|
tempFrame.close();
|
|
return result;
|
|
}
|
|
}
|
|
const combinedLayout = ParseVideoFrameCopyToOptions(this, options);
|
|
assert(this.format);
|
|
const destBytes = toUint8Array(destination);
|
|
if (destBytes.byteLength < combinedLayout.allocationSize) {
|
|
throw new TypeError(\`Destination buffer too small. Required: \${combinedLayout.allocationSize}, Available: \${destBytes.byteLength}\`);
|
|
}
|
|
const planeConfigs = getPlaneConfigs(this.format);
|
|
let dataPlanes;
|
|
if (this._data instanceof VideoSampleResource) {
|
|
let result = this._data.getDataPlanes();
|
|
if (result instanceof Promise)
|
|
result = await result;
|
|
if (!Array.isArray(result) || result.some((x) => !(x.data instanceof Uint8Array) || !Number.isInteger(x.stride) || x.stride < 0)) {
|
|
throw new TypeError('getDataPlanes() must return an array of objects with a Uint8Array "data" property and a non-negative integer "stride" property.');
|
|
}
|
|
dataPlanes = result;
|
|
} else if (this._data instanceof Uint8Array) {
|
|
assert(this._layout);
|
|
assert(this._layout.length === planeConfigs.length);
|
|
dataPlanes = this._layout.map((planeLayout, i2) => {
|
|
const height = Math.ceil(this.codedHeight / planeConfigs[i2].heightDivisor);
|
|
return {
|
|
data: this._data.subarray(planeLayout.offset, planeLayout.offset + planeLayout.stride * height),
|
|
stride: planeLayout.stride
|
|
};
|
|
});
|
|
} else {
|
|
const canvas = this._data;
|
|
const context = canvas.getContext("2d");
|
|
assert(context);
|
|
const imageData = context.getImageData(0, 0, this.codedWidth, this.codedHeight);
|
|
dataPlanes = [{
|
|
data: toUint8Array(imageData.data),
|
|
stride: 4 * this.codedWidth
|
|
}];
|
|
}
|
|
const planeLayouts = [];
|
|
const numPlanes = planeConfigs.length;
|
|
for (let planeIndex = 0; planeIndex < numPlanes; planeIndex++) {
|
|
const computedLayout = combinedLayout.computedLayouts[planeIndex];
|
|
const sourceStride = dataPlanes[planeIndex].stride;
|
|
const sourceData = dataPlanes[planeIndex].data;
|
|
let sourceOffset = computedLayout.sourceTop * sourceStride;
|
|
sourceOffset += computedLayout.sourceLeftBytes;
|
|
let destinationOffset = computedLayout.destinationOffset;
|
|
const rowBytes = computedLayout.sourceWidthBytes;
|
|
const layout = {
|
|
offset: destinationOffset,
|
|
stride: computedLayout.destinationStride
|
|
};
|
|
for (let row = 0; row < computedLayout.sourceHeight; row++) {
|
|
if (sourceOffset + rowBytes > sourceData.byteLength) {
|
|
throw new Error(\`Source buffer OOB read.\`);
|
|
}
|
|
if (destinationOffset + rowBytes > destBytes.byteLength) {
|
|
throw new Error(\`Destination buffer OOB write.\`);
|
|
}
|
|
const srcSub = sourceData.subarray(sourceOffset, sourceOffset + rowBytes);
|
|
destBytes.set(srcSub, destinationOffset);
|
|
sourceOffset += sourceStride;
|
|
destinationOffset += computedLayout.destinationStride;
|
|
}
|
|
planeLayouts.push(layout);
|
|
}
|
|
if (options.format !== void 0) {
|
|
const needsRgbConversion = this.format.startsWith("RGB") !== options.format.startsWith("RGB");
|
|
const needsAlphaConversion = this.format.includes("X") && options.format.includes("A");
|
|
if (needsRgbConversion || needsAlphaConversion) {
|
|
for (let i2 = 0; i2 < combinedLayout.allocationSize; i2 += 4) {
|
|
if (needsRgbConversion) {
|
|
const r2 = destBytes[i2];
|
|
const b = destBytes[i2 + 2];
|
|
destBytes[i2] = b;
|
|
destBytes[i2 + 2] = r2;
|
|
}
|
|
if (needsAlphaConversion) {
|
|
destBytes[i2 + 3] = 255;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return planeLayouts;
|
|
}
|
|
/**
|
|
* Converts this video sample to a VideoFrame for use with the WebCodecs API. The VideoFrame returned by this
|
|
* method *must* be closed separately from this video sample.
|
|
*/
|
|
toVideoFrame() {
|
|
if (this._closed) {
|
|
throw new Error("VideoSample is closed.");
|
|
}
|
|
assert(this._data !== null);
|
|
if (this._data instanceof VideoSampleResource) {
|
|
if (this.format === null) {
|
|
throw new Error("Cannot convert a VideoSampleResource-backed VideoSample to VideoFrame if format is null.");
|
|
}
|
|
const planes = this._data.getDataPlanes();
|
|
if (planes instanceof Promise) {
|
|
throw new Error("Cannot convert a VideoSampleResource-backed VideoSample to VideoFrame if getDataPlanes() returns a promise.");
|
|
}
|
|
const size = planes.reduce((a2, b) => a2 + b.data.byteLength, 0);
|
|
const buffer = new Uint8Array(size);
|
|
let offset = 0;
|
|
const offsets = [];
|
|
for (const plane of planes) {
|
|
buffer.set(plane.data, offset);
|
|
offsets.push(offset);
|
|
offset += plane.data.byteLength;
|
|
}
|
|
return new VideoFrame(buffer, {
|
|
format: this.format,
|
|
layout: planes.map((x, i2) => ({
|
|
offset: offsets[i2],
|
|
stride: x.stride
|
|
})),
|
|
codedWidth: this.codedWidth,
|
|
codedHeight: this.codedHeight,
|
|
timestamp: this.microsecondTimestamp,
|
|
duration: this.microsecondDuration,
|
|
colorSpace: this.colorSpace,
|
|
displayWidth: this.squarePixelWidth,
|
|
// Not display* since we're not passing rotation
|
|
displayHeight: this.squarePixelHeight
|
|
});
|
|
} else if (isVideoFrame(this._data)) {
|
|
return new VideoFrame(this._data, {
|
|
timestamp: this.microsecondTimestamp,
|
|
duration: this.microsecondDuration || void 0
|
|
// Drag 0 duration to undefined, glitches some codecs
|
|
});
|
|
} else if (this._data instanceof Uint8Array) {
|
|
return new VideoFrame(this._data, {
|
|
format: this.format,
|
|
codedWidth: this.codedWidth,
|
|
codedHeight: this.codedHeight,
|
|
timestamp: this.microsecondTimestamp,
|
|
duration: this.microsecondDuration || void 0,
|
|
colorSpace: this.colorSpace,
|
|
displayWidth: this.squarePixelWidth,
|
|
// Not display* since we're not passing rotation
|
|
displayHeight: this.squarePixelHeight
|
|
});
|
|
} else {
|
|
return new VideoFrame(this._data, {
|
|
timestamp: this.microsecondTimestamp,
|
|
duration: this.microsecondDuration || void 0
|
|
});
|
|
}
|
|
}
|
|
draw(context, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) {
|
|
let sx = 0;
|
|
let sy = 0;
|
|
let sWidth = this.displayWidth;
|
|
let sHeight = this.displayHeight;
|
|
let dx = 0;
|
|
let dy = 0;
|
|
let dWidth = this.displayWidth;
|
|
let dHeight = this.displayHeight;
|
|
if (arg5 !== void 0) {
|
|
sx = arg1;
|
|
sy = arg2;
|
|
sWidth = arg3;
|
|
sHeight = arg4;
|
|
dx = arg5;
|
|
dy = arg6;
|
|
if (arg7 !== void 0) {
|
|
dWidth = arg7;
|
|
dHeight = arg8;
|
|
} else {
|
|
dWidth = sWidth;
|
|
dHeight = sHeight;
|
|
}
|
|
} else {
|
|
dx = arg1;
|
|
dy = arg2;
|
|
if (arg3 !== void 0) {
|
|
dWidth = arg3;
|
|
dHeight = arg4;
|
|
}
|
|
}
|
|
if (!(typeof CanvasRenderingContext2D !== "undefined" && context instanceof CanvasRenderingContext2D || typeof OffscreenCanvasRenderingContext2D !== "undefined" && context instanceof OffscreenCanvasRenderingContext2D)) {
|
|
throw new TypeError("context must be a CanvasRenderingContext2D or OffscreenCanvasRenderingContext2D.");
|
|
}
|
|
if (!Number.isFinite(sx)) {
|
|
throw new TypeError("sx must be a number.");
|
|
}
|
|
if (!Number.isFinite(sy)) {
|
|
throw new TypeError("sy must be a number.");
|
|
}
|
|
if (!Number.isFinite(sWidth) || sWidth < 0) {
|
|
throw new TypeError("sWidth must be a non-negative number.");
|
|
}
|
|
if (!Number.isFinite(sHeight) || sHeight < 0) {
|
|
throw new TypeError("sHeight must be a non-negative number.");
|
|
}
|
|
if (!Number.isFinite(dx)) {
|
|
throw new TypeError("dx must be a number.");
|
|
}
|
|
if (!Number.isFinite(dy)) {
|
|
throw new TypeError("dy must be a number.");
|
|
}
|
|
if (!Number.isFinite(dWidth) || dWidth < 0) {
|
|
throw new TypeError("dWidth must be a non-negative number.");
|
|
}
|
|
if (!Number.isFinite(dHeight) || dHeight < 0) {
|
|
throw new TypeError("dHeight must be a non-negative number.");
|
|
}
|
|
if (this._closed) {
|
|
throw new Error("VideoSample is closed.");
|
|
}
|
|
({ sx, sy, sWidth, sHeight } = this._rotateSourceRegion(sx, sy, sWidth, sHeight, this.rotation));
|
|
const source = this.toCanvasImageSource();
|
|
context.save();
|
|
const centerX = dx + dWidth / 2;
|
|
const centerY = dy + dHeight / 2;
|
|
context.translate(centerX, centerY);
|
|
context.rotate(this.rotation * Math.PI / 180);
|
|
const aspectRatioChange = this.rotation % 180 === 0 ? 1 : dWidth / dHeight;
|
|
context.scale(1 / aspectRatioChange, aspectRatioChange);
|
|
context.drawImage(source, sx, sy, sWidth, sHeight, -dWidth / 2, -dHeight / 2, dWidth, dHeight);
|
|
context.restore();
|
|
}
|
|
/**
|
|
* Draws the sample in the middle of the canvas corresponding to the context with the specified fit behavior.
|
|
*/
|
|
drawWithFit(context, options) {
|
|
if (!(typeof CanvasRenderingContext2D !== "undefined" && context instanceof CanvasRenderingContext2D || typeof OffscreenCanvasRenderingContext2D !== "undefined" && context instanceof OffscreenCanvasRenderingContext2D)) {
|
|
throw new TypeError("context must be a CanvasRenderingContext2D or OffscreenCanvasRenderingContext2D.");
|
|
}
|
|
if (!options || typeof options !== "object") {
|
|
throw new TypeError("options must be an object.");
|
|
}
|
|
if (!["fill", "contain", "cover"].includes(options.fit)) {
|
|
throw new TypeError("options.fit must be 'fill', 'contain', or 'cover'.");
|
|
}
|
|
if (options.rotation !== void 0 && ![0, 90, 180, 270].includes(options.rotation)) {
|
|
throw new TypeError("options.rotation, when provided, must be 0, 90, 180, or 270.");
|
|
}
|
|
if (options.crop !== void 0) {
|
|
validateCropRectangle(options.crop, "options.");
|
|
}
|
|
const canvasWidth = context.canvas.width;
|
|
const canvasHeight = context.canvas.height;
|
|
const rotation = options.rotation ?? this.rotation;
|
|
const [rotatedWidth, rotatedHeight] = rotation % 180 === 0 ? [this.squarePixelWidth, this.squarePixelHeight] : [this.squarePixelHeight, this.squarePixelWidth];
|
|
let finalCrop = options.crop;
|
|
if (finalCrop) {
|
|
finalCrop = clampCropRectangle(finalCrop, rotatedWidth, rotatedHeight);
|
|
}
|
|
let dx;
|
|
let dy;
|
|
let newWidth;
|
|
let newHeight;
|
|
const { sx, sy, sWidth, sHeight } = this._rotateSourceRegion(options.crop?.left ?? 0, options.crop?.top ?? 0, options.crop?.width ?? rotatedWidth, options.crop?.height ?? rotatedHeight, rotation);
|
|
if (options.fit === "fill") {
|
|
dx = 0;
|
|
dy = 0;
|
|
newWidth = canvasWidth;
|
|
newHeight = canvasHeight;
|
|
} else {
|
|
const [sampleWidth, sampleHeight] = options.crop ? [options.crop.width, options.crop.height] : [rotatedWidth, rotatedHeight];
|
|
const scale = options.fit === "contain" ? Math.min(canvasWidth / sampleWidth, canvasHeight / sampleHeight) : Math.max(canvasWidth / sampleWidth, canvasHeight / sampleHeight);
|
|
newWidth = sampleWidth * scale;
|
|
newHeight = sampleHeight * scale;
|
|
dx = (canvasWidth - newWidth) / 2;
|
|
dy = (canvasHeight - newHeight) / 2;
|
|
}
|
|
context.save();
|
|
const aspectRatioChange = rotation % 180 === 0 ? 1 : newWidth / newHeight;
|
|
context.translate(canvasWidth / 2, canvasHeight / 2);
|
|
context.rotate(rotation * Math.PI / 180);
|
|
context.scale(1 / aspectRatioChange, aspectRatioChange);
|
|
context.translate(-canvasWidth / 2, -canvasHeight / 2);
|
|
context.drawImage(this.toCanvasImageSource(), sx, sy, sWidth, sHeight, dx, dy, newWidth, newHeight);
|
|
context.restore();
|
|
}
|
|
/** @internal */
|
|
_rotateSourceRegion(sx, sy, sWidth, sHeight, rotation) {
|
|
if (rotation === 90) {
|
|
[sx, sy, sWidth, sHeight] = [
|
|
sy,
|
|
this.squarePixelHeight - sx - sWidth,
|
|
sHeight,
|
|
sWidth
|
|
];
|
|
} else if (rotation === 180) {
|
|
[sx, sy] = [
|
|
this.squarePixelWidth - sx - sWidth,
|
|
this.squarePixelHeight - sy - sHeight
|
|
];
|
|
} else if (rotation === 270) {
|
|
[sx, sy, sWidth, sHeight] = [
|
|
this.squarePixelWidth - sy - sHeight,
|
|
sx,
|
|
sHeight,
|
|
sWidth
|
|
];
|
|
}
|
|
return { sx, sy, sWidth, sHeight };
|
|
}
|
|
/**
|
|
* Converts this video sample to a
|
|
* [\`CanvasImageSource\`](https://udn.realityripple.com/docs/Web/API/CanvasImageSource) for drawing to a canvas.
|
|
*
|
|
* You must use the value returned by this method immediately, as any VideoFrame created internally may
|
|
* automatically be closed in the next microtask.
|
|
*/
|
|
toCanvasImageSource() {
|
|
if (this._closed) {
|
|
throw new Error("VideoSample is closed.");
|
|
}
|
|
assert(this._data !== null);
|
|
if (this._data instanceof VideoSampleResource || this._data instanceof Uint8Array) {
|
|
const videoFrame = this.toVideoFrame();
|
|
queueMicrotask(() => videoFrame.close());
|
|
return videoFrame;
|
|
} else {
|
|
return this._data;
|
|
}
|
|
}
|
|
/**
|
|
* Transform this video sample to a new video sample given the options. Can be used to resize, rotate, and crop
|
|
* the sample.
|
|
*
|
|
* In non-browser environments, this method will not work by default. To make it work, register a custom
|
|
* transformer function via {@link registerVideoSampleTransformer}.
|
|
*/
|
|
async transform(options) {
|
|
if (!options || typeof options !== "object") {
|
|
throw new TypeError("options must be an object.");
|
|
}
|
|
if (options.width !== void 0 && (!Number.isInteger(options.width) || options.width <= 0)) {
|
|
throw new TypeError("options.width, when provided, must be a positive integer.");
|
|
}
|
|
if (options.height !== void 0 && (!Number.isInteger(options.height) || options.height <= 0)) {
|
|
throw new TypeError("options.height, when provided, must be a positive integer.");
|
|
}
|
|
if (options.roundDimensionsTo !== void 0 && (!Number.isInteger(options.roundDimensionsTo) || options.roundDimensionsTo <= 0)) {
|
|
throw new TypeError("options.roundDimensionsTo, when provided, must be a positive integer.");
|
|
}
|
|
if (options.fit !== void 0 && !["fill", "contain", "cover"].includes(options.fit)) {
|
|
throw new TypeError('options.fit, when provided, must be one of "fill", "contain", or "cover".');
|
|
}
|
|
if (options.width !== void 0 && options.height !== void 0 && options.fit === void 0) {
|
|
throw new TypeError("When both options.width and options.height are provided, options.fit must also be provided.");
|
|
}
|
|
if (options.rotate !== void 0 && ![0, 90, 180, 270].includes(options.rotate)) {
|
|
throw new TypeError("options.rotate, when provided, must be 0, 90, 180 or 270.");
|
|
}
|
|
if (options.crop !== void 0) {
|
|
validateCropRectangle(options.crop, "options.");
|
|
}
|
|
if (options.alpha !== void 0 && !["keep", "discard"].includes(options.alpha)) {
|
|
throw new TypeError("options.alpha, when provided, must be 'keep' or 'discard'.");
|
|
}
|
|
const rotation = normalizeRotation(this.rotation + (options.rotate ?? 0));
|
|
const [rotatedWidth, rotatedHeight] = rotation % 180 === 0 ? [this.squarePixelWidth, this.squarePixelHeight] : [this.squarePixelHeight, this.squarePixelWidth];
|
|
let finalCrop = options.crop;
|
|
if (finalCrop) {
|
|
finalCrop = clampCropRectangle(finalCrop, rotatedWidth, rotatedHeight);
|
|
}
|
|
const cropWidth = finalCrop ? finalCrop.width : rotatedWidth;
|
|
const cropHeight = finalCrop ? finalCrop.height : rotatedHeight;
|
|
const originalAspectRatio = cropWidth / cropHeight;
|
|
let targetWidth;
|
|
let targetHeight;
|
|
if (options.width !== void 0 && options.height === void 0) {
|
|
targetWidth = options.width;
|
|
targetHeight = targetWidth / originalAspectRatio;
|
|
} else if (options.width === void 0 && options.height !== void 0) {
|
|
targetHeight = options.height;
|
|
targetWidth = targetHeight * originalAspectRatio;
|
|
} else if (options.width !== void 0 && options.height !== void 0) {
|
|
targetWidth = options.width;
|
|
targetHeight = options.height;
|
|
} else {
|
|
targetWidth = cropWidth;
|
|
targetHeight = cropHeight;
|
|
}
|
|
targetWidth = roundToMultiple(targetWidth, options.roundDimensionsTo ?? 1);
|
|
targetHeight = roundToMultiple(targetHeight, options.roundDimensionsTo ?? 1);
|
|
const description = {
|
|
width: targetWidth,
|
|
height: targetHeight,
|
|
fit: options.fit ?? "fill",
|
|
rotation,
|
|
crop: finalCrop ?? {
|
|
left: 0,
|
|
top: 0,
|
|
width: rotatedWidth,
|
|
height: rotatedHeight
|
|
},
|
|
alpha: options.alpha ?? "keep"
|
|
};
|
|
for (const transformer of registeredVideoSampleTransformers) {
|
|
let result = transformer(this, description);
|
|
if (result instanceof Promise)
|
|
result = await result;
|
|
if (result !== null) {
|
|
return result;
|
|
}
|
|
}
|
|
let canvas = null;
|
|
let canvasIsNew = false;
|
|
for (const entry of transformationCanvasCache) {
|
|
if (entry.canvas.width === description.width && entry.canvas.height === description.height) {
|
|
canvas = entry.canvas;
|
|
entry.age = transformationCanvasCacheNextAge++;
|
|
break;
|
|
}
|
|
}
|
|
if (canvas === null) {
|
|
if (typeof OffscreenCanvas !== "undefined") {
|
|
canvas = new OffscreenCanvas(description.width, description.height);
|
|
} else {
|
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
throw new Error("Cannot transform VideoSamples in this environment. Either run in an environment with OffscreenCanvas or HTMLCanvasElement, or supply a custom VideoSample transformer using registerVideoSampleTransformer().");
|
|
}
|
|
canvas = document.createElement("canvas");
|
|
canvas.width = description.width;
|
|
canvas.height = description.height;
|
|
}
|
|
canvasIsNew = true;
|
|
if (transformationCanvasCache.length >= TRANSFORMATION_CANVAS_CACHE_MAX_SIZE) {
|
|
transformationCanvasCache.splice(arrayArgmin(transformationCanvasCache, (x) => x.age), 1);
|
|
}
|
|
transformationCanvasCache.push({
|
|
canvas,
|
|
age: transformationCanvasCacheNextAge++
|
|
});
|
|
}
|
|
const context = canvas.getContext("2d", {
|
|
alpha: true
|
|
});
|
|
if (!context) {
|
|
throw new Error("The '2d' canvas context is required to transform VideoSamples. Register a custom transformer using registerVideoSampleTransformer to work around this limitation.");
|
|
}
|
|
if (description.alpha === "discard") {
|
|
context.fillStyle = "black";
|
|
context.fillRect(0, 0, description.width, description.height);
|
|
} else if (!canvasIsNew) {
|
|
context.clearRect(0, 0, description.width, description.height);
|
|
}
|
|
this.drawWithFit(context, {
|
|
fit: description.fit,
|
|
rotation: description.rotation,
|
|
crop: description.crop
|
|
});
|
|
return new _VideoSample(canvas, {
|
|
timestamp: this.timestamp,
|
|
duration: this.duration,
|
|
rotation: 0
|
|
// Any previous rotation is now baked in
|
|
});
|
|
}
|
|
/** Sets the rotation metadata of this video sample. */
|
|
setRotation(newRotation) {
|
|
if (![0, 90, 180, 270].includes(newRotation)) {
|
|
throw new TypeError("newRotation must be 0, 90, 180, or 270.");
|
|
}
|
|
this.rotation = newRotation;
|
|
}
|
|
/** Sets the presentation timestamp of this video sample, in seconds. */
|
|
setTimestamp(newTimestamp) {
|
|
if (!Number.isFinite(newTimestamp)) {
|
|
throw new TypeError("newTimestamp must be a number.");
|
|
}
|
|
this.timestamp = newTimestamp;
|
|
}
|
|
/** Sets the duration of this video sample, in seconds. */
|
|
setDuration(newDuration) {
|
|
if (!Number.isFinite(newDuration) || newDuration < 0) {
|
|
throw new TypeError("newDuration must be a non-negative number.");
|
|
}
|
|
this.duration = newDuration;
|
|
}
|
|
/** Sets the encode options used when this sample is passed to an encoder. */
|
|
setEncodeOptions(newEncodeOptions) {
|
|
if (!newEncodeOptions || typeof newEncodeOptions !== "object") {
|
|
throw new TypeError("newEncodeOptions must be an object.");
|
|
}
|
|
this.encodeOptions = newEncodeOptions;
|
|
}
|
|
/** Calls \`.close()\`. */
|
|
[Symbol.dispose]() {
|
|
this.close();
|
|
}
|
|
};
|
|
var registeredVideoSampleTransformers = [];
|
|
var TRANSFORMATION_CANVAS_CACHE_MAX_SIZE = 3;
|
|
var transformationCanvasCache = [];
|
|
var transformationCanvasCacheNextAge = 0;
|
|
var VideoSampleColorSpace = class {
|
|
/** Creates a new VideoSampleColorSpace. */
|
|
constructor(init) {
|
|
if (init !== void 0) {
|
|
if (!init || typeof init !== "object") {
|
|
throw new TypeError("init.colorSpace, when provided, must be an object.");
|
|
}
|
|
const primariesValues = Object.keys(COLOR_PRIMARIES_MAP);
|
|
if (init.primaries != null && !primariesValues.includes(init.primaries)) {
|
|
throw new TypeError(\`init.colorSpace.primaries, when provided, must be one of \${primariesValues.join(", ")}.\`);
|
|
}
|
|
const transferValues = Object.keys(TRANSFER_CHARACTERISTICS_MAP);
|
|
if (init.transfer != null && !transferValues.includes(init.transfer)) {
|
|
throw new TypeError(\`init.colorSpace.transfer, when provided, must be one of \${transferValues.join(", ")}.\`);
|
|
}
|
|
const matrixValues = Object.keys(MATRIX_COEFFICIENTS_MAP);
|
|
if (init.matrix != null && !matrixValues.includes(init.matrix)) {
|
|
throw new TypeError(\`init.colorSpace.matrix, when provided, must be one of \${matrixValues.join(", ")}.\`);
|
|
}
|
|
if (init.fullRange != null && typeof init.fullRange !== "boolean") {
|
|
throw new TypeError("init.colorSpace.fullRange, when provided, must be a boolean.");
|
|
}
|
|
}
|
|
this.primaries = init?.primaries ?? null;
|
|
this.transfer = init?.transfer ?? null;
|
|
this.matrix = init?.matrix ?? null;
|
|
this.fullRange = init?.fullRange ?? null;
|
|
}
|
|
/** Serializes the color space to a JSON object. */
|
|
toJSON() {
|
|
return {
|
|
primaries: this.primaries,
|
|
transfer: this.transfer,
|
|
matrix: this.matrix,
|
|
fullRange: this.fullRange
|
|
};
|
|
}
|
|
};
|
|
var isVideoFrame = (x) => {
|
|
return typeof VideoFrame !== "undefined" && x instanceof VideoFrame;
|
|
};
|
|
var clampCropRectangle = (crop, outerWidth, outerHeight) => {
|
|
const left = Math.min(crop.left, outerWidth);
|
|
const top = Math.min(crop.top, outerHeight);
|
|
const width = Math.min(crop.width, outerWidth - left);
|
|
const height = Math.min(crop.height, outerHeight - top);
|
|
assert(width >= 0);
|
|
assert(height >= 0);
|
|
return { left, top, width, height };
|
|
};
|
|
var validateCropRectangle = (crop, prefix) => {
|
|
if (!crop || typeof crop !== "object") {
|
|
throw new TypeError(prefix + "crop, when provided, must be an object.");
|
|
}
|
|
if (!Number.isInteger(crop.left) || crop.left < 0) {
|
|
throw new TypeError(prefix + "crop.left must be a non-negative integer.");
|
|
}
|
|
if (!Number.isInteger(crop.top) || crop.top < 0) {
|
|
throw new TypeError(prefix + "crop.top must be a non-negative integer.");
|
|
}
|
|
if (!Number.isInteger(crop.width) || crop.width < 0) {
|
|
throw new TypeError(prefix + "crop.width must be a non-negative integer.");
|
|
}
|
|
if (!Number.isInteger(crop.height) || crop.height < 0) {
|
|
throw new TypeError(prefix + "crop.height must be a non-negative integer.");
|
|
}
|
|
};
|
|
var validateVideoFrameCopyToOptions = (options) => {
|
|
if (!options || typeof options !== "object") {
|
|
throw new TypeError("options must be an object.");
|
|
}
|
|
if (options.colorSpace !== void 0 && !["display-p3", "srgb"].includes(options.colorSpace)) {
|
|
throw new TypeError("options.colorSpace, when provided, must be 'display-p3' or 'srgb'.");
|
|
}
|
|
if (options.format !== void 0 && typeof options.format !== "string") {
|
|
throw new TypeError("options.format, when provided, must be a string.");
|
|
}
|
|
if (options.layout !== void 0) {
|
|
if (!Array.isArray(options.layout)) {
|
|
throw new TypeError("options.layout, when provided, must be an array.");
|
|
}
|
|
for (const plane of options.layout) {
|
|
if (!plane || typeof plane !== "object") {
|
|
throw new TypeError("Each entry in options.layout must be an object.");
|
|
}
|
|
if (!Number.isInteger(plane.offset) || plane.offset < 0) {
|
|
throw new TypeError("plane.offset must be a non-negative integer.");
|
|
}
|
|
if (!Number.isInteger(plane.stride) || plane.stride < 0) {
|
|
throw new TypeError("plane.stride must be a non-negative integer.");
|
|
}
|
|
}
|
|
}
|
|
if (options.rect !== void 0) {
|
|
if (!options.rect || typeof options.rect !== "object") {
|
|
throw new TypeError("options.rect, when provided, must be an object.");
|
|
}
|
|
if (options.rect.x !== void 0 && (!Number.isInteger(options.rect.x) || options.rect.x < 0)) {
|
|
throw new TypeError("options.rect.x, when provided, must be a non-negative integer.");
|
|
}
|
|
if (options.rect.y !== void 0 && (!Number.isInteger(options.rect.y) || options.rect.y < 0)) {
|
|
throw new TypeError("options.rect.y, when provided, must be a non-negative integer.");
|
|
}
|
|
if (options.rect.width !== void 0 && (!Number.isInteger(options.rect.width) || options.rect.width < 0)) {
|
|
throw new TypeError("options.rect.width, when provided, must be a non-negative integer.");
|
|
}
|
|
if (options.rect.height !== void 0 && (!Number.isInteger(options.rect.height) || options.rect.height < 0)) {
|
|
throw new TypeError("options.rect.height, when provided, must be a non-negative integer.");
|
|
}
|
|
}
|
|
};
|
|
var createDefaultPlaneLayout = (format, codedWidth, codedHeight) => {
|
|
const planes = getPlaneConfigs(format);
|
|
const layouts = [];
|
|
let currentOffset = 0;
|
|
for (const plane of planes) {
|
|
const planeWidth = Math.ceil(codedWidth / plane.widthDivisor);
|
|
const planeHeight = Math.ceil(codedHeight / plane.heightDivisor);
|
|
const stride = planeWidth * plane.sampleBytes;
|
|
const planeSize = stride * planeHeight;
|
|
layouts.push({
|
|
offset: currentOffset,
|
|
stride
|
|
});
|
|
currentOffset += planeSize;
|
|
}
|
|
return layouts;
|
|
};
|
|
var getPlaneConfigs = (format) => {
|
|
const yuv = (yBytes, uvBytes, subX, subY, hasAlpha) => {
|
|
const configs = [
|
|
{ sampleBytes: yBytes, widthDivisor: 1, heightDivisor: 1 },
|
|
{ sampleBytes: uvBytes, widthDivisor: subX, heightDivisor: subY },
|
|
{ sampleBytes: uvBytes, widthDivisor: subX, heightDivisor: subY }
|
|
];
|
|
if (hasAlpha) {
|
|
configs.push({ sampleBytes: yBytes, widthDivisor: 1, heightDivisor: 1 });
|
|
}
|
|
return configs;
|
|
};
|
|
switch (format) {
|
|
case "I420":
|
|
return yuv(1, 1, 2, 2, false);
|
|
case "I420P10":
|
|
case "I420P12":
|
|
return yuv(2, 2, 2, 2, false);
|
|
case "I420A":
|
|
return yuv(1, 1, 2, 2, true);
|
|
case "I420AP10":
|
|
case "I420AP12":
|
|
return yuv(2, 2, 2, 2, true);
|
|
case "I422":
|
|
return yuv(1, 1, 2, 1, false);
|
|
case "I422P10":
|
|
case "I422P12":
|
|
return yuv(2, 2, 2, 1, false);
|
|
case "I422A":
|
|
return yuv(1, 1, 2, 1, true);
|
|
case "I422AP10":
|
|
case "I422AP12":
|
|
return yuv(2, 2, 2, 1, true);
|
|
case "I444":
|
|
return yuv(1, 1, 1, 1, false);
|
|
case "I444P10":
|
|
case "I444P12":
|
|
return yuv(2, 2, 1, 1, false);
|
|
case "I444A":
|
|
return yuv(1, 1, 1, 1, true);
|
|
case "I444AP10":
|
|
case "I444AP12":
|
|
return yuv(2, 2, 1, 1, true);
|
|
case "NV12":
|
|
return [
|
|
{ sampleBytes: 1, widthDivisor: 1, heightDivisor: 1 },
|
|
{ sampleBytes: 2, widthDivisor: 2, heightDivisor: 2 }
|
|
// Interleaved U and V
|
|
];
|
|
case "RGBA":
|
|
case "RGBX":
|
|
case "BGRA":
|
|
case "BGRX":
|
|
return [
|
|
{ sampleBytes: 4, widthDivisor: 1, heightDivisor: 1 }
|
|
];
|
|
default:
|
|
assertNever(format);
|
|
assert(false);
|
|
}
|
|
};
|
|
var ParseVideoFrameCopyToOptions = (sample, options) => {
|
|
const defaultRect = {
|
|
left: 0,
|
|
top: 0,
|
|
width: sample.codedWidth,
|
|
height: sample.codedHeight
|
|
};
|
|
const overrideRect = options.rect;
|
|
const parsedRect = ParseVisibleRect(defaultRect, overrideRect, sample.codedWidth, sample.codedHeight, sample.format);
|
|
const optLayout = options.layout;
|
|
let format;
|
|
if (!options.format || options.format === sample.format) {
|
|
format = sample.format;
|
|
} else if (["RGBA", "RGBX", "BGRA", "BGRX"].includes(options.format)) {
|
|
format = options.format;
|
|
} else {
|
|
throw new Error("NotSupportedError: Invalid destination format.");
|
|
}
|
|
return ComputeLayoutAndAllocationSize(parsedRect, format, optLayout);
|
|
};
|
|
var ParseVisibleRect = (defaultRect, overrideRect, codedWidth, codedHeight, format) => {
|
|
const sourceRect = { ...defaultRect };
|
|
if (overrideRect !== void 0) {
|
|
if (overrideRect.width === 0 || overrideRect.height === 0) {
|
|
throw new TypeError("visibleRect dimensions cannot be zero.");
|
|
}
|
|
if ((overrideRect.x || 0) + (overrideRect.width || 0) > codedWidth) {
|
|
throw new TypeError("visibleRect exceeds codedWidth.");
|
|
}
|
|
if ((overrideRect.y || 0) + (overrideRect.height || 0) > codedHeight) {
|
|
throw new TypeError("visibleRect exceeds codedHeight.");
|
|
}
|
|
sourceRect.x = overrideRect.x || 0;
|
|
sourceRect.y = overrideRect.y || 0;
|
|
sourceRect.width = overrideRect.width || 0;
|
|
sourceRect.height = overrideRect.height || 0;
|
|
}
|
|
const validAlignment = VerifyRectOffsetAlignment(format, sourceRect);
|
|
if (!validAlignment) {
|
|
throw new TypeError("visibleRect alignment is invalid for the format.");
|
|
}
|
|
return sourceRect;
|
|
};
|
|
var VerifyRectOffsetAlignment = (format, rect) => {
|
|
if (format === null)
|
|
return true;
|
|
const planes = getPlaneConfigs(format);
|
|
for (let planeIndex = 0; planeIndex < planes.length; planeIndex++) {
|
|
const plane = planes[planeIndex];
|
|
const sampleWidth = plane.widthDivisor;
|
|
const sampleHeight = plane.heightDivisor;
|
|
if ((rect.x || 0) % sampleWidth !== 0)
|
|
return false;
|
|
if ((rect.y || 0) % sampleHeight !== 0)
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
var ComputeLayoutAndAllocationSize = (parsedRect, format, layout) => {
|
|
const planes = getPlaneConfigs(format);
|
|
const numPlanes = planes.length;
|
|
if (layout !== void 0 && layout.length !== numPlanes) {
|
|
throw new TypeError(\`Layout must have \${numPlanes} planes.\`);
|
|
}
|
|
let minAllocationSize = 0;
|
|
const computedLayouts = [];
|
|
const endOffsets = [];
|
|
for (let planeIndex = 0; planeIndex < numPlanes; planeIndex++) {
|
|
const plane = planes[planeIndex];
|
|
const sampleBytes = plane.sampleBytes;
|
|
const sampleWidth = plane.widthDivisor;
|
|
const sampleHeight = plane.heightDivisor;
|
|
const computedLayout = {
|
|
destinationOffset: 0,
|
|
destinationStride: 0,
|
|
sourceTop: 0,
|
|
sourceHeight: 0,
|
|
sourceLeftBytes: 0,
|
|
sourceWidthBytes: 0
|
|
};
|
|
computedLayout.sourceTop = Math.ceil(Math.trunc(parsedRect.y || 0) / sampleHeight);
|
|
computedLayout.sourceHeight = Math.ceil(Math.trunc(parsedRect.height || 0) / sampleHeight);
|
|
computedLayout.sourceLeftBytes = Math.floor(Math.trunc(parsedRect.x || 0) / sampleWidth) * sampleBytes;
|
|
computedLayout.sourceWidthBytes = Math.floor(Math.trunc(parsedRect.width || 0) / sampleWidth) * sampleBytes;
|
|
if (layout !== void 0) {
|
|
const planeLayout = layout[planeIndex];
|
|
if (planeLayout.stride < computedLayout.sourceWidthBytes) {
|
|
throw new TypeError(\`Stride for plane \${planeIndex} is too small.\`);
|
|
}
|
|
computedLayout.destinationOffset = planeLayout.offset;
|
|
computedLayout.destinationStride = planeLayout.stride;
|
|
} else {
|
|
computedLayout.destinationOffset = minAllocationSize;
|
|
computedLayout.destinationStride = computedLayout.sourceWidthBytes;
|
|
}
|
|
const planeSize = computedLayout.destinationStride * computedLayout.sourceHeight;
|
|
const planeEnd = planeSize + computedLayout.destinationOffset;
|
|
if (planeEnd > 4294967295) {
|
|
throw new TypeError("Allocation size exceeds limit.");
|
|
}
|
|
endOffsets.push(planeEnd);
|
|
minAllocationSize = Math.max(minAllocationSize, planeEnd);
|
|
for (let earlierPlaneIndex = 0; earlierPlaneIndex < planeIndex; earlierPlaneIndex++) {
|
|
const earlierLayout = computedLayouts[earlierPlaneIndex];
|
|
if (endOffsets[planeIndex] <= earlierLayout.destinationOffset || endOffsets[earlierPlaneIndex] <= computedLayout.destinationOffset) {
|
|
continue;
|
|
}
|
|
throw new TypeError("Planes overlap.");
|
|
}
|
|
computedLayouts.push(computedLayout);
|
|
}
|
|
return {
|
|
allocationSize: minAllocationSize,
|
|
computedLayouts
|
|
};
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/encode.js
|
|
var canEncodeVideoMemo = /* @__PURE__ */ new Map();
|
|
var validateVideoEncodingConfig = (config) => {
|
|
if (!config || typeof config !== "object") {
|
|
throw new TypeError("Encoding config must be an object.");
|
|
}
|
|
if (!VIDEO_CODECS.includes(config.codec)) {
|
|
throw new TypeError(\`Invalid video codec '\${config.codec}'. Must be one of: \${VIDEO_CODECS.join(", ")}.\`);
|
|
}
|
|
if (!(config.bitrate instanceof Quality) && (!Number.isInteger(config.bitrate) || config.bitrate <= 0)) {
|
|
throw new TypeError("config.bitrate must be a positive integer or a quality.");
|
|
}
|
|
if (config.keyFrameInterval !== void 0 && (!Number.isFinite(config.keyFrameInterval) || config.keyFrameInterval < 0)) {
|
|
throw new TypeError("config.keyFrameInterval, when provided, must be a non-negative number.");
|
|
}
|
|
if (config.sizeChangeBehavior !== void 0 && !["deny", "passThrough", "fill", "contain", "cover"].includes(config.sizeChangeBehavior)) {
|
|
throw new TypeError("config.sizeChangeBehavior, when provided, must be 'deny', 'passThrough', 'fill', 'contain' or 'cover'.");
|
|
}
|
|
if (config.transform !== void 0) {
|
|
if (typeof config.transform !== "object" || !config.transform) {
|
|
throw new TypeError("config.transform, when provided, must be an object.");
|
|
}
|
|
if (config.transform.width !== void 0 && (!Number.isInteger(config.transform.width) || config.transform.width <= 0)) {
|
|
throw new TypeError("config.transform.width, when provided, must be a positive integer.");
|
|
}
|
|
if (config.transform.height !== void 0 && (!Number.isInteger(config.transform.height) || config.transform.height <= 0)) {
|
|
throw new TypeError("config.transform.height, when provided, must be a positive integer.");
|
|
}
|
|
if (config.transform.fit !== void 0 && !["fill", "contain", "cover"].includes(config.transform.fit)) {
|
|
throw new TypeError('config.transform.fit, when provided, must be one of "fill", "contain", or "cover".');
|
|
}
|
|
if (config.transform.width !== void 0 && config.transform.height !== void 0 && config.transform.fit === void 0 && !["fill", "contain", "cover"].includes(config.sizeChangeBehavior)) {
|
|
throw new TypeError("When both config.transform.width and config.transform.height are provided, config.transform.fit must also be provided.");
|
|
}
|
|
if (config.transform.fit !== void 0 && ["fill", "contain", "cover"].includes(config.sizeChangeBehavior) && config.transform.fit !== config.sizeChangeBehavior) {
|
|
throw new TypeError("config.transform.fit, when provided, cannot differ from config.sizeChangeBehavior when config.sizeChangeBehavior is 'fill', 'contain' or 'cover', as sizeChangeBehavior already determines the fitting algorithm.");
|
|
}
|
|
if (config.transform.rotate !== void 0 && ![0, 90, 180, 270].includes(config.transform.rotate)) {
|
|
throw new TypeError("config.transform.rotate, when provided, must be 0, 90, 180 or 270.");
|
|
}
|
|
if (config.transform.crop !== void 0) {
|
|
validateCropRectangle(config.transform.crop, "config.transform.");
|
|
}
|
|
if (config.transform.process !== void 0 && typeof config.transform.process !== "function") {
|
|
throw new TypeError("config.transform.process, when provided, must be a function.");
|
|
}
|
|
if (config.transform.frameRate !== void 0 && (!Number.isFinite(config.transform.frameRate) || config.transform.frameRate <= 0)) {
|
|
throw new TypeError("config.transform.frameRate, when provided, must be a finite positive number.");
|
|
}
|
|
if (config.transform.force !== void 0 && typeof config.transform.force !== "boolean") {
|
|
throw new TypeError("config.transform.force, when provided, must be a boolean.");
|
|
}
|
|
}
|
|
if (config.onEncodedPacket !== void 0 && typeof config.onEncodedPacket !== "function") {
|
|
throw new TypeError("config.onEncodedPacket, when provided, must be a function.");
|
|
}
|
|
if (config.onEncoderConfig !== void 0 && typeof config.onEncoderConfig !== "function") {
|
|
throw new TypeError("config.onEncoderConfig, when provided, must be a function.");
|
|
}
|
|
if (config.onEncodedSample !== void 0 && typeof config.onEncodedSample !== "function") {
|
|
throw new TypeError("config.onEncodedSample, when provided, must be a function.");
|
|
}
|
|
validateVideoEncodingAdditionalOptions(config.codec, config);
|
|
};
|
|
var validateVideoEncodingAdditionalOptions = (codec, options) => {
|
|
if (!options || typeof options !== "object") {
|
|
throw new TypeError("Encoding options must be an object.");
|
|
}
|
|
if (options.alpha !== void 0 && !["discard", "keep"].includes(options.alpha)) {
|
|
throw new TypeError("options.alpha, when provided, must be 'discard' or 'keep'.");
|
|
}
|
|
if (options.bitrateMode !== void 0 && !["constant", "variable"].includes(options.bitrateMode)) {
|
|
throw new TypeError("bitrateMode, when provided, must be 'constant' or 'variable'.");
|
|
}
|
|
if (options.latencyMode !== void 0 && !["quality", "realtime"].includes(options.latencyMode)) {
|
|
throw new TypeError("latencyMode, when provided, must be 'quality' or 'realtime'.");
|
|
}
|
|
if (options.fullCodecString !== void 0 && typeof options.fullCodecString !== "string") {
|
|
throw new TypeError("fullCodecString, when provided, must be a string.");
|
|
}
|
|
if (options.fullCodecString !== void 0 && inferCodecFromCodecString(options.fullCodecString) !== codec) {
|
|
throw new TypeError(\`fullCodecString, when provided, must be a string that matches the specified codec (\${codec}).\`);
|
|
}
|
|
if (options.hardwareAcceleration !== void 0 && !["no-preference", "prefer-hardware", "prefer-software"].includes(options.hardwareAcceleration)) {
|
|
throw new TypeError("hardwareAcceleration, when provided, must be 'no-preference', 'prefer-hardware' or 'prefer-software'.");
|
|
}
|
|
if (options.scalabilityMode !== void 0 && typeof options.scalabilityMode !== "string") {
|
|
throw new TypeError("scalabilityMode, when provided, must be a string.");
|
|
}
|
|
if (options.contentHint !== void 0 && typeof options.contentHint !== "string") {
|
|
throw new TypeError("contentHint, when provided, must be a string.");
|
|
}
|
|
};
|
|
var buildVideoEncoderConfig = (options) => {
|
|
const resolvedBitrate = options.bitrate instanceof Quality ? options.bitrate._toVideoBitrate(options.codec, options.width, options.height) : options.bitrate;
|
|
return {
|
|
codec: options.fullCodecString ?? buildVideoCodecString(options.codec, options.width, options.height, resolvedBitrate),
|
|
width: options.width,
|
|
height: options.height,
|
|
displayWidth: options.squarePixelWidth,
|
|
displayHeight: options.squarePixelHeight,
|
|
bitrate: resolvedBitrate,
|
|
bitrateMode: options.bitrateMode,
|
|
alpha: options.alpha ?? "discard",
|
|
framerate: options.framerate,
|
|
latencyMode: options.latencyMode,
|
|
hardwareAcceleration: options.hardwareAcceleration,
|
|
scalabilityMode: options.scalabilityMode,
|
|
contentHint: options.contentHint,
|
|
...getVideoEncoderConfigExtension(options.codec)
|
|
};
|
|
};
|
|
var Quality = class {
|
|
/** @internal */
|
|
constructor(factor) {
|
|
this._factor = factor;
|
|
}
|
|
/** @internal */
|
|
_toVideoBitrate(codec, width, height) {
|
|
const pixels = width * height;
|
|
const codecEfficiencyFactors = {
|
|
avc: 1,
|
|
// H.264/AVC (baseline)
|
|
hevc: 0.6,
|
|
// H.265/HEVC (~40% more efficient than AVC)
|
|
vp9: 0.6,
|
|
// Similar to HEVC
|
|
av1: 0.4,
|
|
// ~60% more efficient than AVC
|
|
vp8: 1.2
|
|
// Slightly less efficient than AVC
|
|
};
|
|
const referencePixels = 1920 * 1080;
|
|
const referenceBitrate = 3e6;
|
|
const scaleFactor = Math.pow(pixels / referencePixels, 0.95);
|
|
const baseBitrate = referenceBitrate * scaleFactor;
|
|
const codecAdjustedBitrate = baseBitrate * codecEfficiencyFactors[codec];
|
|
const finalBitrate = codecAdjustedBitrate * this._factor;
|
|
return Math.ceil(finalBitrate / 1e3) * 1e3;
|
|
}
|
|
/** @internal */
|
|
_toAudioBitrate(codec) {
|
|
if (PCM_AUDIO_CODECS.includes(codec) || codec === "flac") {
|
|
return void 0;
|
|
}
|
|
const baseRates = {
|
|
aac: 128e3,
|
|
// 128kbps base for AAC
|
|
opus: 64e3,
|
|
// 64kbps base for Opus
|
|
mp3: 16e4,
|
|
// 160kbps base for MP3
|
|
vorbis: 64e3,
|
|
// 64kbps base for Vorbis
|
|
ac3: 384e3,
|
|
// 384kbps base for AC-3
|
|
eac3: 192e3
|
|
// 192kbps base for E-AC-3
|
|
};
|
|
const baseBitrate = baseRates[codec];
|
|
if (!baseBitrate) {
|
|
throw new Error(\`Unhandled codec: \${codec}\`);
|
|
}
|
|
let finalBitrate = baseBitrate * this._factor;
|
|
if (codec === "aac") {
|
|
const validRates = [96e3, 128e3, 16e4, 192e3];
|
|
finalBitrate = validRates.reduce((prev, curr) => Math.abs(curr - finalBitrate) < Math.abs(prev - finalBitrate) ? curr : prev);
|
|
} else if (codec === "opus" || codec === "vorbis") {
|
|
finalBitrate = Math.max(6e3, finalBitrate);
|
|
} else if (codec === "mp3") {
|
|
const validRates = [
|
|
8e3,
|
|
16e3,
|
|
24e3,
|
|
32e3,
|
|
4e4,
|
|
48e3,
|
|
64e3,
|
|
8e4,
|
|
96e3,
|
|
112e3,
|
|
128e3,
|
|
16e4,
|
|
192e3,
|
|
224e3,
|
|
256e3,
|
|
32e4
|
|
];
|
|
finalBitrate = validRates.reduce((prev, curr) => Math.abs(curr - finalBitrate) < Math.abs(prev - finalBitrate) ? curr : prev);
|
|
}
|
|
return Math.round(finalBitrate / 1e3) * 1e3;
|
|
}
|
|
};
|
|
var QUALITY_HIGH = /* @__PURE__ */ new Quality(2);
|
|
var canEncodeVideo = async (codec, options = {}) => {
|
|
const { width = 1280, height = 720, bitrate = 1e6, ...restOptions } = options;
|
|
if (!VIDEO_CODECS.includes(codec)) {
|
|
return false;
|
|
}
|
|
if (!Number.isInteger(width) || width <= 0) {
|
|
throw new TypeError("width must be a positive integer.");
|
|
}
|
|
if (!Number.isInteger(height) || height <= 0) {
|
|
throw new TypeError("height must be a positive integer.");
|
|
}
|
|
if (!(bitrate instanceof Quality) && (!Number.isInteger(bitrate) || bitrate <= 0)) {
|
|
throw new TypeError("bitrate must be a positive integer or a quality.");
|
|
}
|
|
validateVideoEncodingAdditionalOptions(codec, restOptions);
|
|
const encoderConfig = buildVideoEncoderConfig({
|
|
codec,
|
|
width,
|
|
height,
|
|
bitrate,
|
|
framerate: void 0,
|
|
...restOptions,
|
|
alpha: "discard"
|
|
// Since we handle alpha ourselves
|
|
});
|
|
const key = JSON.stringify(encoderConfig);
|
|
const memoized = canEncodeVideoMemo.get(key);
|
|
if (memoized) {
|
|
return memoized;
|
|
}
|
|
const promise = (async () => {
|
|
if (customVideoEncoders.some((x) => x.supports(codec, encoderConfig))) {
|
|
return true;
|
|
}
|
|
if (typeof VideoEncoder === "undefined") {
|
|
return false;
|
|
}
|
|
const hasOddDimension = width % 2 === 1 || height % 2 === 1;
|
|
if (hasOddDimension && (codec === "avc" || codec === "hevc")) {
|
|
return false;
|
|
}
|
|
const support = await VideoEncoder.isConfigSupported(encoderConfig);
|
|
if (!support.supported) {
|
|
return false;
|
|
}
|
|
if (isFirefox()) {
|
|
return new Promise(async (resolve) => {
|
|
try {
|
|
const encoder = new VideoEncoder({
|
|
output: () => {
|
|
},
|
|
error: () => resolve(false)
|
|
});
|
|
encoder.configure(encoderConfig);
|
|
const frameData = new Uint8Array(width * height * 4);
|
|
const frame = new VideoFrame(frameData, {
|
|
format: "RGBA",
|
|
codedWidth: width,
|
|
codedHeight: height,
|
|
timestamp: 0
|
|
});
|
|
encoder.encode(frame);
|
|
frame.close();
|
|
await encoder.flush();
|
|
resolve(true);
|
|
} catch {
|
|
resolve(false);
|
|
}
|
|
});
|
|
}
|
|
return true;
|
|
})();
|
|
canEncodeVideoMemo.set(key, promise);
|
|
return promise;
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/custom-coder.js
|
|
var customVideoEncoders = [];
|
|
|
|
// node_modules/mediabunny/dist/modules/src/reader.js
|
|
var FileSlice = class _FileSlice {
|
|
constructor(bytes2, view2, offset, start, end) {
|
|
this.bytes = bytes2;
|
|
this.view = view2;
|
|
this.offset = offset;
|
|
this.start = start;
|
|
this.end = end;
|
|
this.bufferPos = start - offset;
|
|
}
|
|
static tempFromBytes(bytes2) {
|
|
return new _FileSlice(bytes2, toDataView(bytes2), 0, 0, bytes2.length);
|
|
}
|
|
get length() {
|
|
return this.end - this.start;
|
|
}
|
|
get filePos() {
|
|
return this.offset + this.bufferPos;
|
|
}
|
|
set filePos(value) {
|
|
this.bufferPos = value - this.offset;
|
|
}
|
|
/** The number of bytes left from the current pos to the end of the slice. */
|
|
get remainingLength() {
|
|
return Math.max(this.end - this.filePos, 0);
|
|
}
|
|
skip(byteCount) {
|
|
this.bufferPos += byteCount;
|
|
}
|
|
/** Creates a new subslice of this slice whose byte range must be contained within this slice. */
|
|
slice(filePos, length = this.end - filePos) {
|
|
if (filePos < this.start || filePos + length > this.end) {
|
|
throw new RangeError("Slicing outside of original slice.");
|
|
}
|
|
return new _FileSlice(this.bytes, this.view, this.offset, filePos, filePos + length);
|
|
}
|
|
};
|
|
var checkIsInRange = (slice, bytesToRead) => {
|
|
if (slice.filePos < slice.start || slice.filePos + bytesToRead > slice.end) {
|
|
throw new RangeError(\`Tried reading [\${slice.filePos}, \${slice.filePos + bytesToRead}), but slice is [\${slice.start}, \${slice.end}). This is likely an internal error, please report it alongside the file that caused it.\`);
|
|
}
|
|
};
|
|
var readBytes = (slice, length) => {
|
|
checkIsInRange(slice, length);
|
|
const bytes2 = slice.bytes.subarray(slice.bufferPos, slice.bufferPos + length);
|
|
slice.bufferPos += length;
|
|
return bytes2;
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/muxer.js
|
|
var Muxer = class {
|
|
constructor(output) {
|
|
this.mutex = new AsyncMutex();
|
|
this.trackTimestampInfo = /* @__PURE__ */ new WeakMap();
|
|
this.output = output;
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
onTrackClose(track) {
|
|
}
|
|
validateTimestamp(track, timestampInSeconds, isKeyPacket) {
|
|
if (timestampInSeconds < 0) {
|
|
throw new Error(\`Timestamps must be non-negative (got \${timestampInSeconds}s).\`);
|
|
}
|
|
let timestampInfo = this.trackTimestampInfo.get(track);
|
|
if (!timestampInfo) {
|
|
if (!isKeyPacket) {
|
|
throw new Error("First packet must be a key packet.");
|
|
}
|
|
timestampInfo = {
|
|
maxTimestamp: timestampInSeconds,
|
|
maxTimestampBeforeLastKeyPacket: null
|
|
};
|
|
this.trackTimestampInfo.set(track, timestampInfo);
|
|
} else {
|
|
if (isKeyPacket) {
|
|
timestampInfo.maxTimestampBeforeLastKeyPacket = timestampInfo.maxTimestamp;
|
|
}
|
|
if (timestampInfo.maxTimestampBeforeLastKeyPacket !== null && timestampInSeconds < timestampInfo.maxTimestampBeforeLastKeyPacket) {
|
|
throw new Error(\`Timestamps cannot be smaller than the largest timestamp of the previous GOP (a GOP begins with a key packet and ends right before the next key packet). Got \${timestampInSeconds}s, but largest timestamp is \${timestampInfo.maxTimestampBeforeLastKeyPacket}s.\`);
|
|
}
|
|
timestampInfo.maxTimestamp = Math.max(timestampInfo.maxTimestamp, timestampInSeconds);
|
|
}
|
|
}
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/subtitles.js
|
|
var inlineTimestampRegex = /<(?:(\\d{2}):)?(\\d{2}):(\\d{2}).(\\d{3})>/g;
|
|
var timestampRegex = /(?:(\\d{2}):)?(\\d{2}):(\\d{2}).(\\d{3})/;
|
|
var parseSubtitleTimestamp = (string) => {
|
|
const match = timestampRegex.exec(string);
|
|
if (!match)
|
|
throw new Error("Expected match.");
|
|
return 60 * 60 * 1e3 * Number(match[1] || "0") + 60 * 1e3 * Number(match[2]) + 1e3 * Number(match[3]) + Number(match[4]);
|
|
};
|
|
var formatSubtitleTimestamp = (timestamp) => {
|
|
const hours = Math.floor(timestamp / (60 * 60 * 1e3));
|
|
const minutes = Math.floor(timestamp % (60 * 60 * 1e3) / (60 * 1e3));
|
|
const seconds = Math.floor(timestamp % (60 * 1e3) / 1e3);
|
|
const milliseconds = timestamp % 1e3;
|
|
return hours.toString().padStart(2, "0") + ":" + minutes.toString().padStart(2, "0") + ":" + seconds.toString().padStart(2, "0") + "." + milliseconds.toString().padStart(3, "0");
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/isobmff/isobmff-boxes.js
|
|
var IsobmffBoxWriter = class {
|
|
constructor(writer) {
|
|
this.writer = writer;
|
|
this.helper = new Uint8Array(8);
|
|
this.helperView = new DataView(this.helper.buffer);
|
|
this.offsets = /* @__PURE__ */ new WeakMap();
|
|
}
|
|
writeU32(value) {
|
|
this.helperView.setUint32(0, value, false);
|
|
this.writer.write(this.helper.subarray(0, 4));
|
|
}
|
|
writeU64(value) {
|
|
this.helperView.setUint32(0, Math.floor(value / 2 ** 32), false);
|
|
this.helperView.setUint32(4, value, false);
|
|
this.writer.write(this.helper.subarray(0, 8));
|
|
}
|
|
writeAscii(text) {
|
|
for (let i2 = 0; i2 < text.length; i2++) {
|
|
this.helperView.setUint8(i2 % 8, text.charCodeAt(i2));
|
|
if (i2 % 8 === 7)
|
|
this.writer.write(this.helper);
|
|
}
|
|
if (text.length % 8 !== 0) {
|
|
this.writer.write(this.helper.subarray(0, text.length % 8));
|
|
}
|
|
}
|
|
writeBox(box2) {
|
|
this.offsets.set(box2, this.writer.getPos());
|
|
if (box2.contents && !box2.children) {
|
|
this.writeBoxHeader(box2, box2.size ?? box2.contents.byteLength + 8);
|
|
this.writer.write(box2.contents);
|
|
} else {
|
|
const startPos = this.writer.getPos();
|
|
this.writeBoxHeader(box2, 0);
|
|
if (box2.contents)
|
|
this.writer.write(box2.contents);
|
|
if (box2.children) {
|
|
for (const child of box2.children)
|
|
if (child)
|
|
this.writeBox(child);
|
|
}
|
|
const endPos = this.writer.getPos();
|
|
const size = box2.size ?? endPos - startPos;
|
|
this.writer.seek(startPos);
|
|
this.writeBoxHeader(box2, size);
|
|
this.writer.seek(endPos);
|
|
}
|
|
}
|
|
writeBoxHeader(box2, size) {
|
|
this.writeU32(box2.largeSize ? 1 : size);
|
|
this.writeAscii(box2.type);
|
|
if (box2.largeSize)
|
|
this.writeU64(size);
|
|
}
|
|
measureBoxHeader(box2) {
|
|
return 8 + (box2.largeSize ? 8 : 0);
|
|
}
|
|
patchBox(box2) {
|
|
const boxOffset = this.offsets.get(box2);
|
|
assert(boxOffset !== void 0);
|
|
const endPos = this.writer.getPos();
|
|
this.writer.seek(boxOffset);
|
|
this.writeBox(box2);
|
|
this.writer.seek(endPos);
|
|
}
|
|
measureBox(box2) {
|
|
if (box2.contents && !box2.children) {
|
|
const headerSize = this.measureBoxHeader(box2);
|
|
return headerSize + box2.contents.byteLength;
|
|
} else {
|
|
let result = this.measureBoxHeader(box2);
|
|
if (box2.contents)
|
|
result += box2.contents.byteLength;
|
|
if (box2.children) {
|
|
for (const child of box2.children)
|
|
if (child)
|
|
result += this.measureBox(child);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
};
|
|
var bytes = /* @__PURE__ */ new Uint8Array(8);
|
|
var view = /* @__PURE__ */ new DataView(bytes.buffer);
|
|
var u8 = (value) => {
|
|
return [(value % 256 + 256) % 256];
|
|
};
|
|
var u16 = (value) => {
|
|
view.setUint16(0, value, false);
|
|
return [bytes[0], bytes[1]];
|
|
};
|
|
var i16 = (value) => {
|
|
view.setInt16(0, value, false);
|
|
return [bytes[0], bytes[1]];
|
|
};
|
|
var u24 = (value) => {
|
|
view.setUint32(0, value, false);
|
|
return [bytes[1], bytes[2], bytes[3]];
|
|
};
|
|
var u32 = (value) => {
|
|
view.setUint32(0, value, false);
|
|
return [bytes[0], bytes[1], bytes[2], bytes[3]];
|
|
};
|
|
var i32 = (value) => {
|
|
view.setInt32(0, value, false);
|
|
return [bytes[0], bytes[1], bytes[2], bytes[3]];
|
|
};
|
|
var u64 = (value) => {
|
|
view.setUint32(0, Math.floor(value / 2 ** 32), false);
|
|
view.setUint32(4, value, false);
|
|
return [bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]];
|
|
};
|
|
var i64 = (value) => {
|
|
view.setInt32(0, Math.floor(value / 2 ** 32), false);
|
|
view.setUint32(4, value, false);
|
|
return [bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]];
|
|
};
|
|
var fixed_8_8 = (value) => {
|
|
view.setInt16(0, 2 ** 8 * value, false);
|
|
return [bytes[0], bytes[1]];
|
|
};
|
|
var fixed_16_16 = (value) => {
|
|
view.setInt32(0, 2 ** 16 * value, false);
|
|
return [bytes[0], bytes[1], bytes[2], bytes[3]];
|
|
};
|
|
var fixed_2_30 = (value) => {
|
|
view.setInt32(0, 2 ** 30 * value, false);
|
|
return [bytes[0], bytes[1], bytes[2], bytes[3]];
|
|
};
|
|
var variableUnsignedInt = (value, byteLength) => {
|
|
const bytes2 = [];
|
|
let remaining = value;
|
|
do {
|
|
let byte = remaining & 127;
|
|
remaining >>= 7;
|
|
if (bytes2.length > 0) {
|
|
byte |= 128;
|
|
}
|
|
bytes2.push(byte);
|
|
if (byteLength !== void 0) {
|
|
byteLength--;
|
|
}
|
|
} while (remaining > 0 || byteLength);
|
|
return bytes2.reverse();
|
|
};
|
|
var ascii = (text, nullTerminated = false) => {
|
|
const bytes2 = Array(text.length).fill(null).map((_2, i2) => text.charCodeAt(i2));
|
|
if (nullTerminated)
|
|
bytes2.push(0);
|
|
return bytes2;
|
|
};
|
|
var rotationMatrix = (rotationInDegrees) => {
|
|
const theta = rotationInDegrees * (Math.PI / 180);
|
|
const cosTheta = Math.round(Math.cos(theta));
|
|
const sinTheta = Math.round(Math.sin(theta));
|
|
return [
|
|
cosTheta,
|
|
sinTheta,
|
|
0,
|
|
-sinTheta,
|
|
cosTheta,
|
|
0,
|
|
0,
|
|
0,
|
|
1
|
|
];
|
|
};
|
|
var IDENTITY_MATRIX = /* @__PURE__ */ rotationMatrix(0);
|
|
var matrixToBytes = (matrix) => {
|
|
return [
|
|
fixed_16_16(matrix[0]),
|
|
fixed_16_16(matrix[1]),
|
|
fixed_2_30(matrix[2]),
|
|
fixed_16_16(matrix[3]),
|
|
fixed_16_16(matrix[4]),
|
|
fixed_2_30(matrix[5]),
|
|
fixed_16_16(matrix[6]),
|
|
fixed_16_16(matrix[7]),
|
|
fixed_2_30(matrix[8])
|
|
];
|
|
};
|
|
var box = (type, contents, children) => ({
|
|
type,
|
|
contents: contents && new Uint8Array(contents.flat(10)),
|
|
children
|
|
});
|
|
var fullBox = (type, version, flags, contents, children) => box(type, [u8(version), u24(flags), contents ?? []], children);
|
|
var ftyp = (details) => {
|
|
const minorVersion = 512;
|
|
if (details.isQuickTime) {
|
|
return box("ftyp", [
|
|
ascii("qt "),
|
|
// Major brand
|
|
u32(minorVersion),
|
|
// Minor version
|
|
// Compatible brands
|
|
ascii("qt ")
|
|
]);
|
|
}
|
|
if (details.fragmented) {
|
|
if (details.cmaf) {
|
|
return box("ftyp", [
|
|
ascii("iso5"),
|
|
// Major brand
|
|
u32(minorVersion),
|
|
// Minor version
|
|
// Compatible brands
|
|
ascii("iso5"),
|
|
ascii("iso6"),
|
|
ascii("mp41"),
|
|
ascii("cmfc"),
|
|
ascii("dash")
|
|
]);
|
|
} else {
|
|
return box("ftyp", [
|
|
ascii("iso5"),
|
|
// Major brand
|
|
u32(minorVersion),
|
|
// Minor version
|
|
// Compatible brands
|
|
ascii("iso5"),
|
|
ascii("iso6"),
|
|
ascii("mp41")
|
|
]);
|
|
}
|
|
}
|
|
return box("ftyp", [
|
|
ascii("isom"),
|
|
// Major brand
|
|
u32(minorVersion),
|
|
// Minor version
|
|
// Compatible brands
|
|
ascii("isom"),
|
|
details.holdsAvc ? ascii("avc1") : [],
|
|
ascii("mp41")
|
|
]);
|
|
};
|
|
var styp = () => box("styp", [
|
|
ascii("iso5"),
|
|
// Major brand
|
|
u32(0),
|
|
// Minor version
|
|
// Compatible brands
|
|
ascii("iso5"),
|
|
ascii("iso6"),
|
|
ascii("mp41"),
|
|
ascii("cmfc"),
|
|
ascii("dash")
|
|
]);
|
|
var sidx = (muxer, referencedSize) => {
|
|
let duration = muxer.maxWrittenEndTimestamp - muxer.minWrittenTimestamp;
|
|
if (!Number.isFinite(duration)) {
|
|
duration = 0;
|
|
}
|
|
return fullBox("sidx", 1, 0, [
|
|
u32(1),
|
|
// Reference ID
|
|
u32(GLOBAL_TIMESCALE),
|
|
// Timescale
|
|
u64(intoTimescale(muxer.minWrittenTimestamp, GLOBAL_TIMESCALE)),
|
|
// Earliest presentation time
|
|
u64(0),
|
|
// First offset
|
|
u16(0),
|
|
// Reserved
|
|
u16(1),
|
|
// Reference count
|
|
u32(referencedSize & 2147483647),
|
|
// Reference type (0) + referenced size
|
|
u32(intoTimescale(duration, GLOBAL_TIMESCALE)),
|
|
// Subsegment duration
|
|
u32(0)
|
|
// Starts with SAP + SAP type + SAP delta time (no information provided)
|
|
]);
|
|
};
|
|
var mdat = (reserveLargeSize) => ({ type: "mdat", largeSize: reserveLargeSize });
|
|
var free = (size) => ({ type: "free", size });
|
|
var moov = (muxer) => {
|
|
return box("moov", void 0, [
|
|
mvhd(muxer.creationTime, muxer.trackDatas),
|
|
...muxer.trackDatas.map((x) => trak(x, muxer.creationTime)),
|
|
muxer.isFragmented ? mvex(muxer.trackDatas) : null,
|
|
udta(muxer)
|
|
]);
|
|
};
|
|
var mvhd = (creationTime, trackDatas) => {
|
|
const duration = Math.max(0, ...trackDatas.map((trackData) => intoTimescale(presentationSpan(trackData), GLOBAL_TIMESCALE) + intoTimescale(trackData.startTimestampOffset ?? 0, GLOBAL_TIMESCALE)));
|
|
const nextTrackId = Math.max(0, ...trackDatas.map((x) => x.track.id)) + 1;
|
|
const needsU64 = !isU32(creationTime) || !isU32(duration);
|
|
const u32OrU64 = needsU64 ? u64 : u32;
|
|
return fullBox("mvhd", +needsU64, 0, [
|
|
u32OrU64(creationTime),
|
|
// Creation time
|
|
u32OrU64(creationTime),
|
|
// Modification time
|
|
u32(GLOBAL_TIMESCALE),
|
|
// Timescale
|
|
u32OrU64(duration),
|
|
// Duration
|
|
fixed_16_16(1),
|
|
// Preferred rate
|
|
fixed_8_8(1),
|
|
// Preferred volume
|
|
Array(10).fill(0),
|
|
// Reserved
|
|
matrixToBytes(IDENTITY_MATRIX),
|
|
// Matrix
|
|
Array(24).fill(0),
|
|
// Pre-defined
|
|
u32(nextTrackId)
|
|
// Next track ID
|
|
]);
|
|
};
|
|
var presentationSpan = (trackData) => {
|
|
if (trackData.samples.length === 0) {
|
|
return 0;
|
|
}
|
|
let minTimestamp = Infinity;
|
|
let maxEndTimestamp = -Infinity;
|
|
for (let i2 = 0; i2 < trackData.samples.length; i2++) {
|
|
const sample = trackData.samples[i2];
|
|
if (sample.timestamp < minTimestamp) {
|
|
minTimestamp = sample.timestamp;
|
|
}
|
|
if (sample.timestamp + sample.duration > maxEndTimestamp) {
|
|
maxEndTimestamp = sample.timestamp + sample.duration;
|
|
}
|
|
}
|
|
if (minTimestamp === Infinity) {
|
|
return 0;
|
|
}
|
|
return maxEndTimestamp - minTimestamp;
|
|
};
|
|
var trak = (trackData, creationTime) => {
|
|
const trackMetadata = getTrackMetadata(trackData);
|
|
const needsEditList = trackData.startTimestampOffset !== null && trackData.startTimestampOffset > 0;
|
|
return box("trak", void 0, [
|
|
tkhd(trackData, creationTime),
|
|
needsEditList ? edts(trackData, trackData.startTimestampOffset) : null,
|
|
mdia(trackData, creationTime),
|
|
trackMetadata.name !== void 0 ? box("udta", void 0, [
|
|
box("name", [
|
|
...textEncoder.encode(trackMetadata.name)
|
|
])
|
|
]) : null
|
|
]);
|
|
};
|
|
var tkhd = (trackData, creationTime) => {
|
|
const durationInGlobalTimescale = intoTimescale(presentationSpan(trackData), GLOBAL_TIMESCALE) + intoTimescale(trackData.startTimestampOffset ?? 0, GLOBAL_TIMESCALE);
|
|
const needsU64 = !isU32(creationTime) || !isU32(durationInGlobalTimescale);
|
|
const u32OrU64 = needsU64 ? u64 : u32;
|
|
let matrix;
|
|
if (trackData.type === "video") {
|
|
const rotation = trackData.track.metadata.rotation;
|
|
matrix = rotationMatrix(rotation ?? 0);
|
|
} else {
|
|
matrix = IDENTITY_MATRIX;
|
|
}
|
|
let flags = 2;
|
|
if (trackData.track.metadata.disposition?.default !== false) {
|
|
flags |= 1;
|
|
}
|
|
return fullBox("tkhd", +needsU64, flags, [
|
|
u32OrU64(creationTime),
|
|
// Creation time
|
|
u32OrU64(creationTime),
|
|
// Modification time
|
|
u32(trackData.track.id),
|
|
// Track ID
|
|
u32(0),
|
|
// Reserved
|
|
u32OrU64(durationInGlobalTimescale),
|
|
// Duration
|
|
Array(8).fill(0),
|
|
// Reserved
|
|
u16(0),
|
|
// Layer
|
|
u16(trackData.track.id),
|
|
// Alternate group
|
|
fixed_8_8(trackData.type === "audio" ? 1 : 0),
|
|
// Volume
|
|
u16(0),
|
|
// Reserved
|
|
matrixToBytes(matrix),
|
|
// Matrix
|
|
fixed_16_16(trackData.type === "video" ? trackData.info.width : 0),
|
|
// Track width
|
|
fixed_16_16(trackData.type === "video" ? trackData.info.height : 0)
|
|
// Track height
|
|
]);
|
|
};
|
|
var edts = (trackData, offset) => {
|
|
const startOffset = intoTimescale(offset, GLOBAL_TIMESCALE);
|
|
const mediaDuration = intoTimescale(presentationSpan(trackData), GLOBAL_TIMESCALE);
|
|
const needs64Bits = !isU32(startOffset) || !isU32(mediaDuration);
|
|
const u32OrU64 = needs64Bits ? u64 : u32;
|
|
const i32OrI64 = needs64Bits ? i64 : i32;
|
|
return box("edts", void 0, [
|
|
fullBox("elst", needs64Bits ? 1 : 0, 0, [
|
|
u32(2),
|
|
// Entry count
|
|
// #1
|
|
u32OrU64(startOffset),
|
|
// Segment duration
|
|
i32OrI64(-1),
|
|
// Media time
|
|
fixed_16_16(1),
|
|
// Media rate
|
|
// #2
|
|
u32OrU64(mediaDuration),
|
|
// Segment duration
|
|
i32OrI64(0),
|
|
// Media time
|
|
fixed_16_16(1)
|
|
// Media rate
|
|
])
|
|
]);
|
|
};
|
|
var mdia = (trackData, creationTime) => box("mdia", void 0, [
|
|
mdhd(trackData, creationTime),
|
|
hdlr(true, TRACK_TYPE_TO_COMPONENT_SUBTYPE[trackData.type], TRACK_TYPE_TO_HANDLER_NAME[trackData.type]),
|
|
minf(trackData)
|
|
]);
|
|
var mdhd = (trackData, creationTime) => {
|
|
const localDuration = intoTimescale(presentationSpan(trackData), trackData.timescale);
|
|
const needsU64 = !isU32(creationTime) || !isU32(localDuration);
|
|
const u32OrU64 = needsU64 ? u64 : u32;
|
|
return fullBox("mdhd", +needsU64, 0, [
|
|
u32OrU64(creationTime),
|
|
// Creation time
|
|
u32OrU64(creationTime),
|
|
// Modification time
|
|
u32(trackData.timescale),
|
|
// Timescale
|
|
u32OrU64(localDuration),
|
|
// Duration
|
|
u16(getLanguageCodeInt(trackData.track.metadata.languageCode ?? UNDETERMINED_LANGUAGE)),
|
|
// Language
|
|
u16(0)
|
|
// Quality
|
|
]);
|
|
};
|
|
var TRACK_TYPE_TO_COMPONENT_SUBTYPE = {
|
|
video: "vide",
|
|
audio: "soun",
|
|
subtitle: "text"
|
|
};
|
|
var TRACK_TYPE_TO_HANDLER_NAME = {
|
|
video: "MediabunnyVideoHandler",
|
|
audio: "MediabunnySoundHandler",
|
|
subtitle: "MediabunnyTextHandler"
|
|
};
|
|
var hdlr = (hasComponentType, handlerType, name, manufacturer = "\\0\\0\\0\\0") => fullBox("hdlr", 0, 0, [
|
|
hasComponentType ? ascii("mhlr") : u32(0),
|
|
// Component type
|
|
ascii(handlerType),
|
|
// Component subtype
|
|
ascii(manufacturer),
|
|
// Component manufacturer
|
|
u32(0),
|
|
// Component flags
|
|
u32(0),
|
|
// Component flags mask
|
|
ascii(name, true)
|
|
// Component name
|
|
]);
|
|
var minf = (trackData) => box("minf", void 0, [
|
|
TRACK_TYPE_TO_HEADER_BOX[trackData.type](),
|
|
dinf(),
|
|
stbl(trackData)
|
|
]);
|
|
var vmhd = () => fullBox("vmhd", 0, 1, [
|
|
u16(0),
|
|
// Graphics mode
|
|
u16(0),
|
|
// Opcolor R
|
|
u16(0),
|
|
// Opcolor G
|
|
u16(0)
|
|
// Opcolor B
|
|
]);
|
|
var smhd = () => fullBox("smhd", 0, 0, [
|
|
u16(0),
|
|
// Balance
|
|
u16(0)
|
|
// Reserved
|
|
]);
|
|
var nmhd = () => fullBox("nmhd", 0, 0);
|
|
var TRACK_TYPE_TO_HEADER_BOX = {
|
|
video: vmhd,
|
|
audio: smhd,
|
|
subtitle: nmhd
|
|
};
|
|
var dinf = () => box("dinf", void 0, [
|
|
dref()
|
|
]);
|
|
var dref = () => fullBox("dref", 0, 0, [
|
|
u32(1)
|
|
// Entry count
|
|
], [
|
|
url()
|
|
]);
|
|
var url = () => fullBox("url ", 0, 1);
|
|
var stbl = (trackData) => {
|
|
const needsCtts = trackData.compositionTimeOffsetTable.length > 1 || trackData.compositionTimeOffsetTable.some((x) => x.sampleCompositionTimeOffset !== 0);
|
|
return box("stbl", void 0, [
|
|
stsd(trackData),
|
|
stts(trackData),
|
|
needsCtts ? ctts(trackData) : null,
|
|
needsCtts ? cslg(trackData) : null,
|
|
stsc(trackData),
|
|
stsz(trackData),
|
|
stco(trackData),
|
|
stss(trackData)
|
|
]);
|
|
};
|
|
var stsd = (trackData) => {
|
|
let sampleDescription;
|
|
if (trackData.type === "video") {
|
|
sampleDescription = videoSampleDescription(videoCodecToBoxName(trackData.track.source._codec, trackData.info.decoderConfig.codec), trackData);
|
|
} else if (trackData.type === "audio") {
|
|
const boxName = audioCodecToBoxName(trackData.track.source._codec, trackData.muxer.isQuickTime);
|
|
assert(boxName);
|
|
sampleDescription = soundSampleDescription(boxName, trackData);
|
|
} else if (trackData.type === "subtitle") {
|
|
sampleDescription = subtitleSampleDescription(SUBTITLE_CODEC_TO_BOX_NAME[trackData.track.source._codec], trackData);
|
|
}
|
|
assert(sampleDescription);
|
|
return fullBox("stsd", 0, 0, [
|
|
u32(1)
|
|
// Entry count
|
|
], [
|
|
sampleDescription
|
|
]);
|
|
};
|
|
var videoSampleDescription = (compressionType, trackData) => box(compressionType, [
|
|
Array(6).fill(0),
|
|
// Reserved
|
|
u16(1),
|
|
// Data reference index
|
|
u16(0),
|
|
// Pre-defined
|
|
u16(0),
|
|
// Reserved
|
|
Array(12).fill(0),
|
|
// Pre-defined
|
|
u16(trackData.info.width),
|
|
// Width
|
|
u16(trackData.info.height),
|
|
// Height
|
|
u32(4718592),
|
|
// Horizontal resolution
|
|
u32(4718592),
|
|
// Vertical resolution
|
|
u32(0),
|
|
// Reserved
|
|
u16(1),
|
|
// Frame count
|
|
Array(32).fill(0),
|
|
// Compressor name
|
|
u16(24),
|
|
// Depth
|
|
i16(65535)
|
|
// Pre-defined
|
|
], [
|
|
VIDEO_CODEC_TO_CONFIGURATION_BOX[trackData.track.source._codec](trackData),
|
|
pasp(trackData),
|
|
colorSpaceIsComplete(trackData.info.decoderConfig.colorSpace) ? colr(trackData) : null
|
|
]);
|
|
var pasp = (trackData) => {
|
|
if (trackData.info.pixelAspectRatio.num === trackData.info.pixelAspectRatio.den) {
|
|
return null;
|
|
}
|
|
return box("pasp", [
|
|
u32(trackData.info.pixelAspectRatio.num),
|
|
u32(trackData.info.pixelAspectRatio.den)
|
|
]);
|
|
};
|
|
var colr = (trackData) => box("colr", [
|
|
ascii(trackData.muxer.isQuickTime ? "nclc" : "nclx"),
|
|
// Colour type
|
|
u16(COLOR_PRIMARIES_MAP[trackData.info.decoderConfig.colorSpace.primaries]),
|
|
// Colour primaries
|
|
u16(TRANSFER_CHARACTERISTICS_MAP[trackData.info.decoderConfig.colorSpace.transfer]),
|
|
// Transfer characteristics
|
|
u16(MATRIX_COEFFICIENTS_MAP[trackData.info.decoderConfig.colorSpace.matrix]),
|
|
// Matrix coefficients
|
|
trackData.muxer.isQuickTime ? [] : u8((trackData.info.decoderConfig.colorSpace.fullRange ? 1 : 0) << 7)
|
|
// Full range flag
|
|
]);
|
|
var avcC = (trackData) => trackData.info.decoderConfig && box("avcC", [
|
|
// For AVC, description is an AVCDecoderConfigurationRecord, so nothing else to do here
|
|
...toUint8Array(trackData.info.decoderConfig.description)
|
|
]);
|
|
var hvcC = (trackData) => trackData.info.decoderConfig && box("hvcC", [
|
|
// For HEVC, description is an HEVCDecoderConfigurationRecord, so nothing else to do here
|
|
...toUint8Array(trackData.info.decoderConfig.description)
|
|
]);
|
|
var vpcC = (trackData) => {
|
|
if (!trackData.info.decoderConfig) {
|
|
return null;
|
|
}
|
|
const decoderConfig = trackData.info.decoderConfig;
|
|
const parts = decoderConfig.codec.split(".");
|
|
const profile = Number(parts[1]);
|
|
const level = Number(parts[2]);
|
|
const bitDepth = Number(parts[3]);
|
|
const chromaSubsampling = parts[4] ? Number(parts[4]) : 1;
|
|
const videoFullRangeFlag = parts[8] ? Number(parts[8]) : Number(decoderConfig.colorSpace?.fullRange ?? 0);
|
|
const thirdByte = (bitDepth << 4) + (chromaSubsampling << 1) + videoFullRangeFlag;
|
|
const colourPrimaries = parts[5] ? Number(parts[5]) : decoderConfig.colorSpace?.primaries ? COLOR_PRIMARIES_MAP[decoderConfig.colorSpace.primaries] : 2;
|
|
const transferCharacteristics = parts[6] ? Number(parts[6]) : decoderConfig.colorSpace?.transfer ? TRANSFER_CHARACTERISTICS_MAP[decoderConfig.colorSpace.transfer] : 2;
|
|
const matrixCoefficients = parts[7] ? Number(parts[7]) : decoderConfig.colorSpace?.matrix ? MATRIX_COEFFICIENTS_MAP[decoderConfig.colorSpace.matrix] : 2;
|
|
return fullBox("vpcC", 1, 0, [
|
|
u8(profile),
|
|
// Profile
|
|
u8(level),
|
|
// Level
|
|
u8(thirdByte),
|
|
// Bit depth, chroma subsampling, full range
|
|
u8(colourPrimaries),
|
|
// Colour primaries
|
|
u8(transferCharacteristics),
|
|
// Transfer characteristics
|
|
u8(matrixCoefficients),
|
|
// Matrix coefficients
|
|
u16(0)
|
|
// Codec initialization data size
|
|
]);
|
|
};
|
|
var av1C = (trackData) => {
|
|
return box("av1C", generateAv1CodecConfigurationFromCodecString(trackData.info.decoderConfig.codec));
|
|
};
|
|
var soundSampleDescription = (compressionType, trackData) => {
|
|
let version = 0;
|
|
let contents;
|
|
let sampleSizeInBits = 16;
|
|
const isPcmCodec = PCM_AUDIO_CODECS.includes(trackData.track.source._codec);
|
|
if (isPcmCodec) {
|
|
const codec = trackData.track.source._codec;
|
|
const { sampleSize } = parsePcmCodec(codec);
|
|
sampleSizeInBits = 8 * sampleSize;
|
|
if (sampleSizeInBits > 16) {
|
|
version = 1;
|
|
}
|
|
}
|
|
if (trackData.muxer.isQuickTime) {
|
|
version = 1;
|
|
}
|
|
if (version === 0) {
|
|
contents = [
|
|
Array(6).fill(0),
|
|
// Reserved
|
|
u16(1),
|
|
// Data reference index
|
|
u16(version),
|
|
// Version
|
|
u16(0),
|
|
// Revision level
|
|
u32(0),
|
|
// Vendor
|
|
u16(trackData.info.numberOfChannels),
|
|
// Number of channels
|
|
u16(sampleSizeInBits),
|
|
// Sample size (bits)
|
|
u16(0),
|
|
// Compression ID
|
|
u16(0),
|
|
// Packet size
|
|
u16(trackData.info.sampleRate < 2 ** 16 ? trackData.info.sampleRate : 0),
|
|
// Sample rate (upper)
|
|
u16(0)
|
|
// Sample rate (lower)
|
|
];
|
|
} else {
|
|
const compressionId = isPcmCodec ? 0 : -2;
|
|
contents = [
|
|
Array(6).fill(0),
|
|
// Reserved
|
|
u16(1),
|
|
// Data reference index
|
|
u16(version),
|
|
// Version
|
|
u16(0),
|
|
// Revision level
|
|
u32(0),
|
|
// Vendor
|
|
u16(trackData.info.numberOfChannels),
|
|
// Number of channels
|
|
u16(Math.min(sampleSizeInBits, 16)),
|
|
// Sample size (bits)
|
|
i16(compressionId),
|
|
// Compression ID
|
|
u16(0),
|
|
// Packet size
|
|
u16(trackData.info.sampleRate < 2 ** 16 ? trackData.info.sampleRate : 0),
|
|
// Sample rate (upper)
|
|
u16(0),
|
|
// Sample rate (lower)
|
|
isPcmCodec ? [
|
|
u32(1),
|
|
// Samples per packet (must be 1 for uncompressed formats)
|
|
u32(sampleSizeInBits / 8),
|
|
// Bytes per packet
|
|
u32(trackData.info.numberOfChannels * sampleSizeInBits / 8)
|
|
// Bytes per frame
|
|
] : [
|
|
u32(0),
|
|
// Samples per packet (don't bother, still works with 0)
|
|
u32(0),
|
|
// Bytes per packet (variable)
|
|
u32(0)
|
|
// Bytes per frame (variable)
|
|
],
|
|
u32(2)
|
|
// Bytes per sample (constant in FFmpeg)
|
|
];
|
|
}
|
|
return box(compressionType, contents, [
|
|
audioCodecToConfigurationBox(trackData.track.source._codec, trackData.muxer.isQuickTime)?.(trackData) ?? null
|
|
]);
|
|
};
|
|
var esds = (trackData) => {
|
|
let objectTypeIndication;
|
|
switch (trackData.track.source._codec) {
|
|
case "aac":
|
|
{
|
|
objectTypeIndication = 64;
|
|
}
|
|
;
|
|
break;
|
|
case "mp3":
|
|
{
|
|
objectTypeIndication = 107;
|
|
}
|
|
;
|
|
break;
|
|
case "vorbis":
|
|
{
|
|
objectTypeIndication = 221;
|
|
}
|
|
;
|
|
break;
|
|
default:
|
|
throw new Error(\`Unhandled audio codec: \${trackData.track.source._codec}\`);
|
|
}
|
|
let bytes2 = [
|
|
...u8(objectTypeIndication),
|
|
// Object type indication
|
|
...u8(21),
|
|
// stream type(6bits)=5 audio, flags(2bits)=1
|
|
...u24(0),
|
|
// 24bit buffer size
|
|
...u32(0),
|
|
// max bitrate
|
|
...u32(0)
|
|
// avg bitrate
|
|
];
|
|
if (trackData.info.decoderConfig.description) {
|
|
const description = toUint8Array(trackData.info.decoderConfig.description);
|
|
bytes2 = [
|
|
...bytes2,
|
|
...u8(5),
|
|
// TAG(5) = DecoderSpecificInfo
|
|
...variableUnsignedInt(description.byteLength),
|
|
...description
|
|
];
|
|
}
|
|
bytes2 = [
|
|
...u16(1),
|
|
// ES_ID = 1
|
|
...u8(0),
|
|
// flags etc = 0
|
|
...u8(4),
|
|
// TAG(4) = ES Descriptor
|
|
...variableUnsignedInt(bytes2.length),
|
|
...bytes2,
|
|
...u8(6),
|
|
// TAG(6)
|
|
...u8(1),
|
|
// length
|
|
...u8(2)
|
|
// data
|
|
];
|
|
bytes2 = [
|
|
...u8(3),
|
|
// TAG(3) = Object Descriptor
|
|
...variableUnsignedInt(bytes2.length),
|
|
...bytes2
|
|
];
|
|
return fullBox("esds", 0, 0, bytes2);
|
|
};
|
|
var wave = (trackData) => {
|
|
return box("wave", void 0, [
|
|
frma(trackData),
|
|
enda(trackData),
|
|
box("\\0\\0\\0\\0")
|
|
// NULL tag at the end
|
|
]);
|
|
};
|
|
var frma = (trackData) => {
|
|
return box("frma", [
|
|
ascii(audioCodecToBoxName(trackData.track.source._codec, trackData.muxer.isQuickTime))
|
|
]);
|
|
};
|
|
var enda = (trackData) => {
|
|
const { littleEndian } = parsePcmCodec(trackData.track.source._codec);
|
|
return box("enda", [
|
|
u16(+littleEndian)
|
|
]);
|
|
};
|
|
var dOps = (trackData) => {
|
|
let outputChannelCount = trackData.info.numberOfChannels;
|
|
let preSkip = 3840;
|
|
let inputSampleRate = trackData.info.sampleRate;
|
|
let outputGain = 0;
|
|
let channelMappingFamily = 0;
|
|
let channelMappingTable = new Uint8Array(0);
|
|
const description = trackData.info.decoderConfig?.description;
|
|
if (description) {
|
|
assert(description.byteLength >= 18);
|
|
const bytes2 = toUint8Array(description);
|
|
const header = parseOpusIdentificationHeader(bytes2);
|
|
outputChannelCount = header.outputChannelCount;
|
|
preSkip = header.preSkip;
|
|
inputSampleRate = header.inputSampleRate;
|
|
outputGain = header.outputGain;
|
|
channelMappingFamily = header.channelMappingFamily;
|
|
if (header.channelMappingTable) {
|
|
channelMappingTable = header.channelMappingTable;
|
|
}
|
|
}
|
|
return box("dOps", [
|
|
u8(0),
|
|
// Version
|
|
u8(outputChannelCount),
|
|
// OutputChannelCount
|
|
u16(preSkip),
|
|
// PreSkip
|
|
u32(inputSampleRate),
|
|
// InputSampleRate
|
|
i16(outputGain),
|
|
// OutputGain
|
|
u8(channelMappingFamily),
|
|
// ChannelMappingFamily
|
|
...channelMappingTable
|
|
]);
|
|
};
|
|
var dfLa = (trackData) => {
|
|
const description = trackData.info.decoderConfig?.description;
|
|
assert(description);
|
|
const bytes2 = toUint8Array(description);
|
|
return fullBox("dfLa", 0, 0, [
|
|
...bytes2.subarray(4)
|
|
]);
|
|
};
|
|
var pcmC = (trackData) => {
|
|
const { littleEndian, sampleSize } = parsePcmCodec(trackData.track.source._codec);
|
|
const formatFlags = +littleEndian;
|
|
return fullBox("pcmC", 0, 0, [
|
|
u8(formatFlags),
|
|
u8(8 * sampleSize)
|
|
]);
|
|
};
|
|
var dac3 = (trackData) => {
|
|
const frameInfo = parseAc3SyncFrame(trackData.info.firstPacket.data);
|
|
if (!frameInfo) {
|
|
throw new Error("Couldn't extract AC-3 frame info from the audio packet. Ensure the packets contain valid AC-3 sync frames (as specified in ETSI TS 102 366).");
|
|
}
|
|
const bytes2 = new Uint8Array(3);
|
|
const bitstream = new Bitstream(bytes2);
|
|
bitstream.writeBits(2, frameInfo.fscod);
|
|
bitstream.writeBits(5, frameInfo.bsid);
|
|
bitstream.writeBits(3, frameInfo.bsmod);
|
|
bitstream.writeBits(3, frameInfo.acmod);
|
|
bitstream.writeBits(1, frameInfo.lfeon);
|
|
bitstream.writeBits(5, frameInfo.bitRateCode);
|
|
bitstream.writeBits(5, 0);
|
|
return box("dac3", [...bytes2]);
|
|
};
|
|
var dec3 = (trackData) => {
|
|
const frameInfo = parseEac3SyncFrame(trackData.info.firstPacket.data);
|
|
if (!frameInfo) {
|
|
throw new Error("Couldn't extract E-AC-3 frame info from the audio packet. Ensure the packets contain valid E-AC-3 sync frames (as specified in ETSI TS 102 366).");
|
|
}
|
|
let totalBits = 16;
|
|
for (const sub of frameInfo.substreams) {
|
|
totalBits += 23;
|
|
if (sub.numDepSub > 0) {
|
|
totalBits += 9;
|
|
} else {
|
|
totalBits += 1;
|
|
}
|
|
}
|
|
const size = Math.ceil(totalBits / 8);
|
|
const bytes2 = new Uint8Array(size);
|
|
const bitstream = new Bitstream(bytes2);
|
|
bitstream.writeBits(13, frameInfo.dataRate);
|
|
bitstream.writeBits(3, frameInfo.substreams.length - 1);
|
|
for (const sub of frameInfo.substreams) {
|
|
bitstream.writeBits(2, sub.fscod);
|
|
bitstream.writeBits(5, sub.bsid);
|
|
bitstream.writeBits(1, 0);
|
|
bitstream.writeBits(1, 0);
|
|
bitstream.writeBits(3, sub.bsmod);
|
|
bitstream.writeBits(3, sub.acmod);
|
|
bitstream.writeBits(1, sub.lfeon);
|
|
bitstream.writeBits(3, 0);
|
|
bitstream.writeBits(4, sub.numDepSub);
|
|
if (sub.numDepSub > 0) {
|
|
bitstream.writeBits(9, sub.chanLoc);
|
|
} else {
|
|
bitstream.writeBits(1, 0);
|
|
}
|
|
}
|
|
return box("dec3", [...bytes2]);
|
|
};
|
|
var subtitleSampleDescription = (compressionType, trackData) => box(compressionType, [
|
|
Array(6).fill(0),
|
|
// Reserved
|
|
u16(1)
|
|
// Data reference index
|
|
], [
|
|
SUBTITLE_CODEC_TO_CONFIGURATION_BOX[trackData.track.source._codec](trackData)
|
|
]);
|
|
var vttC = (trackData) => box("vttC", [
|
|
...textEncoder.encode(trackData.info.config.description)
|
|
]);
|
|
var stts = (trackData) => {
|
|
return fullBox("stts", 0, 0, [
|
|
u32(trackData.timeToSampleTable.length),
|
|
// Number of entries
|
|
trackData.timeToSampleTable.map((x) => [
|
|
u32(x.sampleCount),
|
|
// Sample count
|
|
u32(x.sampleDelta)
|
|
// Sample duration
|
|
])
|
|
]);
|
|
};
|
|
var stss = (trackData) => {
|
|
if (trackData.samples.every((x) => x.type === "key"))
|
|
return null;
|
|
const keySamples = [...trackData.samples.entries()].filter(([, sample]) => sample.type === "key");
|
|
return fullBox("stss", 0, 0, [
|
|
u32(keySamples.length),
|
|
// Number of entries
|
|
keySamples.map(([index]) => u32(index + 1))
|
|
// Sync sample table
|
|
]);
|
|
};
|
|
var stsc = (trackData) => {
|
|
return fullBox("stsc", 0, 0, [
|
|
u32(trackData.compactlyCodedChunkTable.length),
|
|
// Number of entries
|
|
trackData.compactlyCodedChunkTable.map((x) => [
|
|
u32(x.firstChunk),
|
|
// First chunk
|
|
u32(x.samplesPerChunk),
|
|
// Samples per chunk
|
|
u32(1)
|
|
// Sample description index
|
|
])
|
|
]);
|
|
};
|
|
var stsz = (trackData) => {
|
|
if (trackData.type === "audio" && trackData.info.requiresPcmTransformation) {
|
|
const { sampleSize } = parsePcmCodec(trackData.track.source._codec);
|
|
return fullBox("stsz", 0, 0, [
|
|
u32(sampleSize * trackData.info.numberOfChannels),
|
|
// Sample size
|
|
u32(trackData.samples.reduce((acc, x) => acc + intoTimescale(x.duration, trackData.timescale), 0))
|
|
]);
|
|
}
|
|
return fullBox("stsz", 0, 0, [
|
|
u32(0),
|
|
// Sample size (0 means non-constant size)
|
|
u32(trackData.samples.length),
|
|
// Number of entries
|
|
trackData.samples.map((x) => u32(x.size))
|
|
// Sample size table
|
|
]);
|
|
};
|
|
var stco = (trackData) => {
|
|
if (trackData.finalizedChunks.length > 0 && last(trackData.finalizedChunks).offset >= 2 ** 32) {
|
|
return fullBox("co64", 0, 0, [
|
|
u32(trackData.finalizedChunks.length),
|
|
// Number of entries
|
|
trackData.finalizedChunks.map((x) => u64(x.offset))
|
|
// Chunk offset table
|
|
]);
|
|
}
|
|
return fullBox("stco", 0, 0, [
|
|
u32(trackData.finalizedChunks.length),
|
|
// Number of entries
|
|
trackData.finalizedChunks.map((x) => u32(x.offset))
|
|
// Chunk offset table
|
|
]);
|
|
};
|
|
var ctts = (trackData) => {
|
|
return fullBox("ctts", 1, 0, [
|
|
u32(trackData.compositionTimeOffsetTable.length),
|
|
// Number of entries
|
|
trackData.compositionTimeOffsetTable.map((x) => [
|
|
u32(x.sampleCount),
|
|
// Sample count
|
|
i32(x.sampleCompositionTimeOffset)
|
|
// Sample offset
|
|
])
|
|
]);
|
|
};
|
|
var cslg = (trackData) => {
|
|
let leastDecodeToDisplayDelta = Infinity;
|
|
let greatestDecodeToDisplayDelta = -Infinity;
|
|
let compositionStartTime = Infinity;
|
|
let compositionEndTime = -Infinity;
|
|
assert(trackData.compositionTimeOffsetTable.length > 0);
|
|
assert(trackData.samples.length > 0);
|
|
for (let i2 = 0; i2 < trackData.compositionTimeOffsetTable.length; i2++) {
|
|
const entry = trackData.compositionTimeOffsetTable[i2];
|
|
leastDecodeToDisplayDelta = Math.min(leastDecodeToDisplayDelta, entry.sampleCompositionTimeOffset);
|
|
greatestDecodeToDisplayDelta = Math.max(greatestDecodeToDisplayDelta, entry.sampleCompositionTimeOffset);
|
|
}
|
|
for (let i2 = 0; i2 < trackData.samples.length; i2++) {
|
|
const sample = trackData.samples[i2];
|
|
compositionStartTime = Math.min(compositionStartTime, intoTimescale(sample.timestamp, trackData.timescale));
|
|
compositionEndTime = Math.max(compositionEndTime, intoTimescale(sample.timestamp + sample.duration, trackData.timescale));
|
|
}
|
|
const compositionToDtsShift = Math.max(-leastDecodeToDisplayDelta, 0);
|
|
if (compositionEndTime >= 2 ** 31) {
|
|
return null;
|
|
}
|
|
return fullBox("cslg", 0, 0, [
|
|
i32(compositionToDtsShift),
|
|
// Composition to DTS shift
|
|
i32(leastDecodeToDisplayDelta),
|
|
// Least decode to display delta
|
|
i32(greatestDecodeToDisplayDelta),
|
|
// Greatest decode to display delta
|
|
i32(compositionStartTime),
|
|
// Composition start time
|
|
i32(compositionEndTime)
|
|
// Composition end time
|
|
]);
|
|
};
|
|
var mvex = (trackDatas) => {
|
|
return box("mvex", void 0, trackDatas.map(trex));
|
|
};
|
|
var trex = (trackData) => {
|
|
return fullBox("trex", 0, 0, [
|
|
u32(trackData.track.id),
|
|
// Track ID
|
|
u32(1),
|
|
// Default sample description index
|
|
u32(0),
|
|
// Default sample duration
|
|
u32(0),
|
|
// Default sample size
|
|
u32(0)
|
|
// Default sample flags
|
|
]);
|
|
};
|
|
var moof = (sequenceNumber, trackDatas) => {
|
|
return box("moof", void 0, [
|
|
mfhd(sequenceNumber),
|
|
...trackDatas.map(traf)
|
|
]);
|
|
};
|
|
var mfhd = (sequenceNumber) => {
|
|
return fullBox("mfhd", 0, 0, [
|
|
u32(sequenceNumber)
|
|
// Sequence number
|
|
]);
|
|
};
|
|
var fragmentSampleFlags = (sample) => {
|
|
let byte1 = 0;
|
|
let byte2 = 0;
|
|
const byte3 = 0;
|
|
const byte4 = 0;
|
|
const sampleIsDifferenceSample = sample.type === "delta";
|
|
byte2 |= +sampleIsDifferenceSample;
|
|
if (sampleIsDifferenceSample) {
|
|
byte1 |= 1;
|
|
} else {
|
|
byte1 |= 2;
|
|
}
|
|
return byte1 << 24 | byte2 << 16 | byte3 << 8 | byte4;
|
|
};
|
|
var traf = (trackData) => {
|
|
return box("traf", void 0, [
|
|
tfhd(trackData),
|
|
tfdt(trackData),
|
|
trun(trackData)
|
|
]);
|
|
};
|
|
var tfhd = (trackData) => {
|
|
assert(trackData.currentChunk);
|
|
let tfFlags = 0;
|
|
tfFlags |= 8;
|
|
tfFlags |= 16;
|
|
tfFlags |= 32;
|
|
tfFlags |= 131072;
|
|
const referenceSample = trackData.currentChunk.samples[1] ?? trackData.currentChunk.samples[0];
|
|
const referenceSampleInfo = {
|
|
duration: referenceSample.timescaleUnitsToNextSample,
|
|
size: referenceSample.size,
|
|
flags: fragmentSampleFlags(referenceSample)
|
|
};
|
|
return fullBox("tfhd", 0, tfFlags, [
|
|
u32(trackData.track.id),
|
|
// Track ID
|
|
u32(referenceSampleInfo.duration),
|
|
// Default sample duration
|
|
u32(referenceSampleInfo.size),
|
|
// Default sample size
|
|
u32(referenceSampleInfo.flags)
|
|
// Default sample flags
|
|
]);
|
|
};
|
|
var tfdt = (trackData) => {
|
|
assert(trackData.currentChunk);
|
|
return fullBox("tfdt", 1, 0, [
|
|
u64(intoTimescale(trackData.currentChunk.startTimestamp, trackData.timescale))
|
|
// Base Media Decode Time
|
|
]);
|
|
};
|
|
var trun = (trackData) => {
|
|
assert(trackData.currentChunk);
|
|
const allSampleDurations = trackData.currentChunk.samples.map((x) => x.timescaleUnitsToNextSample);
|
|
const allSampleSizes = trackData.currentChunk.samples.map((x) => x.size);
|
|
const allSampleFlags = trackData.currentChunk.samples.map(fragmentSampleFlags);
|
|
const allSampleCompositionTimeOffsets = trackData.currentChunk.samples.map((x) => intoTimescale(x.timestamp - x.decodeTimestamp, trackData.timescale));
|
|
const uniqueSampleDurations = new Set(allSampleDurations);
|
|
const uniqueSampleSizes = new Set(allSampleSizes);
|
|
const uniqueSampleFlags = new Set(allSampleFlags);
|
|
const uniqueSampleCompositionTimeOffsets = new Set(allSampleCompositionTimeOffsets);
|
|
const firstSampleFlagsPresent = uniqueSampleFlags.size === 2 && allSampleFlags[0] !== allSampleFlags[1];
|
|
const sampleDurationPresent = uniqueSampleDurations.size > 1;
|
|
const sampleSizePresent = uniqueSampleSizes.size > 1;
|
|
const sampleFlagsPresent = !firstSampleFlagsPresent && uniqueSampleFlags.size > 1;
|
|
const sampleCompositionTimeOffsetsPresent = uniqueSampleCompositionTimeOffsets.size > 1 || [...uniqueSampleCompositionTimeOffsets].some((x) => x !== 0);
|
|
let flags = 0;
|
|
flags |= 1;
|
|
flags |= 4 * +firstSampleFlagsPresent;
|
|
flags |= 256 * +sampleDurationPresent;
|
|
flags |= 512 * +sampleSizePresent;
|
|
flags |= 1024 * +sampleFlagsPresent;
|
|
flags |= 2048 * +sampleCompositionTimeOffsetsPresent;
|
|
return fullBox("trun", 1, flags, [
|
|
u32(trackData.currentChunk.samples.length),
|
|
// Sample count
|
|
u32(trackData.currentChunk.offset - trackData.currentChunk.moofOffset || 0),
|
|
// Data offset
|
|
firstSampleFlagsPresent ? u32(allSampleFlags[0]) : [],
|
|
trackData.currentChunk.samples.map((_2, i2) => [
|
|
sampleDurationPresent ? u32(allSampleDurations[i2]) : [],
|
|
// Sample duration
|
|
sampleSizePresent ? u32(allSampleSizes[i2]) : [],
|
|
// Sample size
|
|
sampleFlagsPresent ? u32(allSampleFlags[i2]) : [],
|
|
// Sample flags
|
|
// Sample composition time offsets
|
|
sampleCompositionTimeOffsetsPresent ? i32(allSampleCompositionTimeOffsets[i2]) : []
|
|
])
|
|
]);
|
|
};
|
|
var mfra = (trackDatas) => {
|
|
return box("mfra", void 0, [
|
|
...trackDatas.map(tfra),
|
|
mfro()
|
|
]);
|
|
};
|
|
var tfra = (trackData, trackIndex) => {
|
|
const version = 1;
|
|
return fullBox("tfra", version, 0, [
|
|
u32(trackData.track.id),
|
|
// Track ID
|
|
u32(63),
|
|
// This specifies that traf number, trun number and sample number are 32-bit ints
|
|
u32(trackData.finalizedChunks.length),
|
|
// Number of entries
|
|
trackData.finalizedChunks.map((chunk) => [
|
|
u64(intoTimescale(chunk.samples[0].timestamp, trackData.timescale)),
|
|
// Time (in presentation time)
|
|
u64(chunk.moofOffset),
|
|
// moof offset
|
|
u32(trackIndex + 1),
|
|
// traf number
|
|
u32(1),
|
|
// trun number
|
|
u32(1)
|
|
// Sample number
|
|
])
|
|
]);
|
|
};
|
|
var mfro = () => {
|
|
return fullBox("mfro", 0, 0, [
|
|
// This value needs to be overwritten manually from the outside, where the actual size of the enclosing mfra box
|
|
// is known
|
|
u32(0)
|
|
// Size
|
|
]);
|
|
};
|
|
var vtte = () => box("vtte");
|
|
var vttc = (payload, timestamp, identifier, settings, sourceId) => box("vttc", void 0, [
|
|
sourceId !== null ? box("vsid", [i32(sourceId)]) : null,
|
|
identifier !== null ? box("iden", [...textEncoder.encode(identifier)]) : null,
|
|
timestamp !== null ? box("ctim", [...textEncoder.encode(formatSubtitleTimestamp(timestamp))]) : null,
|
|
settings !== null ? box("sttg", [...textEncoder.encode(settings)]) : null,
|
|
box("payl", [...textEncoder.encode(payload)])
|
|
]);
|
|
var vtta = (notes) => box("vtta", [...textEncoder.encode(notes)]);
|
|
var udta = (muxer) => {
|
|
const boxes = [];
|
|
const metadataFormat = muxer.format._options.metadataFormat ?? "auto";
|
|
const metadataTags = muxer.output._metadataTags;
|
|
if (metadataFormat === "mdir" || metadataFormat === "auto" && !muxer.isQuickTime) {
|
|
const metaBox = metaMdir(metadataTags);
|
|
if (metaBox)
|
|
boxes.push(metaBox);
|
|
} else if (metadataFormat === "mdta") {
|
|
const metaBox = metaMdta(metadataTags);
|
|
if (metaBox)
|
|
boxes.push(metaBox);
|
|
} else if (metadataFormat === "udta" || metadataFormat === "auto" && muxer.isQuickTime) {
|
|
addQuickTimeMetadataTagBoxes(boxes, muxer.output._metadataTags);
|
|
}
|
|
if (boxes.length === 0) {
|
|
return null;
|
|
}
|
|
return box("udta", void 0, boxes);
|
|
};
|
|
var addQuickTimeMetadataTagBoxes = (boxes, tags) => {
|
|
for (const { key, value } of keyValueIterator(tags)) {
|
|
switch (key) {
|
|
case "title":
|
|
{
|
|
boxes.push(metadataTagStringBoxShort("©nam", value));
|
|
}
|
|
;
|
|
break;
|
|
case "description":
|
|
{
|
|
boxes.push(metadataTagStringBoxShort("©des", value));
|
|
}
|
|
;
|
|
break;
|
|
case "artist":
|
|
{
|
|
boxes.push(metadataTagStringBoxShort("©ART", value));
|
|
}
|
|
;
|
|
break;
|
|
case "album":
|
|
{
|
|
boxes.push(metadataTagStringBoxShort("©alb", value));
|
|
}
|
|
;
|
|
break;
|
|
case "albumArtist":
|
|
{
|
|
boxes.push(metadataTagStringBoxShort("albr", value));
|
|
}
|
|
;
|
|
break;
|
|
case "genre":
|
|
{
|
|
boxes.push(metadataTagStringBoxShort("©gen", value));
|
|
}
|
|
;
|
|
break;
|
|
case "date":
|
|
{
|
|
boxes.push(metadataTagStringBoxShort("©day", value.toISOString().slice(0, 10)));
|
|
}
|
|
;
|
|
break;
|
|
case "comment":
|
|
{
|
|
boxes.push(metadataTagStringBoxShort("©cmt", value));
|
|
}
|
|
;
|
|
break;
|
|
case "lyrics":
|
|
{
|
|
boxes.push(metadataTagStringBoxShort("©lyr", value));
|
|
}
|
|
;
|
|
break;
|
|
case "raw":
|
|
{
|
|
}
|
|
;
|
|
break;
|
|
case "discNumber":
|
|
case "discsTotal":
|
|
case "trackNumber":
|
|
case "tracksTotal":
|
|
case "images":
|
|
{
|
|
}
|
|
;
|
|
break;
|
|
default:
|
|
assertNever(key);
|
|
}
|
|
}
|
|
if (tags.raw) {
|
|
for (const key in tags.raw) {
|
|
const value = tags.raw[key];
|
|
if (value == null || key.length !== 4 || boxes.some((x) => x.type === key)) {
|
|
continue;
|
|
}
|
|
if (typeof value === "string") {
|
|
boxes.push(metadataTagStringBoxShort(key, value));
|
|
} else if (value instanceof Uint8Array) {
|
|
boxes.push(box(key, Array.from(value)));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
var metadataTagStringBoxShort = (name, value) => {
|
|
const encoded = textEncoder.encode(value);
|
|
return box(name, [
|
|
u16(encoded.length),
|
|
u16(getLanguageCodeInt("und")),
|
|
Array.from(encoded)
|
|
]);
|
|
};
|
|
var DATA_BOX_MIME_TYPE_MAP = {
|
|
"image/jpeg": 13,
|
|
"image/png": 14,
|
|
"image/bmp": 27
|
|
};
|
|
var generateMetadataPairs = (tags, isMdta) => {
|
|
const pairs = [];
|
|
for (const { key, value } of keyValueIterator(tags)) {
|
|
switch (key) {
|
|
case "title":
|
|
{
|
|
pairs.push({ key: isMdta ? "title" : "©nam", value: dataStringBoxLong(value) });
|
|
}
|
|
;
|
|
break;
|
|
case "description":
|
|
{
|
|
pairs.push({ key: isMdta ? "description" : "©des", value: dataStringBoxLong(value) });
|
|
}
|
|
;
|
|
break;
|
|
case "artist":
|
|
{
|
|
pairs.push({ key: isMdta ? "artist" : "©ART", value: dataStringBoxLong(value) });
|
|
}
|
|
;
|
|
break;
|
|
case "album":
|
|
{
|
|
pairs.push({ key: isMdta ? "album" : "©alb", value: dataStringBoxLong(value) });
|
|
}
|
|
;
|
|
break;
|
|
case "albumArtist":
|
|
{
|
|
pairs.push({ key: isMdta ? "album_artist" : "aART", value: dataStringBoxLong(value) });
|
|
}
|
|
;
|
|
break;
|
|
case "comment":
|
|
{
|
|
pairs.push({ key: isMdta ? "comment" : "©cmt", value: dataStringBoxLong(value) });
|
|
}
|
|
;
|
|
break;
|
|
case "genre":
|
|
{
|
|
pairs.push({ key: isMdta ? "genre" : "©gen", value: dataStringBoxLong(value) });
|
|
}
|
|
;
|
|
break;
|
|
case "lyrics":
|
|
{
|
|
pairs.push({ key: isMdta ? "lyrics" : "©lyr", value: dataStringBoxLong(value) });
|
|
}
|
|
;
|
|
break;
|
|
case "date":
|
|
{
|
|
pairs.push({
|
|
key: isMdta ? "date" : "©day",
|
|
value: dataStringBoxLong(value.toISOString().slice(0, 10))
|
|
});
|
|
}
|
|
;
|
|
break;
|
|
case "images":
|
|
{
|
|
for (const image of value) {
|
|
if (image.kind !== "coverFront") {
|
|
continue;
|
|
}
|
|
pairs.push({ key: "covr", value: box("data", [
|
|
u32(DATA_BOX_MIME_TYPE_MAP[image.mimeType] ?? 0),
|
|
// Type indicator
|
|
u32(0),
|
|
// Locale indicator
|
|
Array.from(image.data)
|
|
// Kinda slow, hopefully temp
|
|
]) });
|
|
}
|
|
}
|
|
;
|
|
break;
|
|
case "trackNumber":
|
|
{
|
|
if (isMdta) {
|
|
const string = tags.tracksTotal !== void 0 ? \`\${value}/\${tags.tracksTotal}\` : value.toString();
|
|
pairs.push({ key: "track", value: dataStringBoxLong(string) });
|
|
} else {
|
|
pairs.push({ key: "trkn", value: box("data", [
|
|
u32(0),
|
|
// 8 bytes empty
|
|
u32(0),
|
|
u16(0),
|
|
// Empty
|
|
u16(value),
|
|
u16(tags.tracksTotal ?? 0),
|
|
u16(0)
|
|
// Empty
|
|
]) });
|
|
}
|
|
}
|
|
;
|
|
break;
|
|
case "discNumber":
|
|
{
|
|
if (!isMdta) {
|
|
pairs.push({ key: "disc", value: box("data", [
|
|
u32(0),
|
|
// 8 bytes empty
|
|
u32(0),
|
|
u16(0),
|
|
// Empty
|
|
u16(value),
|
|
u16(tags.discsTotal ?? 0),
|
|
u16(0)
|
|
// Empty
|
|
]) });
|
|
}
|
|
}
|
|
;
|
|
break;
|
|
case "tracksTotal":
|
|
case "discsTotal":
|
|
{
|
|
}
|
|
;
|
|
break;
|
|
case "raw":
|
|
{
|
|
}
|
|
;
|
|
break;
|
|
default:
|
|
assertNever(key);
|
|
}
|
|
}
|
|
if (tags.raw) {
|
|
for (const key in tags.raw) {
|
|
const value = tags.raw[key];
|
|
if (value == null || !isMdta && key.length !== 4 || pairs.some((x) => x.key === key)) {
|
|
continue;
|
|
}
|
|
if (typeof value === "string") {
|
|
pairs.push({ key, value: dataStringBoxLong(value) });
|
|
} else if (value instanceof Uint8Array) {
|
|
pairs.push({ key, value: box("data", [
|
|
u32(0),
|
|
// Type indicator
|
|
u32(0),
|
|
// Locale indicator
|
|
Array.from(value)
|
|
]) });
|
|
} else if (value instanceof RichImageData) {
|
|
pairs.push({ key, value: box("data", [
|
|
u32(DATA_BOX_MIME_TYPE_MAP[value.mimeType] ?? 0),
|
|
// Type indicator
|
|
u32(0),
|
|
// Locale indicator
|
|
Array.from(value.data)
|
|
// Kinda slow, hopefully temp
|
|
]) });
|
|
}
|
|
}
|
|
}
|
|
return pairs;
|
|
};
|
|
var metaMdir = (tags) => {
|
|
const pairs = generateMetadataPairs(tags, false);
|
|
if (pairs.length === 0) {
|
|
return null;
|
|
}
|
|
return fullBox("meta", 0, 0, void 0, [
|
|
hdlr(false, "mdir", "", "appl"),
|
|
// mdir handler
|
|
box("ilst", void 0, pairs.map((pair) => box(pair.key, void 0, [pair.value])))
|
|
// Item list without keys box
|
|
]);
|
|
};
|
|
var metaMdta = (tags) => {
|
|
const pairs = generateMetadataPairs(tags, true);
|
|
if (pairs.length === 0) {
|
|
return null;
|
|
}
|
|
return box("meta", void 0, [
|
|
hdlr(false, "mdta", ""),
|
|
// mdta handler
|
|
fullBox("keys", 0, 0, [
|
|
u32(pairs.length)
|
|
], pairs.map((pair) => box("mdta", [
|
|
...textEncoder.encode(pair.key)
|
|
]))),
|
|
box("ilst", void 0, pairs.map((pair, i2) => {
|
|
const boxName = String.fromCharCode(...u32(i2 + 1));
|
|
return box(boxName, void 0, [pair.value]);
|
|
}))
|
|
]);
|
|
};
|
|
var dataStringBoxLong = (value) => {
|
|
return box("data", [
|
|
u32(1),
|
|
// Type indicator (UTF-8)
|
|
u32(0),
|
|
// Locale indicator
|
|
...textEncoder.encode(value)
|
|
]);
|
|
};
|
|
var videoCodecToBoxName = (codec, fullCodecString) => {
|
|
switch (codec) {
|
|
case "avc":
|
|
return fullCodecString.startsWith("avc3") ? "avc3" : "avc1";
|
|
case "hevc":
|
|
return "hvc1";
|
|
case "vp8":
|
|
return "vp08";
|
|
case "vp9":
|
|
return "vp09";
|
|
case "av1":
|
|
return "av01";
|
|
}
|
|
};
|
|
var VIDEO_CODEC_TO_CONFIGURATION_BOX = {
|
|
avc: avcC,
|
|
hevc: hvcC,
|
|
vp8: vpcC,
|
|
vp9: vpcC,
|
|
av1: av1C
|
|
};
|
|
var audioCodecToBoxName = (codec, isQuickTime) => {
|
|
switch (codec) {
|
|
case "aac":
|
|
return "mp4a";
|
|
case "mp3":
|
|
return "mp4a";
|
|
case "opus":
|
|
return "Opus";
|
|
case "vorbis":
|
|
return "mp4a";
|
|
case "flac":
|
|
return "fLaC";
|
|
case "ulaw":
|
|
return "ulaw";
|
|
case "alaw":
|
|
return "alaw";
|
|
case "pcm-u8":
|
|
return "raw ";
|
|
case "pcm-s8":
|
|
return "sowt";
|
|
case "ac3":
|
|
return "ac-3";
|
|
case "eac3":
|
|
return "ec-3";
|
|
}
|
|
if (isQuickTime) {
|
|
switch (codec) {
|
|
case "pcm-s16":
|
|
return "sowt";
|
|
case "pcm-s16be":
|
|
return "twos";
|
|
case "pcm-s24":
|
|
return "in24";
|
|
case "pcm-s24be":
|
|
return "in24";
|
|
case "pcm-s32":
|
|
return "in32";
|
|
case "pcm-s32be":
|
|
return "in32";
|
|
case "pcm-f32":
|
|
return "fl32";
|
|
case "pcm-f32be":
|
|
return "fl32";
|
|
case "pcm-f64":
|
|
return "fl64";
|
|
case "pcm-f64be":
|
|
return "fl64";
|
|
}
|
|
} else {
|
|
switch (codec) {
|
|
case "pcm-s16":
|
|
return "ipcm";
|
|
case "pcm-s16be":
|
|
return "ipcm";
|
|
case "pcm-s24":
|
|
return "ipcm";
|
|
case "pcm-s24be":
|
|
return "ipcm";
|
|
case "pcm-s32":
|
|
return "ipcm";
|
|
case "pcm-s32be":
|
|
return "ipcm";
|
|
case "pcm-f32":
|
|
return "fpcm";
|
|
case "pcm-f32be":
|
|
return "fpcm";
|
|
case "pcm-f64":
|
|
return "fpcm";
|
|
case "pcm-f64be":
|
|
return "fpcm";
|
|
}
|
|
}
|
|
};
|
|
var audioCodecToConfigurationBox = (codec, isQuickTime) => {
|
|
switch (codec) {
|
|
case "aac":
|
|
return esds;
|
|
case "mp3":
|
|
return esds;
|
|
case "opus":
|
|
return dOps;
|
|
case "vorbis":
|
|
return esds;
|
|
case "flac":
|
|
return dfLa;
|
|
case "ac3":
|
|
return dac3;
|
|
case "eac3":
|
|
return dec3;
|
|
}
|
|
if (isQuickTime) {
|
|
switch (codec) {
|
|
case "pcm-s24":
|
|
return wave;
|
|
case "pcm-s24be":
|
|
return wave;
|
|
case "pcm-s32":
|
|
return wave;
|
|
case "pcm-s32be":
|
|
return wave;
|
|
case "pcm-f32":
|
|
return wave;
|
|
case "pcm-f32be":
|
|
return wave;
|
|
case "pcm-f64":
|
|
return wave;
|
|
case "pcm-f64be":
|
|
return wave;
|
|
}
|
|
} else {
|
|
switch (codec) {
|
|
case "pcm-s16":
|
|
return pcmC;
|
|
case "pcm-s16be":
|
|
return pcmC;
|
|
case "pcm-s24":
|
|
return pcmC;
|
|
case "pcm-s24be":
|
|
return pcmC;
|
|
case "pcm-s32":
|
|
return pcmC;
|
|
case "pcm-s32be":
|
|
return pcmC;
|
|
case "pcm-f32":
|
|
return pcmC;
|
|
case "pcm-f32be":
|
|
return pcmC;
|
|
case "pcm-f64":
|
|
return pcmC;
|
|
case "pcm-f64be":
|
|
return pcmC;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
var SUBTITLE_CODEC_TO_BOX_NAME = {
|
|
webvtt: "wvtt"
|
|
};
|
|
var SUBTITLE_CODEC_TO_CONFIGURATION_BOX = {
|
|
webvtt: vttC
|
|
};
|
|
var getLanguageCodeInt = (code) => {
|
|
assert(code.length === 3);
|
|
;
|
|
let language = 0;
|
|
for (let i2 = 0; i2 < 3; i2++) {
|
|
language <<= 5;
|
|
language += code.charCodeAt(i2) - 96;
|
|
}
|
|
return language;
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/writer.js
|
|
var Writer = class {
|
|
constructor(target, isMonotonic) {
|
|
this.finalized = false;
|
|
this.started = false;
|
|
this.pos = 0;
|
|
this.trackedWrites = null;
|
|
this.trackedStart = -1;
|
|
this.trackedEnd = -1;
|
|
if (target._writerAcquired) {
|
|
throw new Error("Can't have multiple Writers for the same Target.");
|
|
}
|
|
this.target = target;
|
|
target._setMonotonicity(isMonotonic);
|
|
target._writerAcquired = true;
|
|
}
|
|
start() {
|
|
assert(!this.started);
|
|
this.target._start();
|
|
this.started = true;
|
|
}
|
|
/** Writes the given data to the target, at the current position. */
|
|
write(data) {
|
|
assert(this.started && !this.finalized);
|
|
this.maybeTrackWrites(data);
|
|
this.target._write(data, this.pos);
|
|
this.pos += data.byteLength;
|
|
}
|
|
/** Sets the current position for future writes to a new one. */
|
|
seek(newPos) {
|
|
this.pos = newPos;
|
|
}
|
|
/** Returns the current position. */
|
|
getPos() {
|
|
return this.pos;
|
|
}
|
|
/** Signals to the writer that it may be time to flush. */
|
|
async flush() {
|
|
assert(this.started && !this.finalized);
|
|
return this.target._flush();
|
|
}
|
|
/** Called after muxing has finished. */
|
|
async finalize() {
|
|
assert(this.started && !this.finalized);
|
|
await this.target._finalize();
|
|
this.finalized = true;
|
|
}
|
|
maybeTrackWrites(data) {
|
|
if (!this.trackedWrites) {
|
|
return;
|
|
}
|
|
let pos = this.getPos();
|
|
if (pos < this.trackedStart) {
|
|
if (pos + data.byteLength <= this.trackedStart) {
|
|
return;
|
|
}
|
|
data = data.subarray(this.trackedStart - pos);
|
|
pos = 0;
|
|
}
|
|
const neededSize = pos + data.byteLength - this.trackedStart;
|
|
let newLength = this.trackedWrites.byteLength;
|
|
while (newLength < neededSize) {
|
|
newLength *= 2;
|
|
}
|
|
if (newLength !== this.trackedWrites.byteLength) {
|
|
const copy = new Uint8Array(newLength);
|
|
copy.set(this.trackedWrites, 0);
|
|
this.trackedWrites = copy;
|
|
}
|
|
this.trackedWrites.set(data, pos - this.trackedStart);
|
|
this.trackedEnd = Math.max(this.trackedEnd, pos + data.byteLength);
|
|
}
|
|
startTrackingWrites() {
|
|
this.trackedWrites = new Uint8Array(2 ** 10);
|
|
this.trackedStart = this.getPos();
|
|
this.trackedEnd = this.trackedStart;
|
|
}
|
|
stopTrackingWrites() {
|
|
if (!this.trackedWrites) {
|
|
throw new Error("Internal error: Can't get tracked writes since nothing was tracked.");
|
|
}
|
|
const slice = this.trackedWrites.subarray(0, this.trackedEnd - this.trackedStart);
|
|
const result = {
|
|
data: slice,
|
|
start: this.trackedStart,
|
|
end: this.trackedEnd
|
|
};
|
|
this.trackedWrites = null;
|
|
return result;
|
|
}
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/target.js
|
|
var Target = class extends EventEmitter {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._writerAcquired = false;
|
|
this._monotonicity = null;
|
|
this.onwrite = null;
|
|
}
|
|
/** @internal */
|
|
_setMonotonicity(monotonicity) {
|
|
if (this._monotonicity !== false) {
|
|
this._monotonicity = monotonicity;
|
|
} else {
|
|
}
|
|
}
|
|
/** @internal */
|
|
_dispatchWrite(start, end) {
|
|
this.onwrite?.(start, end);
|
|
this._emit("write", { start, end });
|
|
}
|
|
/**
|
|
* Returns a new {@link RangedTarget} that writes data to this target using the given offset.
|
|
*
|
|
* Useful for writing a file into a section of a larger file.
|
|
*/
|
|
slice(offset) {
|
|
if (!Number.isInteger(offset) || offset < 0) {
|
|
throw new TypeError("offset must be a non-negative integer.");
|
|
}
|
|
return new RangedTarget(this, offset);
|
|
}
|
|
};
|
|
var ARRAY_BUFFER_INITIAL_SIZE = 2 ** 16;
|
|
var ARRAY_BUFFER_MAX_SIZE = 2 ** 32;
|
|
var BufferTarget = class extends Target {
|
|
/** Creates a new {@link BufferTarget}. The buffer holding the data will be created and managed internally. */
|
|
constructor(options = {}) {
|
|
super();
|
|
this.buffer = null;
|
|
this._maxPos = 0;
|
|
if (!options || typeof options !== "object") {
|
|
throw new TypeError("BufferTarget options, when provided, must be an object.");
|
|
}
|
|
if (options.onFinalize !== void 0 && typeof options.onFinalize !== "function") {
|
|
throw new TypeError("options.onFinalize, when provided, must be a function.");
|
|
}
|
|
this._options = options;
|
|
this._supportsResize = "resize" in new ArrayBuffer(0);
|
|
if (this._supportsResize) {
|
|
try {
|
|
this._buffer = new ArrayBuffer(ARRAY_BUFFER_INITIAL_SIZE, { maxByteLength: ARRAY_BUFFER_MAX_SIZE });
|
|
} catch {
|
|
this._buffer = new ArrayBuffer(ARRAY_BUFFER_INITIAL_SIZE);
|
|
this._supportsResize = false;
|
|
}
|
|
} else {
|
|
this._buffer = new ArrayBuffer(ARRAY_BUFFER_INITIAL_SIZE);
|
|
}
|
|
this._bytes = new Uint8Array(this._buffer);
|
|
}
|
|
/** @internal */
|
|
_ensureSize(size) {
|
|
let newLength = this._buffer.byteLength;
|
|
while (newLength < size)
|
|
newLength *= 2;
|
|
if (newLength === this._buffer.byteLength)
|
|
return;
|
|
if (newLength > ARRAY_BUFFER_MAX_SIZE) {
|
|
throw new Error(\`ArrayBuffer exceeded maximum size of \${ARRAY_BUFFER_MAX_SIZE} bytes. Please consider using another target.\`);
|
|
}
|
|
if (this._supportsResize) {
|
|
this._buffer.resize(newLength);
|
|
} else {
|
|
const newBuffer = new ArrayBuffer(newLength);
|
|
const newBytes = new Uint8Array(newBuffer);
|
|
newBytes.set(this._bytes, 0);
|
|
this._buffer = newBuffer;
|
|
this._bytes = newBytes;
|
|
}
|
|
}
|
|
/** @internal */
|
|
_start() {
|
|
}
|
|
/** @internal */
|
|
_write(data, pos) {
|
|
this._ensureSize(pos + data.byteLength);
|
|
this._bytes.set(data, pos);
|
|
this._maxPos = Math.max(this._maxPos, pos + data.byteLength);
|
|
this._dispatchWrite(pos, pos + data.byteLength);
|
|
}
|
|
/** @internal */
|
|
async _flush() {
|
|
}
|
|
/** @internal */
|
|
async _finalize() {
|
|
this.buffer = this._buffer.slice(0, this._maxPos);
|
|
if (this._options.onFinalize) {
|
|
await this._options.onFinalize(this.buffer);
|
|
}
|
|
this._emit("finalized");
|
|
}
|
|
/** @internal */
|
|
async _close() {
|
|
}
|
|
/** @internal */
|
|
_getSlice(start, end) {
|
|
return this._bytes.slice(start, end);
|
|
}
|
|
};
|
|
var DEFAULT_CHUNK_SIZE = 2 ** 24;
|
|
var RangedTarget = class extends Target {
|
|
/** @internal */
|
|
constructor(baseTarget, offset) {
|
|
super();
|
|
this._baseTarget = baseTarget;
|
|
this._offset = offset;
|
|
}
|
|
/** @internal */
|
|
_start() {
|
|
}
|
|
/** @internal */
|
|
_write(data, pos) {
|
|
this._baseTarget._write(data, this._offset + pos);
|
|
this._dispatchWrite(pos, pos + data.byteLength);
|
|
}
|
|
/** @internal */
|
|
_flush() {
|
|
return this._baseTarget._flush();
|
|
}
|
|
/** @internal */
|
|
async _finalize() {
|
|
this._emit("finalized");
|
|
}
|
|
/** @internal */
|
|
async _close() {
|
|
}
|
|
/** @internal */
|
|
_setMonotonicity(monotonicity) {
|
|
super._setMonotonicity(monotonicity);
|
|
this._baseTarget._setMonotonicity(monotonicity);
|
|
}
|
|
};
|
|
var PathedTarget = class {
|
|
/** Creates a new {@link PathedTarget} from a root path and a callback. */
|
|
constructor(rootPath, getTarget) {
|
|
this.rootPath = rootPath;
|
|
this.getTarget = getTarget;
|
|
if (typeof rootPath !== "string") {
|
|
throw new TypeError("rootPath must be a string.");
|
|
}
|
|
if (typeof getTarget !== "function") {
|
|
throw new TypeError("getTarget must be a function.");
|
|
}
|
|
}
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/isobmff/isobmff-muxer.js
|
|
var GLOBAL_TIMESCALE = 57600;
|
|
var TIMESTAMP_OFFSET = 2082844800;
|
|
var getTrackMetadata = (trackData) => {
|
|
const metadata = {};
|
|
const track = trackData.track;
|
|
if (track.metadata.name !== void 0) {
|
|
metadata.name = track.metadata.name;
|
|
}
|
|
return metadata;
|
|
};
|
|
var intoTimescale = (timeInSeconds, timescale, round = true) => {
|
|
const value = timeInSeconds * timescale;
|
|
return round ? Math.round(value) : value;
|
|
};
|
|
var IsobmffMuxer = class extends Muxer {
|
|
constructor(output, format) {
|
|
super(output);
|
|
this.writer = null;
|
|
this.boxWriter = null;
|
|
this.initWriter = null;
|
|
this.initBoxWriter = null;
|
|
this.auxTarget = new BufferTarget();
|
|
this.auxWriter = new Writer(this.auxTarget, false);
|
|
this.auxBoxWriter = new IsobmffBoxWriter(this.auxWriter);
|
|
this.mdat = null;
|
|
this.ftypSize = null;
|
|
this.trackDatas = [];
|
|
this.allTracksKnown = promiseWithResolvers();
|
|
this.creationTime = Math.floor(Date.now() / 1e3) + TIMESTAMP_OFFSET;
|
|
this.finalizedChunks = [];
|
|
this.nextFragmentNumber = 1;
|
|
this.maxWrittenTimestamp = -Infinity;
|
|
this.minWrittenTimestamp = Infinity;
|
|
this.maxWrittenEndTimestamp = -Infinity;
|
|
this.segmentHeaderSize = null;
|
|
this.format = format;
|
|
this.isQuickTime = format instanceof MovOutputFormat;
|
|
this.isCmaf = format instanceof CmafOutputFormat;
|
|
this.minimumFragmentDuration = format._options.minimumFragmentDuration ?? (format instanceof CmafOutputFormat ? Infinity : 1);
|
|
}
|
|
async start() {
|
|
const release = await this.mutex.acquire();
|
|
if (!this.isCmaf) {
|
|
this.writer = await this.output._getRootWriter((target) => this.format._options.fastStart !== void 0 ? this.format._options.fastStart === "fragmented" : target instanceof BufferTarget);
|
|
this.boxWriter = new IsobmffBoxWriter(this.writer);
|
|
this.fastStart = this.format._options.fastStart ?? (this.writer.target instanceof BufferTarget ? "in-memory" : false);
|
|
this.isFragmented = this.fastStart === "fragmented";
|
|
} else {
|
|
this.fastStart = "fragmented";
|
|
this.isFragmented = true;
|
|
}
|
|
if (this.isCmaf) {
|
|
if (!this.output._hasInitTarget()) {
|
|
throw new Error(\`CMAF outputs require the initTarget field in OutputOptions to be set; the init segment will be written to it.\`);
|
|
}
|
|
const initTarget = await this.output._getInitTarget();
|
|
const initWriter = new Writer(initTarget, true);
|
|
initWriter.start();
|
|
this.initWriter = initWriter;
|
|
this.initBoxWriter = new IsobmffBoxWriter(initWriter);
|
|
}
|
|
const holdsAvc = this.output._tracks.some((x) => x.isVideoTrack() && x.source._codec === "avc");
|
|
{
|
|
const boxWriter = this.initBoxWriter ?? this.boxWriter;
|
|
assert(boxWriter);
|
|
if (this.format._options.onFtyp) {
|
|
boxWriter.writer.startTrackingWrites();
|
|
}
|
|
boxWriter.writeBox(ftyp({
|
|
isQuickTime: this.isQuickTime,
|
|
holdsAvc,
|
|
fragmented: this.isFragmented,
|
|
cmaf: this.isCmaf
|
|
}));
|
|
if (this.format._options.onFtyp) {
|
|
const { data, start } = boxWriter.writer.stopTrackingWrites();
|
|
this.format._options.onFtyp(data, start);
|
|
}
|
|
this.ftypSize = boxWriter.writer.getPos();
|
|
if (this.isCmaf) {
|
|
await this.initWriter.flush();
|
|
}
|
|
}
|
|
if (this.fastStart === "in-memory") {
|
|
} else if (this.fastStart === "reserve") {
|
|
for (const track of this.output._tracks) {
|
|
if (track.metadata.maximumPacketCount === void 0) {
|
|
throw new Error("All tracks must specify maximumPacketCount in their metadata when using fastStart: 'reserve'.");
|
|
}
|
|
}
|
|
} else if (this.isFragmented) {
|
|
} else {
|
|
assert(this.writer);
|
|
assert(this.boxWriter);
|
|
if (this.format._options.onMdat) {
|
|
this.writer.startTrackingWrites();
|
|
}
|
|
this.mdat = mdat(true);
|
|
this.boxWriter.writeBox(this.mdat);
|
|
}
|
|
await this.writer?.flush();
|
|
release();
|
|
}
|
|
allTracksAreKnown() {
|
|
for (const track of this.output._tracks) {
|
|
if (!track.source._closed && !this.trackDatas.some((x) => x.track === track)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
async getMimeType() {
|
|
await this.allTracksKnown.promise;
|
|
const codecStrings = this.trackDatas.map((trackData) => {
|
|
if (trackData.type === "video") {
|
|
return trackData.info.decoderConfig.codec;
|
|
} else if (trackData.type === "audio") {
|
|
return trackData.info.decoderConfig.codec;
|
|
} else {
|
|
const map = {
|
|
webvtt: "wvtt"
|
|
};
|
|
return map[trackData.track.source._codec];
|
|
}
|
|
});
|
|
return buildIsobmffMimeType({
|
|
isQuickTime: this.isQuickTime,
|
|
hasVideo: this.trackDatas.some((x) => x.type === "video"),
|
|
hasAudio: this.trackDatas.some((x) => x.type === "audio"),
|
|
codecStrings
|
|
});
|
|
}
|
|
getVideoTrackData(track, packet, meta) {
|
|
const existingTrackData = this.trackDatas.find((x) => x.track === track);
|
|
if (existingTrackData) {
|
|
return existingTrackData;
|
|
}
|
|
validateVideoChunkMetadata(meta);
|
|
assert(meta);
|
|
assert(meta.decoderConfig);
|
|
const decoderConfig = { ...meta.decoderConfig };
|
|
assert(decoderConfig.codedWidth !== void 0);
|
|
assert(decoderConfig.codedHeight !== void 0);
|
|
let requiresAnnexBTransformation = false;
|
|
if (track.source._codec === "avc" && !decoderConfig.description) {
|
|
const decoderConfigurationRecord = extractAvcDecoderConfigurationRecord(packet.data);
|
|
if (!decoderConfigurationRecord) {
|
|
throw new Error("Couldn't extract an AVCDecoderConfigurationRecord from the AVC packet. Make sure the packets are in Annex B format (as specified in ITU-T-REC-H.264) when not providing a description, or provide a description (must be an AVCDecoderConfigurationRecord as specified in ISO 14496-15) and ensure the packets are in AVCC format.");
|
|
}
|
|
decoderConfig.description = serializeAvcDecoderConfigurationRecord(decoderConfigurationRecord);
|
|
requiresAnnexBTransformation = true;
|
|
} else if (track.source._codec === "hevc" && !decoderConfig.description) {
|
|
const decoderConfigurationRecord = extractHevcDecoderConfigurationRecord(packet.data);
|
|
if (!decoderConfigurationRecord) {
|
|
throw new Error("Couldn't extract an HEVCDecoderConfigurationRecord from the HEVC packet. Make sure the packets are in Annex B format (as specified in ITU-T-REC-H.265) when not providing a description, or provide a description (must be an HEVCDecoderConfigurationRecord as specified in ISO 14496-15) and ensure the packets are in HEVC format.");
|
|
}
|
|
decoderConfig.description = serializeHevcDecoderConfigurationRecord(decoderConfigurationRecord);
|
|
requiresAnnexBTransformation = true;
|
|
}
|
|
const timescale = computeRationalApproximation(1 / (track.metadata.frameRate ?? GLOBAL_TIMESCALE), 1e6).den;
|
|
const displayAspectWidth = decoderConfig.displayAspectWidth;
|
|
const displayAspectHeight = decoderConfig.displayAspectHeight;
|
|
const pixelAspectRatio = displayAspectWidth === void 0 || displayAspectHeight === void 0 ? { num: 1, den: 1 } : simplifyRational({
|
|
num: displayAspectWidth * decoderConfig.codedHeight,
|
|
den: displayAspectHeight * decoderConfig.codedWidth
|
|
});
|
|
const newTrackData = {
|
|
muxer: this,
|
|
track,
|
|
type: "video",
|
|
info: {
|
|
width: decoderConfig.codedWidth,
|
|
height: decoderConfig.codedHeight,
|
|
pixelAspectRatio,
|
|
decoderConfig,
|
|
requiresAnnexBTransformation
|
|
},
|
|
timescale,
|
|
samples: [],
|
|
sampleQueue: [],
|
|
timestampProcessingQueue: [],
|
|
timeToSampleTable: [],
|
|
compositionTimeOffsetTable: [],
|
|
lastTimescaleUnits: null,
|
|
lastSample: null,
|
|
startTimestampOffset: null,
|
|
finalizedChunks: [],
|
|
currentChunk: null,
|
|
compactlyCodedChunkTable: [],
|
|
closed: false
|
|
};
|
|
this.trackDatas.push(newTrackData);
|
|
this.trackDatas.sort((a2, b) => a2.track.id - b.track.id);
|
|
if (this.allTracksAreKnown()) {
|
|
this.allTracksKnown.resolve();
|
|
}
|
|
return newTrackData;
|
|
}
|
|
getAudioTrackData(track, packet, meta) {
|
|
const existingTrackData = this.trackDatas.find((x) => x.track === track);
|
|
if (existingTrackData) {
|
|
return existingTrackData;
|
|
}
|
|
validateAudioChunkMetadata(meta);
|
|
assert(meta);
|
|
assert(meta.decoderConfig);
|
|
const decoderConfig = { ...meta.decoderConfig };
|
|
let requiresAdtsStripping = false;
|
|
if (track.source._codec === "aac" && !decoderConfig.description) {
|
|
const adtsFrame = readAdtsFrameHeader(FileSlice.tempFromBytes(packet.data));
|
|
if (!adtsFrame) {
|
|
throw new Error("Couldn't parse ADTS header from the AAC packet. Make sure the packets are in ADTS format (as specified in ISO 13818-7) when not providing a description, or provide a description (must be an AudioSpecificConfig as specified in ISO 14496-3) and ensure the packets are raw AAC data.");
|
|
}
|
|
const sampleRate = aacFrequencyTable[adtsFrame.samplingFrequencyIndex];
|
|
const numberOfChannels = aacChannelMap[adtsFrame.channelConfiguration];
|
|
if (sampleRate === void 0 || numberOfChannels === void 0) {
|
|
throw new Error("Invalid ADTS frame header.");
|
|
}
|
|
decoderConfig.description = buildAacAudioSpecificConfig({
|
|
objectType: adtsFrame.objectType,
|
|
sampleRate,
|
|
numberOfChannels
|
|
});
|
|
requiresAdtsStripping = true;
|
|
}
|
|
const newTrackData = {
|
|
muxer: this,
|
|
track,
|
|
type: "audio",
|
|
info: {
|
|
numberOfChannels: meta.decoderConfig.numberOfChannels,
|
|
sampleRate: meta.decoderConfig.sampleRate,
|
|
decoderConfig,
|
|
requiresPcmTransformation: !this.isFragmented && PCM_AUDIO_CODECS.includes(track.source._codec),
|
|
expectedNextPcmPacketTimestamp: null,
|
|
requiresAdtsStripping,
|
|
firstPacket: packet
|
|
},
|
|
timescale: decoderConfig.sampleRate,
|
|
samples: [],
|
|
sampleQueue: [],
|
|
timestampProcessingQueue: [],
|
|
timeToSampleTable: [],
|
|
compositionTimeOffsetTable: [],
|
|
lastTimescaleUnits: null,
|
|
lastSample: null,
|
|
startTimestampOffset: null,
|
|
finalizedChunks: [],
|
|
currentChunk: null,
|
|
compactlyCodedChunkTable: [],
|
|
closed: false
|
|
};
|
|
this.trackDatas.push(newTrackData);
|
|
this.trackDatas.sort((a2, b) => a2.track.id - b.track.id);
|
|
if (this.allTracksAreKnown()) {
|
|
this.allTracksKnown.resolve();
|
|
}
|
|
return newTrackData;
|
|
}
|
|
getSubtitleTrackData(track, meta) {
|
|
const existingTrackData = this.trackDatas.find((x) => x.track === track);
|
|
if (existingTrackData) {
|
|
return existingTrackData;
|
|
}
|
|
validateSubtitleMetadata(meta);
|
|
assert(meta);
|
|
assert(meta.config);
|
|
const newTrackData = {
|
|
muxer: this,
|
|
track,
|
|
type: "subtitle",
|
|
info: {
|
|
config: meta.config
|
|
},
|
|
timescale: 1e3,
|
|
// Reasonable
|
|
samples: [],
|
|
sampleQueue: [],
|
|
timestampProcessingQueue: [],
|
|
timeToSampleTable: [],
|
|
compositionTimeOffsetTable: [],
|
|
lastTimescaleUnits: null,
|
|
lastSample: null,
|
|
startTimestampOffset: null,
|
|
finalizedChunks: [],
|
|
currentChunk: null,
|
|
compactlyCodedChunkTable: [],
|
|
closed: false,
|
|
lastCueEndTimestamp: 0,
|
|
cueQueue: [],
|
|
nextSourceId: 0,
|
|
cueToSourceId: /* @__PURE__ */ new WeakMap()
|
|
};
|
|
this.trackDatas.push(newTrackData);
|
|
this.trackDatas.sort((a2, b) => a2.track.id - b.track.id);
|
|
if (this.allTracksAreKnown()) {
|
|
this.allTracksKnown.resolve();
|
|
}
|
|
return newTrackData;
|
|
}
|
|
async addEncodedVideoPacket(track, packet, meta) {
|
|
const release = await this.mutex.acquire();
|
|
try {
|
|
const trackData = this.getVideoTrackData(track, packet, meta);
|
|
let packetData = packet.data;
|
|
if (trackData.info.requiresAnnexBTransformation) {
|
|
const nalUnits = [...iterateNalUnitsInAnnexB(packetData)].map((loc) => packetData.subarray(loc.offset, loc.offset + loc.length));
|
|
if (nalUnits.length === 0) {
|
|
throw new Error("Failed to transform packet data. Make sure all packets are provided in Annex B format, as specified in ITU-T-REC-H.264 and ITU-T-REC-H.265.");
|
|
}
|
|
packetData = concatNalUnitsInLengthPrefixed(nalUnits, 4);
|
|
}
|
|
this.validateTimestamp(trackData.track, packet.timestamp, packet.type === "key");
|
|
const internalSample = this.createSampleForTrack(trackData, packetData, packet.timestamp, packet.duration, packet.type);
|
|
await this.registerSample(trackData, internalSample);
|
|
} finally {
|
|
release();
|
|
}
|
|
}
|
|
async addEncodedAudioPacket(track, packet, meta) {
|
|
const release = await this.mutex.acquire();
|
|
try {
|
|
const trackData = this.getAudioTrackData(track, packet, meta);
|
|
let packetData = packet.data;
|
|
if (trackData.info.requiresAdtsStripping) {
|
|
const adtsFrame = readAdtsFrameHeader(FileSlice.tempFromBytes(packetData));
|
|
if (!adtsFrame) {
|
|
throw new Error("Expected ADTS frame, didn't get one.");
|
|
}
|
|
const headerLength = adtsFrame.crcCheck === null ? MIN_ADTS_FRAME_HEADER_SIZE : MAX_ADTS_FRAME_HEADER_SIZE;
|
|
packetData = packetData.subarray(headerLength);
|
|
}
|
|
this.validateTimestamp(trackData.track, packet.timestamp, packet.type === "key");
|
|
let timestamp = packet.timestamp;
|
|
let duration = packet.duration;
|
|
if (trackData.info.requiresPcmTransformation) {
|
|
const pcmInfo = parsePcmCodec(trackData.info.decoderConfig.codec);
|
|
const frameSize = pcmInfo.sampleSize * trackData.info.numberOfChannels;
|
|
duration = packetData.byteLength / frameSize / trackData.info.sampleRate;
|
|
if (trackData.info.expectedNextPcmPacketTimestamp !== null) {
|
|
const diff = timestamp - trackData.info.expectedNextPcmPacketTimestamp;
|
|
if (diff < 0.01) {
|
|
timestamp = trackData.info.expectedNextPcmPacketTimestamp;
|
|
} else {
|
|
const paddedDuration = await this.padWithSilence(trackData, trackData.info.expectedNextPcmPacketTimestamp, diff);
|
|
timestamp = trackData.info.expectedNextPcmPacketTimestamp + paddedDuration;
|
|
}
|
|
}
|
|
trackData.info.expectedNextPcmPacketTimestamp = timestamp + duration;
|
|
}
|
|
const internalSample = this.createSampleForTrack(trackData, packetData, timestamp, duration, packet.type);
|
|
await this.registerSample(trackData, internalSample);
|
|
} finally {
|
|
release();
|
|
}
|
|
}
|
|
async padWithSilence(trackData, timestamp, duration) {
|
|
const deltaInTimescale = intoTimescale(duration, trackData.timescale);
|
|
duration = deltaInTimescale / trackData.timescale;
|
|
if (deltaInTimescale > 0) {
|
|
const { sampleSize, silentValue } = parsePcmCodec(trackData.info.decoderConfig.codec);
|
|
const samplesNeeded = deltaInTimescale * trackData.info.numberOfChannels;
|
|
const data = new Uint8Array(sampleSize * samplesNeeded).fill(silentValue);
|
|
const paddingSample = this.createSampleForTrack(trackData, new Uint8Array(data.buffer), timestamp, duration, "key");
|
|
await this.registerSample(trackData, paddingSample);
|
|
}
|
|
return duration;
|
|
}
|
|
async addSubtitleCue(track, cue, meta) {
|
|
const release = await this.mutex.acquire();
|
|
try {
|
|
const trackData = this.getSubtitleTrackData(track, meta);
|
|
this.validateTimestamp(trackData.track, cue.timestamp, true);
|
|
if (track.source._codec === "webvtt") {
|
|
trackData.cueQueue.push(cue);
|
|
await this.processWebVTTCues(trackData, cue.timestamp);
|
|
} else {
|
|
}
|
|
} finally {
|
|
release();
|
|
}
|
|
}
|
|
async processWebVTTCues(trackData, until) {
|
|
while (trackData.cueQueue.length > 0) {
|
|
const timestamps = /* @__PURE__ */ new Set([]);
|
|
for (const cue of trackData.cueQueue) {
|
|
assert(cue.timestamp <= until);
|
|
assert(trackData.lastCueEndTimestamp <= cue.timestamp + cue.duration);
|
|
timestamps.add(Math.max(cue.timestamp, trackData.lastCueEndTimestamp));
|
|
timestamps.add(cue.timestamp + cue.duration);
|
|
}
|
|
const sortedTimestamps = [...timestamps].sort((a2, b) => a2 - b);
|
|
const sampleStart = sortedTimestamps[0];
|
|
const sampleEnd = sortedTimestamps[1] ?? sampleStart;
|
|
if (until < sampleEnd) {
|
|
break;
|
|
}
|
|
if (trackData.lastCueEndTimestamp < sampleStart) {
|
|
this.auxWriter.seek(0);
|
|
const box2 = vtte();
|
|
this.auxBoxWriter.writeBox(box2);
|
|
const body2 = this.auxTarget._getSlice(0, this.auxWriter.getPos());
|
|
const sample2 = this.createSampleForTrack(trackData, body2, trackData.lastCueEndTimestamp, sampleStart - trackData.lastCueEndTimestamp, "key");
|
|
await this.registerSample(trackData, sample2);
|
|
trackData.lastCueEndTimestamp = sampleStart;
|
|
}
|
|
this.auxWriter.seek(0);
|
|
for (let i2 = 0; i2 < trackData.cueQueue.length; i2++) {
|
|
const cue = trackData.cueQueue[i2];
|
|
if (cue.timestamp >= sampleEnd) {
|
|
break;
|
|
}
|
|
inlineTimestampRegex.lastIndex = 0;
|
|
const containsTimestamp = inlineTimestampRegex.test(cue.text);
|
|
const endTimestamp = cue.timestamp + cue.duration;
|
|
let sourceId = trackData.cueToSourceId.get(cue);
|
|
if (sourceId === void 0 && sampleEnd < endTimestamp) {
|
|
sourceId = trackData.nextSourceId++;
|
|
trackData.cueToSourceId.set(cue, sourceId);
|
|
}
|
|
if (cue.notes) {
|
|
const box3 = vtta(cue.notes);
|
|
this.auxBoxWriter.writeBox(box3);
|
|
}
|
|
const box2 = vttc(cue.text, containsTimestamp ? sampleStart : null, cue.identifier ?? null, cue.settings ?? null, sourceId ?? null);
|
|
this.auxBoxWriter.writeBox(box2);
|
|
if (endTimestamp === sampleEnd) {
|
|
trackData.cueQueue.splice(i2--, 1);
|
|
}
|
|
}
|
|
const body = this.auxTarget._getSlice(0, this.auxWriter.getPos());
|
|
const sample = this.createSampleForTrack(trackData, body, sampleStart, sampleEnd - sampleStart, "key");
|
|
await this.registerSample(trackData, sample);
|
|
trackData.lastCueEndTimestamp = sampleEnd;
|
|
}
|
|
}
|
|
createSampleForTrack(trackData, data, timestamp, duration, type) {
|
|
const sample = {
|
|
timestamp,
|
|
decodeTimestamp: timestamp,
|
|
// This may be refined later
|
|
duration,
|
|
data,
|
|
size: data.byteLength,
|
|
type,
|
|
timescaleUnitsToNextSample: intoTimescale(duration, trackData.timescale)
|
|
// Will be refined
|
|
};
|
|
return sample;
|
|
}
|
|
processTimestamps(trackData, nextSample) {
|
|
if (trackData.timestampProcessingQueue.length === 0) {
|
|
return;
|
|
}
|
|
if (trackData.type === "audio" && trackData.info.requiresPcmTransformation) {
|
|
if (!this.isFragmented) {
|
|
trackData.startTimestampOffset ??= trackData.timestampProcessingQueue[0].timestamp;
|
|
}
|
|
let totalDuration = 0;
|
|
for (let i2 = 0; i2 < trackData.timestampProcessingQueue.length; i2++) {
|
|
const sample = trackData.timestampProcessingQueue[i2];
|
|
const duration = intoTimescale(sample.duration, trackData.timescale);
|
|
totalDuration += duration;
|
|
}
|
|
if (trackData.timeToSampleTable.length === 0) {
|
|
trackData.timeToSampleTable.push({
|
|
sampleCount: totalDuration,
|
|
sampleDelta: 1
|
|
});
|
|
} else {
|
|
const lastEntry = last(trackData.timeToSampleTable);
|
|
lastEntry.sampleCount += totalDuration;
|
|
}
|
|
trackData.timestampProcessingQueue.length = 0;
|
|
return;
|
|
}
|
|
const sortedTimestamps = trackData.timestampProcessingQueue.map((x) => x.timestamp).sort((a2, b) => a2 - b);
|
|
if (!this.isFragmented) {
|
|
trackData.startTimestampOffset ??= sortedTimestamps[0];
|
|
}
|
|
for (let i2 = 0; i2 < trackData.timestampProcessingQueue.length; i2++) {
|
|
const sample = trackData.timestampProcessingQueue[i2];
|
|
sample.decodeTimestamp = sortedTimestamps[i2];
|
|
const sampleCompositionTimeOffset = intoTimescale(sample.timestamp - sample.decodeTimestamp, trackData.timescale);
|
|
const durationInTimescale = intoTimescale(sample.duration, trackData.timescale);
|
|
if (trackData.lastTimescaleUnits !== null) {
|
|
assert(trackData.lastSample);
|
|
const timescaleUnits = intoTimescale(sample.decodeTimestamp, trackData.timescale, false);
|
|
const delta = Math.round(timescaleUnits - trackData.lastTimescaleUnits);
|
|
assert(delta >= 0);
|
|
trackData.lastTimescaleUnits += delta;
|
|
trackData.lastSample.timescaleUnitsToNextSample = delta;
|
|
if (!this.isFragmented) {
|
|
let lastTableEntry = last(trackData.timeToSampleTable);
|
|
assert(lastTableEntry);
|
|
if (lastTableEntry.sampleCount === 1) {
|
|
lastTableEntry.sampleDelta = delta;
|
|
const entryBefore = trackData.timeToSampleTable[trackData.timeToSampleTable.length - 2];
|
|
if (entryBefore && entryBefore.sampleDelta === delta) {
|
|
entryBefore.sampleCount++;
|
|
trackData.timeToSampleTable.pop();
|
|
lastTableEntry = entryBefore;
|
|
}
|
|
} else if (lastTableEntry.sampleDelta !== delta) {
|
|
lastTableEntry.sampleCount--;
|
|
trackData.timeToSampleTable.push(lastTableEntry = {
|
|
sampleCount: 1,
|
|
sampleDelta: delta
|
|
});
|
|
}
|
|
if (lastTableEntry.sampleDelta === durationInTimescale) {
|
|
lastTableEntry.sampleCount++;
|
|
} else {
|
|
trackData.timeToSampleTable.push({
|
|
sampleCount: 1,
|
|
sampleDelta: durationInTimescale
|
|
});
|
|
}
|
|
const lastCompositionTimeOffsetTableEntry = last(trackData.compositionTimeOffsetTable);
|
|
assert(lastCompositionTimeOffsetTableEntry);
|
|
if (lastCompositionTimeOffsetTableEntry.sampleCompositionTimeOffset === sampleCompositionTimeOffset) {
|
|
lastCompositionTimeOffsetTableEntry.sampleCount++;
|
|
} else {
|
|
trackData.compositionTimeOffsetTable.push({
|
|
sampleCount: 1,
|
|
sampleCompositionTimeOffset
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
trackData.lastTimescaleUnits = intoTimescale(sample.decodeTimestamp, trackData.timescale, false);
|
|
if (!this.isFragmented) {
|
|
trackData.timeToSampleTable.push({
|
|
sampleCount: 1,
|
|
sampleDelta: durationInTimescale
|
|
});
|
|
trackData.compositionTimeOffsetTable.push({
|
|
sampleCount: 1,
|
|
sampleCompositionTimeOffset
|
|
});
|
|
}
|
|
}
|
|
trackData.lastSample = sample;
|
|
}
|
|
trackData.timestampProcessingQueue.length = 0;
|
|
assert(trackData.lastSample);
|
|
assert(trackData.lastTimescaleUnits !== null);
|
|
if (nextSample !== void 0 && trackData.lastSample.timescaleUnitsToNextSample === 0) {
|
|
assert(nextSample.type === "key");
|
|
const timescaleUnits = intoTimescale(nextSample.timestamp, trackData.timescale, false);
|
|
const delta = Math.round(timescaleUnits - trackData.lastTimescaleUnits);
|
|
trackData.lastSample.timescaleUnitsToNextSample = delta;
|
|
}
|
|
}
|
|
async registerSample(trackData, sample) {
|
|
if (sample.type === "key") {
|
|
this.processTimestamps(trackData, sample);
|
|
}
|
|
trackData.timestampProcessingQueue.push(sample);
|
|
if (this.isFragmented) {
|
|
trackData.sampleQueue.push(sample);
|
|
await this.interleaveSamples();
|
|
} else if (this.fastStart === "reserve") {
|
|
await this.registerSampleFastStartReserve(trackData, sample);
|
|
} else {
|
|
await this.addSampleToTrack(trackData, sample);
|
|
}
|
|
}
|
|
async addSampleToTrack(trackData, sample) {
|
|
if (!this.isFragmented) {
|
|
trackData.samples.push(sample);
|
|
if (this.fastStart === "reserve") {
|
|
const maximumPacketCount = trackData.track.metadata.maximumPacketCount;
|
|
assert(maximumPacketCount !== void 0);
|
|
if (trackData.samples.length > maximumPacketCount) {
|
|
throw new Error(\`Track #\${trackData.track.id} has already reached the maximum packet count (\${maximumPacketCount}). Either add less packets or increase the maximum packet count.\`);
|
|
}
|
|
}
|
|
}
|
|
let beginNewChunk = false;
|
|
if (!trackData.currentChunk) {
|
|
beginNewChunk = true;
|
|
} else {
|
|
trackData.currentChunk.startTimestamp = Math.min(trackData.currentChunk.startTimestamp, sample.timestamp);
|
|
const currentChunkDuration = sample.timestamp - trackData.currentChunk.startTimestamp;
|
|
if (this.isFragmented) {
|
|
const keyFrameQueuedEverywhere = this.trackDatas.every((otherTrackData) => {
|
|
if (trackData === otherTrackData) {
|
|
return sample.type === "key";
|
|
}
|
|
const firstQueuedSample = otherTrackData.sampleQueue[0];
|
|
if (firstQueuedSample) {
|
|
return firstQueuedSample.type === "key";
|
|
}
|
|
return otherTrackData.closed;
|
|
});
|
|
if (currentChunkDuration >= this.minimumFragmentDuration && keyFrameQueuedEverywhere && sample.timestamp > this.maxWrittenTimestamp) {
|
|
beginNewChunk = true;
|
|
await this.finalizeFragment();
|
|
}
|
|
} else {
|
|
beginNewChunk = currentChunkDuration >= 0.5;
|
|
}
|
|
}
|
|
if (beginNewChunk) {
|
|
if (trackData.currentChunk) {
|
|
await this.finalizeCurrentChunk(trackData);
|
|
}
|
|
trackData.currentChunk = {
|
|
startTimestamp: sample.timestamp,
|
|
samples: [],
|
|
offset: null,
|
|
moofOffset: null
|
|
};
|
|
}
|
|
assert(trackData.currentChunk);
|
|
trackData.currentChunk.samples.push(sample);
|
|
if (this.isFragmented) {
|
|
this.maxWrittenTimestamp = Math.max(this.maxWrittenTimestamp, sample.timestamp);
|
|
this.maxWrittenEndTimestamp = Math.max(this.maxWrittenEndTimestamp, sample.timestamp + sample.duration);
|
|
this.minWrittenTimestamp = Math.min(this.minWrittenTimestamp, sample.timestamp);
|
|
}
|
|
}
|
|
async finalizeCurrentChunk(trackData) {
|
|
assert(!this.isFragmented);
|
|
assert(this.writer);
|
|
if (!trackData.currentChunk)
|
|
return;
|
|
trackData.finalizedChunks.push(trackData.currentChunk);
|
|
this.finalizedChunks.push(trackData.currentChunk);
|
|
let sampleCount = trackData.currentChunk.samples.length;
|
|
if (trackData.type === "audio" && trackData.info.requiresPcmTransformation) {
|
|
sampleCount = trackData.currentChunk.samples.reduce((acc, sample) => acc + intoTimescale(sample.duration, trackData.timescale), 0);
|
|
}
|
|
if (trackData.compactlyCodedChunkTable.length === 0 || last(trackData.compactlyCodedChunkTable).samplesPerChunk !== sampleCount) {
|
|
trackData.compactlyCodedChunkTable.push({
|
|
firstChunk: trackData.finalizedChunks.length,
|
|
// 1-indexed
|
|
samplesPerChunk: sampleCount
|
|
});
|
|
}
|
|
if (this.fastStart === "in-memory") {
|
|
trackData.currentChunk.offset = 0;
|
|
return;
|
|
}
|
|
trackData.currentChunk.offset = this.writer.getPos();
|
|
for (const sample of trackData.currentChunk.samples) {
|
|
assert(sample.data);
|
|
this.writer.write(sample.data);
|
|
sample.data = null;
|
|
}
|
|
await this.writer.flush();
|
|
}
|
|
async interleaveSamples(isFinalCall = false) {
|
|
assert(this.isFragmented);
|
|
if (!isFinalCall && !this.allTracksAreKnown()) {
|
|
return;
|
|
}
|
|
outer: while (true) {
|
|
let trackWithMinTimestamp = null;
|
|
let minTimestamp = Infinity;
|
|
for (const trackData of this.trackDatas) {
|
|
if (!isFinalCall && trackData.sampleQueue.length === 0 && !trackData.closed) {
|
|
break outer;
|
|
}
|
|
if (trackData.sampleQueue.length > 0 && trackData.sampleQueue[0].timestamp < minTimestamp) {
|
|
trackWithMinTimestamp = trackData;
|
|
minTimestamp = trackData.sampleQueue[0].timestamp;
|
|
}
|
|
}
|
|
if (!trackWithMinTimestamp) {
|
|
break;
|
|
}
|
|
const sample = trackWithMinTimestamp.sampleQueue.shift();
|
|
await this.addSampleToTrack(trackWithMinTimestamp, sample);
|
|
}
|
|
}
|
|
async finalizeFragment(flushWriter = !this.isCmaf) {
|
|
assert(this.isFragmented);
|
|
const fragmentNumber = this.nextFragmentNumber++;
|
|
if (fragmentNumber === 1) {
|
|
const boxWriter = this.initBoxWriter ?? this.boxWriter;
|
|
assert(boxWriter);
|
|
if (this.format._options.onMoov) {
|
|
boxWriter.writer.startTrackingWrites();
|
|
}
|
|
this.ensureOneEnabledTrack();
|
|
const movieBox = moov(this);
|
|
boxWriter.writeBox(movieBox);
|
|
if (this.format._options.onMoov) {
|
|
const { data, start } = boxWriter.writer.stopTrackingWrites();
|
|
this.format._options.onMoov(data, start);
|
|
}
|
|
if (this.isCmaf) {
|
|
assert(this.initWriter);
|
|
await this.initWriter.flush();
|
|
await this.initWriter.finalize();
|
|
this.writer = await this.output._getRootWriter(true);
|
|
this.boxWriter = new IsobmffBoxWriter(this.writer);
|
|
const stypSize = this.boxWriter.measureBox(styp());
|
|
const sidxSize = this.boxWriter.measureBox(sidx(this, 0));
|
|
this.segmentHeaderSize = stypSize + sidxSize;
|
|
this.writer.seek(this.segmentHeaderSize);
|
|
}
|
|
}
|
|
assert(this.writer);
|
|
assert(this.boxWriter);
|
|
const tracksInFragment = this.trackDatas.filter((x) => x.currentChunk);
|
|
const moofBox = moof(fragmentNumber, tracksInFragment);
|
|
const moofOffset = this.writer.getPos();
|
|
const mdatStartPos = moofOffset + this.boxWriter.measureBox(moofBox);
|
|
let currentPos = mdatStartPos + MIN_BOX_HEADER_SIZE;
|
|
let fragmentStartTimestamp = Infinity;
|
|
for (const trackData of tracksInFragment) {
|
|
trackData.currentChunk.offset = currentPos;
|
|
trackData.currentChunk.moofOffset = moofOffset;
|
|
for (const sample of trackData.currentChunk.samples) {
|
|
currentPos += sample.size;
|
|
}
|
|
fragmentStartTimestamp = Math.min(fragmentStartTimestamp, trackData.currentChunk.startTimestamp);
|
|
}
|
|
const mdatSize = currentPos - mdatStartPos;
|
|
const needsLargeMdatSize = mdatSize >= 2 ** 32;
|
|
if (needsLargeMdatSize) {
|
|
for (const trackData of tracksInFragment) {
|
|
trackData.currentChunk.offset += MAX_BOX_HEADER_SIZE - MIN_BOX_HEADER_SIZE;
|
|
}
|
|
}
|
|
if (this.format._options.onMoof) {
|
|
this.writer.startTrackingWrites();
|
|
}
|
|
const newMoofBox = moof(fragmentNumber, tracksInFragment);
|
|
this.boxWriter.writeBox(newMoofBox);
|
|
if (this.format._options.onMoof) {
|
|
const { data, start } = this.writer.stopTrackingWrites();
|
|
this.format._options.onMoof(data, start, fragmentStartTimestamp);
|
|
}
|
|
assert(this.writer.getPos() === mdatStartPos);
|
|
if (this.format._options.onMdat) {
|
|
this.writer.startTrackingWrites();
|
|
}
|
|
const mdatBox = mdat(needsLargeMdatSize);
|
|
mdatBox.size = mdatSize;
|
|
this.boxWriter.writeBox(mdatBox);
|
|
this.writer.seek(mdatStartPos + (needsLargeMdatSize ? MAX_BOX_HEADER_SIZE : MIN_BOX_HEADER_SIZE));
|
|
for (const trackData of tracksInFragment) {
|
|
for (const sample of trackData.currentChunk.samples) {
|
|
this.writer.write(sample.data);
|
|
sample.data = null;
|
|
}
|
|
}
|
|
if (this.format._options.onMdat) {
|
|
const { data, start } = this.writer.stopTrackingWrites();
|
|
this.format._options.onMdat(data, start);
|
|
}
|
|
for (const trackData of tracksInFragment) {
|
|
trackData.finalizedChunks.push(trackData.currentChunk);
|
|
this.finalizedChunks.push(trackData.currentChunk);
|
|
trackData.currentChunk = null;
|
|
}
|
|
if (flushWriter) {
|
|
await this.writer.flush();
|
|
}
|
|
}
|
|
async registerSampleFastStartReserve(trackData, sample) {
|
|
assert(this.writer);
|
|
assert(this.boxWriter);
|
|
if (this.allTracksAreKnown()) {
|
|
if (!this.mdat) {
|
|
this.ensureOneEnabledTrack();
|
|
const moovBox = moov(this);
|
|
const moovSize = this.boxWriter.measureBox(moovBox);
|
|
const reservedSize = moovSize + this.computeSampleTableSizeUpperBound() + 4096;
|
|
assert(this.ftypSize !== null);
|
|
this.writer.seek(this.ftypSize + reservedSize);
|
|
if (this.format._options.onMdat) {
|
|
this.writer.startTrackingWrites();
|
|
}
|
|
this.mdat = mdat(true);
|
|
this.boxWriter.writeBox(this.mdat);
|
|
for (const trackData2 of this.trackDatas) {
|
|
for (const sample2 of trackData2.sampleQueue) {
|
|
await this.addSampleToTrack(trackData2, sample2);
|
|
}
|
|
trackData2.sampleQueue.length = 0;
|
|
}
|
|
}
|
|
await this.addSampleToTrack(trackData, sample);
|
|
} else {
|
|
trackData.sampleQueue.push(sample);
|
|
}
|
|
}
|
|
computeSampleTableSizeUpperBound() {
|
|
assert(this.fastStart === "reserve");
|
|
let upperBound = 0;
|
|
for (const trackData of this.trackDatas) {
|
|
const n2 = trackData.track.metadata.maximumPacketCount;
|
|
assert(n2 !== void 0);
|
|
upperBound += (4 + 4) * Math.ceil(2 / 3 * n2);
|
|
upperBound += 4 * n2;
|
|
upperBound += (4 + 4) * Math.ceil(2 / 3 * n2);
|
|
upperBound += (4 + 4 + 4) * Math.ceil(2 / 3 * n2);
|
|
upperBound += 4 * n2;
|
|
upperBound += 8 * n2;
|
|
}
|
|
return upperBound;
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
async onTrackClose(track) {
|
|
const release = await this.mutex.acquire();
|
|
const trackData = this.trackDatas.find((x) => x.track === track);
|
|
if (trackData) {
|
|
trackData.closed = true;
|
|
if (trackData.type === "subtitle" && track.source._codec === "webvtt") {
|
|
await this.processWebVTTCues(trackData, Infinity);
|
|
}
|
|
this.processTimestamps(trackData);
|
|
}
|
|
if (this.allTracksAreKnown()) {
|
|
this.allTracksKnown.resolve();
|
|
}
|
|
if (this.isFragmented) {
|
|
await this.interleaveSamples();
|
|
}
|
|
release();
|
|
}
|
|
ensureOneEnabledTrack() {
|
|
for (const type of ["video", "audio", "subtitle"]) {
|
|
const tracks = this.trackDatas.filter((t2) => t2.type === type);
|
|
if (tracks.length === 0) {
|
|
continue;
|
|
}
|
|
const hasEnabled = tracks.some((t2) => t2.track.metadata.disposition?.default !== false);
|
|
if (!hasEnabled) {
|
|
const firstTrack = tracks[0];
|
|
firstTrack.track.metadata.disposition = {
|
|
...firstTrack.track.metadata.disposition,
|
|
default: true
|
|
};
|
|
}
|
|
}
|
|
}
|
|
/** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */
|
|
async finalize() {
|
|
const release = await this.mutex.acquire();
|
|
this.allTracksKnown.resolve();
|
|
this.ensureOneEnabledTrack();
|
|
for (const trackData of this.trackDatas) {
|
|
trackData.closed = true;
|
|
if (trackData.type === "subtitle" && trackData.track.source._codec === "webvtt") {
|
|
await this.processWebVTTCues(trackData, Infinity);
|
|
}
|
|
this.processTimestamps(trackData);
|
|
}
|
|
if (this.isFragmented) {
|
|
await this.interleaveSamples(true);
|
|
await this.finalizeFragment(false);
|
|
} else {
|
|
for (const trackData of this.trackDatas) {
|
|
await this.finalizeCurrentChunk(trackData);
|
|
assert(trackData.startTimestampOffset !== null);
|
|
for (let i2 = 0; i2 < trackData.samples.length; i2++) {
|
|
const sample = trackData.samples[i2];
|
|
sample.timestamp -= trackData.startTimestampOffset;
|
|
sample.decodeTimestamp -= trackData.startTimestampOffset;
|
|
}
|
|
}
|
|
}
|
|
assert(this.writer);
|
|
assert(this.boxWriter);
|
|
if (this.fastStart === "in-memory") {
|
|
this.mdat = mdat(false);
|
|
let mdatSize;
|
|
for (let i2 = 0; i2 < 2; i2++) {
|
|
const movieBox2 = moov(this);
|
|
const movieBoxSize = this.boxWriter.measureBox(movieBox2);
|
|
mdatSize = this.boxWriter.measureBox(this.mdat);
|
|
let currentChunkPos = this.writer.getPos() + movieBoxSize + mdatSize;
|
|
for (const chunk of this.finalizedChunks) {
|
|
chunk.offset = currentChunkPos;
|
|
for (const { data } of chunk.samples) {
|
|
assert(data);
|
|
currentChunkPos += data.byteLength;
|
|
mdatSize += data.byteLength;
|
|
}
|
|
}
|
|
if (currentChunkPos < 2 ** 32)
|
|
break;
|
|
if (mdatSize >= 2 ** 32)
|
|
this.mdat.largeSize = true;
|
|
}
|
|
if (this.format._options.onMoov) {
|
|
this.writer.startTrackingWrites();
|
|
}
|
|
const movieBox = moov(this);
|
|
this.boxWriter.writeBox(movieBox);
|
|
if (this.format._options.onMoov) {
|
|
const { data, start } = this.writer.stopTrackingWrites();
|
|
this.format._options.onMoov(data, start);
|
|
}
|
|
if (this.format._options.onMdat) {
|
|
this.writer.startTrackingWrites();
|
|
}
|
|
this.mdat.size = mdatSize;
|
|
this.boxWriter.writeBox(this.mdat);
|
|
for (const chunk of this.finalizedChunks) {
|
|
for (const sample of chunk.samples) {
|
|
assert(sample.data);
|
|
this.writer.write(sample.data);
|
|
sample.data = null;
|
|
}
|
|
}
|
|
if (this.format._options.onMdat) {
|
|
const { data, start } = this.writer.stopTrackingWrites();
|
|
this.format._options.onMdat(data, start);
|
|
}
|
|
} else if (this.isFragmented) {
|
|
if (this.isCmaf) {
|
|
const contentSize = this.segmentHeaderSize !== null ? this.writer.getPos() - this.segmentHeaderSize : 0;
|
|
this.writer.seek(0);
|
|
this.boxWriter.writeBox(styp());
|
|
this.boxWriter.writeBox(sidx(this, contentSize));
|
|
} else {
|
|
const startPos = this.writer.getPos();
|
|
const mfraBox = mfra(this.trackDatas);
|
|
this.boxWriter.writeBox(mfraBox);
|
|
const mfraBoxSize = this.writer.getPos() - startPos;
|
|
this.writer.seek(this.writer.getPos() - 4);
|
|
this.boxWriter.writeU32(mfraBoxSize);
|
|
}
|
|
} else {
|
|
assert(this.mdat);
|
|
const mdatPos = this.boxWriter.offsets.get(this.mdat);
|
|
assert(mdatPos !== void 0);
|
|
const mdatSize = this.writer.getPos() - mdatPos;
|
|
this.mdat.size = mdatSize;
|
|
this.mdat.largeSize = mdatSize >= 2 ** 32;
|
|
this.boxWriter.patchBox(this.mdat);
|
|
if (this.format._options.onMdat) {
|
|
const { data, start } = this.writer.stopTrackingWrites();
|
|
this.format._options.onMdat(data, start);
|
|
}
|
|
const movieBox = moov(this);
|
|
if (this.fastStart === "reserve") {
|
|
assert(this.ftypSize !== null);
|
|
this.writer.seek(this.ftypSize);
|
|
if (this.format._options.onMoov) {
|
|
this.writer.startTrackingWrites();
|
|
}
|
|
this.boxWriter.writeBox(movieBox);
|
|
const remainingSpace = this.boxWriter.offsets.get(this.mdat) - this.writer.getPos();
|
|
this.boxWriter.writeBox(free(remainingSpace));
|
|
} else {
|
|
if (this.format._options.onMoov) {
|
|
this.writer.startTrackingWrites();
|
|
}
|
|
this.boxWriter.writeBox(movieBox);
|
|
}
|
|
if (this.format._options.onMoov) {
|
|
const { data, start } = this.writer.stopTrackingWrites();
|
|
this.format._options.onMoov(data, start);
|
|
}
|
|
}
|
|
release();
|
|
}
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/matroska/matroska-muxer.js
|
|
var MIN_CLUSTER_TIMESTAMP_MS = -(2 ** 15);
|
|
var MAX_CLUSTER_TIMESTAMP_MS = 2 ** 15 - 1;
|
|
var APP_NAME = "Mediabunny";
|
|
var SEGMENT_SIZE_BYTES = 6;
|
|
var CLUSTER_SIZE_BYTES = 5;
|
|
var TRACK_TYPE_MAP = {
|
|
video: 1,
|
|
audio: 2,
|
|
subtitle: 17
|
|
};
|
|
var MatroskaMuxer = class extends Muxer {
|
|
constructor(output, format) {
|
|
super(output);
|
|
this.trackDatas = [];
|
|
this.allTracksKnown = promiseWithResolvers();
|
|
this.segment = null;
|
|
this.segmentInfo = null;
|
|
this.seekHead = null;
|
|
this.tracksElement = null;
|
|
this.tagsElement = null;
|
|
this.attachmentsElement = null;
|
|
this.segmentDuration = null;
|
|
this.cues = null;
|
|
this.currentCluster = null;
|
|
this.currentClusterStartMsTimestamp = null;
|
|
this.currentClusterMaxMsTimestamp = null;
|
|
this.trackDatasInCurrentCluster = /* @__PURE__ */ new Map();
|
|
this.startTimestamp = Infinity;
|
|
this.endTimestamp = -Infinity;
|
|
this.format = format;
|
|
}
|
|
async start() {
|
|
const release = await this.mutex.acquire();
|
|
this.writer = await this.output._getRootWriter(!!this.format._options.appendOnly);
|
|
this.ebmlWriter = new EBMLWriter(this.writer);
|
|
this.writeEBMLHeader();
|
|
this.createSegmentInfo();
|
|
this.createCues();
|
|
await this.writer.flush();
|
|
release();
|
|
}
|
|
writeEBMLHeader() {
|
|
if (this.format._options.onEbmlHeader) {
|
|
this.writer.startTrackingWrites();
|
|
}
|
|
const ebmlHeader = { id: EBMLId.EBML, data: [
|
|
{ id: EBMLId.EBMLVersion, data: 1 },
|
|
{ id: EBMLId.EBMLReadVersion, data: 1 },
|
|
{ id: EBMLId.EBMLMaxIDLength, data: 4 },
|
|
{ id: EBMLId.EBMLMaxSizeLength, data: 8 },
|
|
{ id: EBMLId.DocType, data: this.format instanceof WebMOutputFormat ? "webm" : "matroska" },
|
|
{ id: EBMLId.DocTypeVersion, data: 2 },
|
|
{ id: EBMLId.DocTypeReadVersion, data: 2 }
|
|
] };
|
|
this.ebmlWriter.writeEBML(ebmlHeader);
|
|
if (this.format._options.onEbmlHeader) {
|
|
const { data, start } = this.writer.stopTrackingWrites();
|
|
this.format._options.onEbmlHeader(data, start);
|
|
}
|
|
}
|
|
/**
|
|
* Creates a SeekHead element which is positioned near the start of the file and allows the media player to seek to
|
|
* relevant sections more easily. Since we don't know the positions of those sections yet, we'll set them later.
|
|
*/
|
|
maybeCreateSeekHead(writeOffsets) {
|
|
if (this.format._options.appendOnly) {
|
|
return;
|
|
}
|
|
const kaxCues = new Uint8Array([28, 83, 187, 107]);
|
|
const kaxInfo = new Uint8Array([21, 73, 169, 102]);
|
|
const kaxTracks = new Uint8Array([22, 84, 174, 107]);
|
|
const kaxAttachments = new Uint8Array([25, 65, 164, 105]);
|
|
const kaxTags = new Uint8Array([18, 84, 195, 103]);
|
|
const seekHead = { id: EBMLId.SeekHead, data: [
|
|
{ id: EBMLId.Seek, data: [
|
|
{ id: EBMLId.SeekID, data: kaxCues },
|
|
{
|
|
id: EBMLId.SeekPosition,
|
|
size: 5,
|
|
data: writeOffsets ? this.ebmlWriter.offsets.get(this.cues) - this.segmentDataOffset : 0
|
|
}
|
|
] },
|
|
{ id: EBMLId.Seek, data: [
|
|
{ id: EBMLId.SeekID, data: kaxInfo },
|
|
{
|
|
id: EBMLId.SeekPosition,
|
|
size: 5,
|
|
data: writeOffsets ? this.ebmlWriter.offsets.get(this.segmentInfo) - this.segmentDataOffset : 0
|
|
}
|
|
] },
|
|
{ id: EBMLId.Seek, data: [
|
|
{ id: EBMLId.SeekID, data: kaxTracks },
|
|
{
|
|
id: EBMLId.SeekPosition,
|
|
size: 5,
|
|
data: writeOffsets ? this.ebmlWriter.offsets.get(this.tracksElement) - this.segmentDataOffset : 0
|
|
}
|
|
] },
|
|
this.attachmentsElement ? { id: EBMLId.Seek, data: [
|
|
{ id: EBMLId.SeekID, data: kaxAttachments },
|
|
{
|
|
id: EBMLId.SeekPosition,
|
|
size: 5,
|
|
data: writeOffsets ? this.ebmlWriter.offsets.get(this.attachmentsElement) - this.segmentDataOffset : 0
|
|
}
|
|
] } : null,
|
|
this.tagsElement ? { id: EBMLId.Seek, data: [
|
|
{ id: EBMLId.SeekID, data: kaxTags },
|
|
{
|
|
id: EBMLId.SeekPosition,
|
|
size: 5,
|
|
data: writeOffsets ? this.ebmlWriter.offsets.get(this.tagsElement) - this.segmentDataOffset : 0
|
|
}
|
|
] } : null
|
|
] };
|
|
this.seekHead = seekHead;
|
|
}
|
|
createSegmentInfo() {
|
|
const segmentDuration = { id: EBMLId.Duration, data: new EBMLFloat64(0) };
|
|
this.segmentDuration = segmentDuration;
|
|
const segmentInfo = { id: EBMLId.Info, data: [
|
|
{ id: EBMLId.TimestampScale, data: 1e6 },
|
|
{ id: EBMLId.MuxingApp, data: APP_NAME },
|
|
{ id: EBMLId.WritingApp, data: APP_NAME },
|
|
!this.format._options.appendOnly ? segmentDuration : null
|
|
] };
|
|
this.segmentInfo = segmentInfo;
|
|
}
|
|
createTracks() {
|
|
const tracksElement = { id: EBMLId.Tracks, data: [] };
|
|
this.tracksElement = tracksElement;
|
|
for (const trackData of this.trackDatas) {
|
|
const codecId = CODEC_STRING_MAP[trackData.track.source._codec];
|
|
assert(codecId);
|
|
let seekPreRollNs = 0;
|
|
if (trackData.type === "audio" && trackData.track.source._codec === "opus") {
|
|
seekPreRollNs = 1e6 * 80;
|
|
const description = trackData.info.decoderConfig.description;
|
|
if (description) {
|
|
const bytes2 = toUint8Array(description);
|
|
const header = parseOpusIdentificationHeader(bytes2);
|
|
seekPreRollNs = Math.round(1e9 * (header.preSkip / OPUS_SAMPLE_RATE));
|
|
}
|
|
}
|
|
tracksElement.data.push({ id: EBMLId.TrackEntry, data: [
|
|
{ id: EBMLId.TrackNumber, data: trackData.track.id },
|
|
{ id: EBMLId.TrackUID, data: trackData.track.id },
|
|
{ id: EBMLId.TrackType, data: TRACK_TYPE_MAP[trackData.type] },
|
|
trackData.track.metadata.disposition?.default === false ? { id: EBMLId.FlagDefault, data: 0 } : null,
|
|
trackData.track.metadata.disposition?.forced ? { id: EBMLId.FlagForced, data: 1 } : null,
|
|
trackData.track.metadata.disposition?.hearingImpaired ? { id: EBMLId.FlagHearingImpaired, data: 1 } : null,
|
|
trackData.track.metadata.disposition?.visuallyImpaired ? { id: EBMLId.FlagVisualImpaired, data: 1 } : null,
|
|
trackData.track.metadata.disposition?.original ? { id: EBMLId.FlagOriginal, data: 1 } : null,
|
|
trackData.track.metadata.disposition?.commentary ? { id: EBMLId.FlagCommentary, data: 1 } : null,
|
|
{ id: EBMLId.FlagLacing, data: 0 },
|
|
{ id: EBMLId.Language, data: trackData.track.metadata.languageCode ?? UNDETERMINED_LANGUAGE },
|
|
{ id: EBMLId.CodecID, data: codecId },
|
|
{ id: EBMLId.CodecDelay, data: 0 },
|
|
{ id: EBMLId.SeekPreRoll, data: seekPreRollNs },
|
|
trackData.track.metadata.name !== void 0 ? { id: EBMLId.Name, data: new EBMLUnicodeString(trackData.track.metadata.name) } : null,
|
|
trackData.type === "video" ? this.videoSpecificTrackInfo(trackData) : null,
|
|
trackData.type === "audio" ? this.audioSpecificTrackInfo(trackData) : null,
|
|
trackData.type === "subtitle" ? this.subtitleSpecificTrackInfo(trackData) : null
|
|
] });
|
|
}
|
|
}
|
|
videoSpecificTrackInfo(trackData) {
|
|
const { frameRate, rotation } = trackData.track.metadata;
|
|
const elements = [
|
|
trackData.info.decoderConfig.description ? {
|
|
id: EBMLId.CodecPrivate,
|
|
data: toUint8Array(trackData.info.decoderConfig.description)
|
|
} : null,
|
|
frameRate ? {
|
|
id: EBMLId.DefaultDuration,
|
|
data: 1e9 / frameRate
|
|
} : null
|
|
];
|
|
const flippedRotation = rotation ? normalizeRotation(-rotation) : 0;
|
|
const hasNonSquarePixelAspectRatio = !!trackData.info.aspectRatio && trackData.info.aspectRatio.num * trackData.info.height !== trackData.info.aspectRatio.den * trackData.info.width;
|
|
const colorSpace = trackData.info.decoderConfig.colorSpace;
|
|
const videoElement = { id: EBMLId.Video, data: [
|
|
{ id: EBMLId.PixelWidth, data: trackData.info.width },
|
|
{ id: EBMLId.PixelHeight, data: trackData.info.height },
|
|
hasNonSquarePixelAspectRatio ? { id: EBMLId.DisplayWidth, data: trackData.info.aspectRatio.num } : null,
|
|
hasNonSquarePixelAspectRatio ? { id: EBMLId.DisplayHeight, data: trackData.info.aspectRatio.den } : null,
|
|
hasNonSquarePixelAspectRatio ? { id: EBMLId.DisplayUnit, data: 3 } : null,
|
|
// 3 = display aspect ratio
|
|
trackData.info.alphaMode ? { id: EBMLId.AlphaMode, data: 1 } : null,
|
|
colorSpaceIsComplete(colorSpace) ? {
|
|
id: EBMLId.Colour,
|
|
data: [
|
|
{
|
|
id: EBMLId.MatrixCoefficients,
|
|
data: MATRIX_COEFFICIENTS_MAP[colorSpace.matrix]
|
|
},
|
|
{
|
|
id: EBMLId.TransferCharacteristics,
|
|
data: TRANSFER_CHARACTERISTICS_MAP[colorSpace.transfer]
|
|
},
|
|
{
|
|
id: EBMLId.Primaries,
|
|
data: COLOR_PRIMARIES_MAP[colorSpace.primaries]
|
|
},
|
|
{
|
|
id: EBMLId.Range,
|
|
data: colorSpace.fullRange ? 2 : 1
|
|
}
|
|
]
|
|
} : null,
|
|
flippedRotation ? {
|
|
id: EBMLId.Projection,
|
|
data: [
|
|
{
|
|
id: EBMLId.ProjectionType,
|
|
data: 0
|
|
// rectangular
|
|
},
|
|
{
|
|
id: EBMLId.ProjectionPoseRoll,
|
|
data: new EBMLFloat32((flippedRotation + 180) % 360 - 180)
|
|
// [0, 270] -> [-180, 90]
|
|
}
|
|
]
|
|
} : null
|
|
] };
|
|
elements.push(videoElement);
|
|
return elements;
|
|
}
|
|
audioSpecificTrackInfo(trackData) {
|
|
const pcmInfo = PCM_AUDIO_CODECS.includes(trackData.track.source._codec) ? parsePcmCodec(trackData.track.source._codec) : null;
|
|
return [
|
|
trackData.info.decoderConfig.description ? {
|
|
id: EBMLId.CodecPrivate,
|
|
data: toUint8Array(trackData.info.decoderConfig.description)
|
|
} : null,
|
|
{ id: EBMLId.Audio, data: [
|
|
{ id: EBMLId.SamplingFrequency, data: new EBMLFloat32(trackData.info.sampleRate) },
|
|
{ id: EBMLId.Channels, data: trackData.info.numberOfChannels },
|
|
pcmInfo ? { id: EBMLId.BitDepth, data: 8 * pcmInfo.sampleSize } : null
|
|
] }
|
|
];
|
|
}
|
|
subtitleSpecificTrackInfo(trackData) {
|
|
return [
|
|
{ id: EBMLId.CodecPrivate, data: textEncoder.encode(trackData.info.config.description) }
|
|
];
|
|
}
|
|
maybeCreateTags() {
|
|
const simpleTags = [];
|
|
const addSimpleTag = (key, value) => {
|
|
simpleTags.push({ id: EBMLId.SimpleTag, data: [
|
|
{ id: EBMLId.TagName, data: new EBMLUnicodeString(key) },
|
|
typeof value === "string" ? { id: EBMLId.TagString, data: new EBMLUnicodeString(value) } : { id: EBMLId.TagBinary, data: value }
|
|
] });
|
|
};
|
|
const metadataTags = this.output._metadataTags;
|
|
const writtenTags = /* @__PURE__ */ new Set();
|
|
for (const { key, value } of keyValueIterator(metadataTags)) {
|
|
switch (key) {
|
|
case "title":
|
|
{
|
|
addSimpleTag("TITLE", value);
|
|
writtenTags.add("TITLE");
|
|
}
|
|
;
|
|
break;
|
|
case "description":
|
|
{
|
|
addSimpleTag("DESCRIPTION", value);
|
|
writtenTags.add("DESCRIPTION");
|
|
}
|
|
;
|
|
break;
|
|
case "artist":
|
|
{
|
|
addSimpleTag("ARTIST", value);
|
|
writtenTags.add("ARTIST");
|
|
}
|
|
;
|
|
break;
|
|
case "album":
|
|
{
|
|
addSimpleTag("ALBUM", value);
|
|
writtenTags.add("ALBUM");
|
|
}
|
|
;
|
|
break;
|
|
case "albumArtist":
|
|
{
|
|
addSimpleTag("ALBUM_ARTIST", value);
|
|
writtenTags.add("ALBUM_ARTIST");
|
|
}
|
|
;
|
|
break;
|
|
case "genre":
|
|
{
|
|
addSimpleTag("GENRE", value);
|
|
writtenTags.add("GENRE");
|
|
}
|
|
;
|
|
break;
|
|
case "comment":
|
|
{
|
|
addSimpleTag("COMMENT", value);
|
|
writtenTags.add("COMMENT");
|
|
}
|
|
;
|
|
break;
|
|
case "lyrics":
|
|
{
|
|
addSimpleTag("LYRICS", value);
|
|
writtenTags.add("LYRICS");
|
|
}
|
|
;
|
|
break;
|
|
case "date":
|
|
{
|
|
addSimpleTag("DATE", value.toISOString().slice(0, 10));
|
|
writtenTags.add("DATE");
|
|
}
|
|
;
|
|
break;
|
|
case "trackNumber":
|
|
{
|
|
const string = metadataTags.tracksTotal !== void 0 ? \`\${value}/\${metadataTags.tracksTotal}\` : value.toString();
|
|
addSimpleTag("PART_NUMBER", string);
|
|
writtenTags.add("PART_NUMBER");
|
|
}
|
|
;
|
|
break;
|
|
case "discNumber":
|
|
{
|
|
const string = metadataTags.discsTotal !== void 0 ? \`\${value}/\${metadataTags.discsTotal}\` : value.toString();
|
|
addSimpleTag("DISC", string);
|
|
writtenTags.add("DISC");
|
|
}
|
|
;
|
|
break;
|
|
case "tracksTotal":
|
|
case "discsTotal":
|
|
{
|
|
}
|
|
;
|
|
break;
|
|
case "images":
|
|
case "raw":
|
|
{
|
|
}
|
|
;
|
|
break;
|
|
default:
|
|
assertNever(key);
|
|
}
|
|
}
|
|
if (metadataTags.raw) {
|
|
for (const key in metadataTags.raw) {
|
|
const value = metadataTags.raw[key];
|
|
if (value == null || writtenTags.has(key)) {
|
|
continue;
|
|
}
|
|
if (typeof value === "string" || value instanceof Uint8Array) {
|
|
addSimpleTag(key, value);
|
|
}
|
|
}
|
|
}
|
|
if (simpleTags.length === 0) {
|
|
return;
|
|
}
|
|
this.tagsElement = {
|
|
id: EBMLId.Tags,
|
|
data: [{ id: EBMLId.Tag, data: [
|
|
{ id: EBMLId.Targets, data: [
|
|
{ id: EBMLId.TargetTypeValue, data: 50 },
|
|
{ id: EBMLId.TargetType, data: "MOVIE" }
|
|
] },
|
|
...simpleTags
|
|
] }]
|
|
};
|
|
}
|
|
maybeCreateAttachments() {
|
|
const metadataTags = this.output._metadataTags;
|
|
const elements = [];
|
|
const existingFileUids = /* @__PURE__ */ new Set();
|
|
const images = metadataTags.images ?? [];
|
|
for (const image of images) {
|
|
let imageName = image.name;
|
|
if (imageName === void 0) {
|
|
const baseName = image.kind === "coverFront" ? "cover" : image.kind === "coverBack" ? "back" : "image";
|
|
imageName = baseName + (imageMimeTypeToExtension(image.mimeType) ?? "");
|
|
}
|
|
let fileUid;
|
|
while (true) {
|
|
fileUid = 0n;
|
|
for (let i2 = 0; i2 < 8; i2++) {
|
|
fileUid <<= 8n;
|
|
fileUid |= BigInt(Math.floor(Math.random() * 256));
|
|
}
|
|
if (fileUid !== 0n && !existingFileUids.has(fileUid)) {
|
|
break;
|
|
}
|
|
}
|
|
existingFileUids.add(fileUid);
|
|
elements.push({
|
|
id: EBMLId.AttachedFile,
|
|
data: [
|
|
image.description !== void 0 ? { id: EBMLId.FileDescription, data: new EBMLUnicodeString(image.description) } : null,
|
|
{ id: EBMLId.FileName, data: new EBMLUnicodeString(imageName) },
|
|
{ id: EBMLId.FileMediaType, data: image.mimeType },
|
|
{ id: EBMLId.FileData, data: image.data },
|
|
{ id: EBMLId.FileUID, data: fileUid }
|
|
]
|
|
});
|
|
}
|
|
for (const [key, value] of Object.entries(metadataTags.raw ?? {})) {
|
|
if (!(value instanceof AttachedFile)) {
|
|
continue;
|
|
}
|
|
const keyIsNumeric = /^\\d+$/.test(key);
|
|
if (!keyIsNumeric) {
|
|
continue;
|
|
}
|
|
if (images.find((x) => x.mimeType === value.mimeType && uint8ArraysAreEqual(x.data, value.data))) {
|
|
continue;
|
|
}
|
|
elements.push({
|
|
id: EBMLId.AttachedFile,
|
|
data: [
|
|
value.description !== void 0 ? { id: EBMLId.FileDescription, data: new EBMLUnicodeString(value.description) } : null,
|
|
{ id: EBMLId.FileName, data: new EBMLUnicodeString(value.name ?? "") },
|
|
{ id: EBMLId.FileMediaType, data: value.mimeType ?? "" },
|
|
{ id: EBMLId.FileData, data: value.data },
|
|
{ id: EBMLId.FileUID, data: BigInt(key) }
|
|
]
|
|
});
|
|
}
|
|
if (elements.length === 0) {
|
|
return;
|
|
}
|
|
this.attachmentsElement = { id: EBMLId.Attachments, data: elements };
|
|
}
|
|
createSegment() {
|
|
this.createTracks();
|
|
this.maybeCreateTags();
|
|
this.maybeCreateAttachments();
|
|
this.maybeCreateSeekHead(false);
|
|
const segment = {
|
|
id: EBMLId.Segment,
|
|
size: this.format._options.appendOnly ? -1 : SEGMENT_SIZE_BYTES,
|
|
data: [
|
|
this.seekHead,
|
|
// null if append-only
|
|
this.segmentInfo,
|
|
this.tracksElement,
|
|
// Matroska spec says put this at the end of the file, but I think placing it before the first cluster
|
|
// makes more sense, and FFmpeg agrees (argumentum ad ffmpegum fallacy)
|
|
this.attachmentsElement,
|
|
this.tagsElement
|
|
]
|
|
};
|
|
this.segment = segment;
|
|
if (this.format._options.onSegmentHeader) {
|
|
this.writer.startTrackingWrites();
|
|
}
|
|
this.ebmlWriter.writeEBML(segment);
|
|
if (this.format._options.onSegmentHeader) {
|
|
const { data, start } = this.writer.stopTrackingWrites();
|
|
this.format._options.onSegmentHeader(data, start);
|
|
}
|
|
}
|
|
createCues() {
|
|
this.cues = { id: EBMLId.Cues, data: [] };
|
|
}
|
|
get segmentDataOffset() {
|
|
assert(this.segment);
|
|
return this.ebmlWriter.dataOffsets.get(this.segment);
|
|
}
|
|
allTracksAreKnown() {
|
|
for (const track of this.output._tracks) {
|
|
if (!track.source._closed && !this.trackDatas.some((x) => x.track === track)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
async getMimeType() {
|
|
await this.allTracksKnown.promise;
|
|
const codecStrings = this.trackDatas.map((trackData) => {
|
|
if (trackData.type === "video") {
|
|
return trackData.info.decoderConfig.codec;
|
|
} else if (trackData.type === "audio") {
|
|
return trackData.info.decoderConfig.codec;
|
|
} else {
|
|
const map = {
|
|
webvtt: "wvtt"
|
|
};
|
|
return map[trackData.track.source._codec];
|
|
}
|
|
});
|
|
return buildMatroskaMimeType({
|
|
isWebM: this.format instanceof WebMOutputFormat,
|
|
hasVideo: this.trackDatas.some((x) => x.type === "video"),
|
|
hasAudio: this.trackDatas.some((x) => x.type === "audio"),
|
|
codecStrings
|
|
});
|
|
}
|
|
getVideoTrackData(track, packet, meta) {
|
|
const existingTrackData = this.trackDatas.find((x) => x.track === track);
|
|
if (existingTrackData) {
|
|
return existingTrackData;
|
|
}
|
|
validateVideoChunkMetadata(meta);
|
|
assert(meta);
|
|
assert(meta.decoderConfig);
|
|
assert(meta.decoderConfig.codedWidth !== void 0);
|
|
assert(meta.decoderConfig.codedHeight !== void 0);
|
|
const displayAspectWidth = meta.decoderConfig.displayAspectWidth;
|
|
const displayAspectHeight = meta.decoderConfig.displayAspectHeight;
|
|
const aspectRatio = displayAspectWidth === void 0 || displayAspectHeight === void 0 ? null : simplifyRational({
|
|
num: displayAspectWidth,
|
|
den: displayAspectHeight
|
|
});
|
|
const newTrackData = {
|
|
track,
|
|
type: "video",
|
|
info: {
|
|
width: meta.decoderConfig.codedWidth,
|
|
height: meta.decoderConfig.codedHeight,
|
|
aspectRatio,
|
|
decoderConfig: meta.decoderConfig,
|
|
alphaMode: !!packet.sideData.alpha
|
|
// The first packet determines if this track has alpha or not
|
|
},
|
|
chunkQueue: [],
|
|
lastWrittenMsTimestamp: null,
|
|
closed: false
|
|
};
|
|
if (track.source._codec === "vp9") {
|
|
newTrackData.info.decoderConfig = {
|
|
...newTrackData.info.decoderConfig,
|
|
description: new Uint8Array(generateVp9CodecConfigurationFromCodecString(newTrackData.info.decoderConfig.codec))
|
|
};
|
|
} else if (track.source._codec === "av1") {
|
|
newTrackData.info.decoderConfig = {
|
|
...newTrackData.info.decoderConfig,
|
|
description: new Uint8Array(generateAv1CodecConfigurationFromCodecString(newTrackData.info.decoderConfig.codec))
|
|
};
|
|
}
|
|
this.trackDatas.push(newTrackData);
|
|
this.trackDatas.sort((a2, b) => a2.track.id - b.track.id);
|
|
if (this.allTracksAreKnown()) {
|
|
this.allTracksKnown.resolve();
|
|
}
|
|
return newTrackData;
|
|
}
|
|
getAudioTrackData(track, packet, meta) {
|
|
const existingTrackData = this.trackDatas.find((x) => x.track === track);
|
|
if (existingTrackData) {
|
|
return existingTrackData;
|
|
}
|
|
validateAudioChunkMetadata(meta);
|
|
assert(meta);
|
|
assert(meta.decoderConfig);
|
|
const decoderConfig = { ...meta.decoderConfig };
|
|
let requiresAdtsStripping = false;
|
|
if (track.source._codec === "aac" && !decoderConfig.description) {
|
|
const adtsFrame = readAdtsFrameHeader(FileSlice.tempFromBytes(packet.data));
|
|
if (!adtsFrame) {
|
|
throw new Error("Couldn't parse ADTS header from the AAC packet. Make sure the packets are in ADTS format (as specified in ISO 13818-7) when not providing a description, or provide a description (must be an AudioSpecificConfig as specified in ISO 14496-3) and ensure the packets are raw AAC data.");
|
|
}
|
|
const sampleRate = aacFrequencyTable[adtsFrame.samplingFrequencyIndex];
|
|
const numberOfChannels = aacChannelMap[adtsFrame.channelConfiguration];
|
|
if (sampleRate === void 0 || numberOfChannels === void 0) {
|
|
throw new Error("Invalid ADTS frame header.");
|
|
}
|
|
decoderConfig.description = buildAacAudioSpecificConfig({
|
|
objectType: adtsFrame.objectType,
|
|
sampleRate,
|
|
numberOfChannels
|
|
});
|
|
requiresAdtsStripping = true;
|
|
}
|
|
const newTrackData = {
|
|
track,
|
|
type: "audio",
|
|
info: {
|
|
numberOfChannels: meta.decoderConfig.numberOfChannels,
|
|
sampleRate: meta.decoderConfig.sampleRate,
|
|
decoderConfig,
|
|
requiresAdtsStripping
|
|
},
|
|
chunkQueue: [],
|
|
lastWrittenMsTimestamp: null,
|
|
closed: false
|
|
};
|
|
this.trackDatas.push(newTrackData);
|
|
this.trackDatas.sort((a2, b) => a2.track.id - b.track.id);
|
|
if (this.allTracksAreKnown()) {
|
|
this.allTracksKnown.resolve();
|
|
}
|
|
return newTrackData;
|
|
}
|
|
getSubtitleTrackData(track, meta) {
|
|
const existingTrackData = this.trackDatas.find((x) => x.track === track);
|
|
if (existingTrackData) {
|
|
return existingTrackData;
|
|
}
|
|
validateSubtitleMetadata(meta);
|
|
assert(meta);
|
|
assert(meta.config);
|
|
const newTrackData = {
|
|
track,
|
|
type: "subtitle",
|
|
info: {
|
|
config: meta.config
|
|
},
|
|
chunkQueue: [],
|
|
lastWrittenMsTimestamp: null,
|
|
closed: false
|
|
};
|
|
this.trackDatas.push(newTrackData);
|
|
this.trackDatas.sort((a2, b) => a2.track.id - b.track.id);
|
|
if (this.allTracksAreKnown()) {
|
|
this.allTracksKnown.resolve();
|
|
}
|
|
return newTrackData;
|
|
}
|
|
async addEncodedVideoPacket(track, packet, meta) {
|
|
const release = await this.mutex.acquire();
|
|
try {
|
|
const trackData = this.getVideoTrackData(track, packet, meta);
|
|
const isKeyFrame = packet.type === "key";
|
|
this.validateTimestamp(trackData.track, packet.timestamp, isKeyFrame);
|
|
let timestamp = packet.timestamp;
|
|
let duration = packet.duration;
|
|
if (track.metadata.frameRate !== void 0) {
|
|
timestamp = roundToDivisor(timestamp, track.metadata.frameRate);
|
|
duration = roundToDivisor(duration, track.metadata.frameRate);
|
|
}
|
|
const additions = trackData.info.alphaMode ? packet.sideData.alpha ?? null : null;
|
|
const videoChunk = this.createInternalChunk(packet.data, timestamp, duration, packet.type, additions);
|
|
if (track.source._codec === "vp9")
|
|
this.fixVP9ColorSpace(trackData, videoChunk);
|
|
trackData.chunkQueue.push(videoChunk);
|
|
await this.interleaveChunks();
|
|
} finally {
|
|
release();
|
|
}
|
|
}
|
|
async addEncodedAudioPacket(track, packet, meta) {
|
|
const release = await this.mutex.acquire();
|
|
try {
|
|
const trackData = this.getAudioTrackData(track, packet, meta);
|
|
let packetData = packet.data;
|
|
if (trackData.info.requiresAdtsStripping) {
|
|
const adtsFrame = readAdtsFrameHeader(FileSlice.tempFromBytes(packetData));
|
|
if (!adtsFrame) {
|
|
throw new Error("Expected ADTS frame, didn't get one.");
|
|
}
|
|
const headerLength = adtsFrame.crcCheck === null ? MIN_ADTS_FRAME_HEADER_SIZE : MAX_ADTS_FRAME_HEADER_SIZE;
|
|
packetData = packetData.subarray(headerLength);
|
|
}
|
|
const isKeyFrame = packet.type === "key";
|
|
this.validateTimestamp(trackData.track, packet.timestamp, isKeyFrame);
|
|
const audioChunk = this.createInternalChunk(packetData, packet.timestamp, packet.duration, packet.type);
|
|
trackData.chunkQueue.push(audioChunk);
|
|
await this.interleaveChunks();
|
|
} finally {
|
|
release();
|
|
}
|
|
}
|
|
async addSubtitleCue(track, cue, meta) {
|
|
const release = await this.mutex.acquire();
|
|
try {
|
|
const trackData = this.getSubtitleTrackData(track, meta);
|
|
this.validateTimestamp(trackData.track, cue.timestamp, true);
|
|
let bodyText = cue.text;
|
|
const timestampMs = Math.round(cue.timestamp * 1e3);
|
|
inlineTimestampRegex.lastIndex = 0;
|
|
bodyText = bodyText.replace(inlineTimestampRegex, (match) => {
|
|
const time = parseSubtitleTimestamp(match.slice(1, -1));
|
|
const offsetTime = time - timestampMs;
|
|
return \`<\${formatSubtitleTimestamp(offsetTime)}>\`;
|
|
});
|
|
const body = textEncoder.encode(bodyText);
|
|
const additions = \`\${cue.settings ?? ""}
|
|
\${cue.identifier ?? ""}
|
|
\${cue.notes ?? ""}\`;
|
|
const subtitleChunk = this.createInternalChunk(body, cue.timestamp, cue.duration, "key", additions.trim() ? textEncoder.encode(additions) : null);
|
|
trackData.chunkQueue.push(subtitleChunk);
|
|
await this.interleaveChunks();
|
|
} finally {
|
|
release();
|
|
}
|
|
}
|
|
async interleaveChunks(isFinalCall = false) {
|
|
if (!isFinalCall && !this.allTracksAreKnown()) {
|
|
return;
|
|
}
|
|
outer: while (true) {
|
|
let trackWithMinTimestamp = null;
|
|
let minTimestamp = Infinity;
|
|
for (const trackData of this.trackDatas) {
|
|
if (!isFinalCall && trackData.chunkQueue.length === 0 && !trackData.closed) {
|
|
break outer;
|
|
}
|
|
if (trackData.chunkQueue.length > 0 && trackData.chunkQueue[0].timestamp < minTimestamp) {
|
|
trackWithMinTimestamp = trackData;
|
|
minTimestamp = trackData.chunkQueue[0].timestamp;
|
|
}
|
|
}
|
|
if (!trackWithMinTimestamp) {
|
|
break;
|
|
}
|
|
const chunk = trackWithMinTimestamp.chunkQueue.shift();
|
|
this.writeBlock(trackWithMinTimestamp, chunk);
|
|
}
|
|
if (!isFinalCall) {
|
|
await this.writer.flush();
|
|
}
|
|
}
|
|
/**
|
|
* Due to [a bug in Chromium](https://bugs.chromium.org/p/chromium/issues/detail?id=1377842), VP9 streams often
|
|
* lack color space information. This method patches in that information.
|
|
*/
|
|
fixVP9ColorSpace(trackData, chunk) {
|
|
if (chunk.type !== "key")
|
|
return;
|
|
if (!trackData.info.decoderConfig.colorSpace || !trackData.info.decoderConfig.colorSpace.matrix)
|
|
return;
|
|
const bitstream = new Bitstream(chunk.data);
|
|
bitstream.skipBits(2);
|
|
const profileLowBit = bitstream.readBits(1);
|
|
const profileHighBit = bitstream.readBits(1);
|
|
const profile = (profileHighBit << 1) + profileLowBit;
|
|
if (profile === 3)
|
|
bitstream.skipBits(1);
|
|
const showExistingFrame = bitstream.readBits(1);
|
|
if (showExistingFrame)
|
|
return;
|
|
const frameType = bitstream.readBits(1);
|
|
if (frameType !== 0)
|
|
return;
|
|
bitstream.skipBits(2);
|
|
const syncCode = bitstream.readBits(24);
|
|
if (syncCode !== 4817730)
|
|
return;
|
|
if (profile >= 2)
|
|
bitstream.skipBits(1);
|
|
const colorSpaceID = {
|
|
rgb: 7,
|
|
bt709: 2,
|
|
bt470bg: 1,
|
|
smpte170m: 3
|
|
}[trackData.info.decoderConfig.colorSpace.matrix];
|
|
writeBits(chunk.data, bitstream.pos, bitstream.pos + 3, colorSpaceID);
|
|
}
|
|
/** Converts a read-only external chunk into an internal one for easier use. */
|
|
createInternalChunk(data, timestamp, duration, type, additions = null) {
|
|
const internalChunk = {
|
|
data,
|
|
type,
|
|
timestamp,
|
|
duration,
|
|
additions
|
|
};
|
|
return internalChunk;
|
|
}
|
|
/** Writes a block containing media data to the file. */
|
|
writeBlock(trackData, chunk) {
|
|
if (!this.segment) {
|
|
this.createSegment();
|
|
}
|
|
const msTimestamp = Math.round(1e3 * chunk.timestamp);
|
|
const keyFrameQueuedEverywhere = this.trackDatas.every((otherTrackData) => {
|
|
if (trackData === otherTrackData) {
|
|
return chunk.type === "key";
|
|
}
|
|
const firstQueuedSample = otherTrackData.chunkQueue[0];
|
|
if (firstQueuedSample) {
|
|
return firstQueuedSample.type === "key";
|
|
}
|
|
return otherTrackData.closed;
|
|
});
|
|
let shouldCreateNewCluster = false;
|
|
if (!this.currentCluster) {
|
|
shouldCreateNewCluster = true;
|
|
} else {
|
|
assert(this.currentClusterStartMsTimestamp !== null);
|
|
assert(this.currentClusterMaxMsTimestamp !== null);
|
|
const relativeTimestamp2 = msTimestamp - this.currentClusterStartMsTimestamp;
|
|
shouldCreateNewCluster = keyFrameQueuedEverywhere && msTimestamp > this.currentClusterMaxMsTimestamp && relativeTimestamp2 >= 1e3 * (this.format._options.minimumClusterDuration ?? 1) || relativeTimestamp2 > MAX_CLUSTER_TIMESTAMP_MS;
|
|
}
|
|
if (shouldCreateNewCluster) {
|
|
this.createNewCluster(msTimestamp);
|
|
}
|
|
const relativeTimestamp = msTimestamp - this.currentClusterStartMsTimestamp;
|
|
if (relativeTimestamp < MIN_CLUSTER_TIMESTAMP_MS) {
|
|
return;
|
|
}
|
|
const prelude = new Uint8Array(4);
|
|
const view2 = new DataView(prelude.buffer);
|
|
view2.setUint8(0, 128 | trackData.track.id);
|
|
view2.setInt16(1, relativeTimestamp, false);
|
|
const msDuration = Math.round(1e3 * chunk.duration);
|
|
if (!chunk.additions) {
|
|
view2.setUint8(3, Number(chunk.type === "key") << 7);
|
|
const simpleBlock = { id: EBMLId.SimpleBlock, data: [
|
|
prelude,
|
|
chunk.data
|
|
] };
|
|
this.ebmlWriter.writeEBML(simpleBlock);
|
|
} else {
|
|
const blockGroup = { id: EBMLId.BlockGroup, data: [
|
|
{ id: EBMLId.Block, data: [
|
|
prelude,
|
|
chunk.data
|
|
] },
|
|
chunk.type === "delta" ? {
|
|
id: EBMLId.ReferenceBlock,
|
|
data: new EBMLSignedInt(trackData.lastWrittenMsTimestamp - msTimestamp)
|
|
} : null,
|
|
chunk.additions ? { id: EBMLId.BlockAdditions, data: [
|
|
{ id: EBMLId.BlockMore, data: [
|
|
{ id: EBMLId.BlockAddID, data: 1 },
|
|
// Some players expect BlockAddID to come first
|
|
{ id: EBMLId.BlockAdditional, data: chunk.additions }
|
|
] }
|
|
] } : null,
|
|
msDuration > 0 ? { id: EBMLId.BlockDuration, data: msDuration } : null
|
|
] };
|
|
this.ebmlWriter.writeEBML(blockGroup);
|
|
}
|
|
this.startTimestamp = Math.min(this.startTimestamp, msTimestamp);
|
|
this.endTimestamp = Math.max(this.endTimestamp, msTimestamp + msDuration);
|
|
trackData.lastWrittenMsTimestamp = msTimestamp;
|
|
if (!this.trackDatasInCurrentCluster.has(trackData)) {
|
|
this.trackDatasInCurrentCluster.set(trackData, {
|
|
firstMsTimestamp: msTimestamp
|
|
});
|
|
}
|
|
this.currentClusterMaxMsTimestamp = Math.max(this.currentClusterMaxMsTimestamp, msTimestamp);
|
|
}
|
|
/** Creates a new Cluster element to contain media chunks. */
|
|
createNewCluster(msTimestamp) {
|
|
if (this.currentCluster) {
|
|
this.finalizeCurrentCluster();
|
|
}
|
|
if (this.format._options.onCluster) {
|
|
this.writer.startTrackingWrites();
|
|
}
|
|
this.currentCluster = {
|
|
id: EBMLId.Cluster,
|
|
size: this.format._options.appendOnly ? -1 : CLUSTER_SIZE_BYTES,
|
|
data: [
|
|
{ id: EBMLId.Timestamp, data: msTimestamp }
|
|
]
|
|
};
|
|
this.ebmlWriter.writeEBML(this.currentCluster);
|
|
this.currentClusterStartMsTimestamp = msTimestamp;
|
|
this.currentClusterMaxMsTimestamp = msTimestamp;
|
|
this.trackDatasInCurrentCluster.clear();
|
|
}
|
|
finalizeCurrentCluster() {
|
|
assert(this.currentCluster);
|
|
if (!this.format._options.appendOnly) {
|
|
const clusterSize = this.writer.getPos() - this.ebmlWriter.dataOffsets.get(this.currentCluster);
|
|
const endPos = this.writer.getPos();
|
|
this.writer.seek(this.ebmlWriter.offsets.get(this.currentCluster) + 4);
|
|
this.ebmlWriter.writeVarInt(clusterSize, CLUSTER_SIZE_BYTES);
|
|
this.writer.seek(endPos);
|
|
}
|
|
if (this.format._options.onCluster) {
|
|
assert(this.currentClusterStartMsTimestamp !== null);
|
|
const { data, start } = this.writer.stopTrackingWrites();
|
|
this.format._options.onCluster(data, start, this.currentClusterStartMsTimestamp / 1e3);
|
|
}
|
|
const clusterOffsetFromSegment = this.ebmlWriter.offsets.get(this.currentCluster) - this.segmentDataOffset;
|
|
const groupedByTimestamp = /* @__PURE__ */ new Map();
|
|
for (const [trackData, { firstMsTimestamp }] of this.trackDatasInCurrentCluster) {
|
|
if (!groupedByTimestamp.has(firstMsTimestamp)) {
|
|
groupedByTimestamp.set(firstMsTimestamp, []);
|
|
}
|
|
groupedByTimestamp.get(firstMsTimestamp).push(trackData);
|
|
}
|
|
const groupedAndSortedByTimestamp = [...groupedByTimestamp.entries()].sort((a2, b) => a2[0] - b[0]);
|
|
for (const [msTimestamp, trackDatas] of groupedAndSortedByTimestamp) {
|
|
assert(this.cues);
|
|
this.cues.data.push({ id: EBMLId.CuePoint, data: [
|
|
{ id: EBMLId.CueTime, data: msTimestamp },
|
|
// Create CueTrackPositions for each track that starts at this timestamp
|
|
...trackDatas.map((trackData) => {
|
|
return { id: EBMLId.CueTrackPositions, data: [
|
|
{ id: EBMLId.CueTrack, data: trackData.track.id },
|
|
{ id: EBMLId.CueClusterPosition, data: clusterOffsetFromSegment }
|
|
] };
|
|
})
|
|
] });
|
|
}
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
async onTrackClose(track) {
|
|
const release = await this.mutex.acquire();
|
|
const trackData = this.trackDatas.find((x) => x.track === track);
|
|
if (trackData) {
|
|
trackData.closed = true;
|
|
}
|
|
if (this.allTracksAreKnown()) {
|
|
this.allTracksKnown.resolve();
|
|
}
|
|
await this.interleaveChunks();
|
|
release();
|
|
}
|
|
/** Finalizes the file, making it ready for use. Must be called after all media chunks have been added. */
|
|
async finalize() {
|
|
const release = await this.mutex.acquire();
|
|
this.allTracksKnown.resolve();
|
|
for (const trackData of this.trackDatas) {
|
|
trackData.closed = true;
|
|
}
|
|
if (!this.segment) {
|
|
this.createSegment();
|
|
}
|
|
await this.interleaveChunks(true);
|
|
if (this.currentCluster) {
|
|
this.finalizeCurrentCluster();
|
|
}
|
|
assert(this.cues);
|
|
this.ebmlWriter.writeEBML(this.cues);
|
|
if (!this.format._options.appendOnly) {
|
|
const segmentSize = this.writer.getPos() - this.segmentDataOffset;
|
|
this.writer.seek(this.ebmlWriter.offsets.get(this.segment) + 4);
|
|
this.ebmlWriter.writeVarInt(segmentSize, SEGMENT_SIZE_BYTES);
|
|
const duration = this.startTimestamp === Infinity ? 0 : this.endTimestamp - this.startTimestamp;
|
|
this.segmentDuration.data = new EBMLFloat64(duration);
|
|
this.writer.seek(this.ebmlWriter.offsets.get(this.segmentDuration));
|
|
this.ebmlWriter.writeEBML(this.segmentDuration);
|
|
assert(this.seekHead);
|
|
this.writer.seek(this.ebmlWriter.offsets.get(this.seekHead));
|
|
this.maybeCreateSeekHead(true);
|
|
this.ebmlWriter.writeEBML(this.seekHead);
|
|
}
|
|
release();
|
|
}
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/media-source.js
|
|
var MediaSource = class {
|
|
constructor() {
|
|
this._connectedTrack = null;
|
|
this._closingPromise = null;
|
|
this._closed = false;
|
|
}
|
|
/** @internal */
|
|
_ensureValidAdd() {
|
|
if (!this._connectedTrack) {
|
|
throw new Error("Source is not connected to an output track.");
|
|
}
|
|
if (this._connectedTrack.output.state === "canceled") {
|
|
throw new Error("Output has been canceled.");
|
|
}
|
|
if (this._connectedTrack.output.state === "finalizing" || this._connectedTrack.output.state === "finalized") {
|
|
throw new Error("Output has been finalized.");
|
|
}
|
|
if (this._connectedTrack.output.state === "pending") {
|
|
throw new Error("Output has not started.");
|
|
}
|
|
if (this._closed) {
|
|
throw new Error("Source is closed.");
|
|
}
|
|
}
|
|
/** @internal */
|
|
async _start() {
|
|
}
|
|
/** @internal */
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
async _flushAndClose(forceClose) {
|
|
}
|
|
/**
|
|
* Closes this source. This prevents future samples from being added and signals to the output file that no further
|
|
* samples will come in for this track. Calling \`.close()\` is optional but recommended after adding the
|
|
* last sample - for improved performance and reduced memory usage.
|
|
*/
|
|
close() {
|
|
if (this._closingPromise) {
|
|
return;
|
|
}
|
|
const connectedTrack = this._connectedTrack;
|
|
if (!connectedTrack) {
|
|
throw new Error("Cannot call close without connecting the source to an output track.");
|
|
}
|
|
if (connectedTrack.output.state === "pending") {
|
|
throw new Error("Cannot call close before output has been started.");
|
|
}
|
|
this._closingPromise = (async () => {
|
|
await this._flushAndClose(false);
|
|
this._closed = true;
|
|
if (connectedTrack.output.state === "finalizing" || connectedTrack.output.state === "finalized") {
|
|
return;
|
|
}
|
|
connectedTrack.output._muxer.onTrackClose(connectedTrack);
|
|
})();
|
|
}
|
|
/** @internal */
|
|
async _flushOrWaitForOngoingClose(forceClose) {
|
|
return this._closingPromise ??= (async () => {
|
|
await this._flushAndClose(forceClose);
|
|
this._closed = true;
|
|
})();
|
|
}
|
|
};
|
|
var VideoSource = class extends MediaSource {
|
|
/** Internal constructor. */
|
|
constructor(codec) {
|
|
super();
|
|
this._connectedTrack = null;
|
|
if (!VIDEO_CODECS.includes(codec)) {
|
|
throw new TypeError(\`Invalid video codec '\${codec}'. Must be one of: \${VIDEO_CODECS.join(", ")}.\`);
|
|
}
|
|
this._codec = codec;
|
|
}
|
|
};
|
|
var maybeEnsureIsKeyPacket = (track, packet) => {
|
|
if (track.metadata.hasOnlyKeyPackets && packet.type !== "key") {
|
|
throw new Error("Cannot add non-key packets to a hasOnlyKeyPackets video track.");
|
|
}
|
|
};
|
|
var VideoEncoderWrapper = class {
|
|
constructor(source, encodingConfig) {
|
|
this.source = source;
|
|
this.encodingConfig = encodingConfig;
|
|
this.ensureEncoderPromise = null;
|
|
this.encoderInitialized = false;
|
|
this.encoder = null;
|
|
this.muxer = null;
|
|
this.lastMultipleOfKeyFrameInterval = -1;
|
|
this.emittedEncoderPackets = 0;
|
|
this.codedWidth = null;
|
|
this.codedHeight = null;
|
|
this.outputWidth = null;
|
|
this.outputHeight = null;
|
|
this.frameRateLastSample = null;
|
|
this.frameRateLastTimestamp = null;
|
|
this.frameRateLastEndTimestamp = null;
|
|
this.preciseTimings = [];
|
|
this.customEncoder = null;
|
|
this.customEncoderCallSerializer = new CallSerializer();
|
|
this.customEncoderQueueSize = 0;
|
|
this.alphaEncoder = null;
|
|
this.splitter = null;
|
|
this.splitterCreationFailed = false;
|
|
this.alphaFrameQueue = [];
|
|
this.error = null;
|
|
this.closed = false;
|
|
this.lastMuxerPromise = Promise.resolve();
|
|
}
|
|
async add(videoSample, shouldClose, encodeOptions) {
|
|
const originalSample = videoSample;
|
|
try {
|
|
this.checkForEncoderError();
|
|
this.source._ensureValidAdd();
|
|
const config = this.encodingConfig;
|
|
const sizeChangeBehavior = config.sizeChangeBehavior ?? "deny";
|
|
let isSizeChange = false;
|
|
if (this.codedWidth !== null && this.codedHeight !== null) {
|
|
if (videoSample.codedWidth !== this.codedWidth || videoSample.codedHeight !== this.codedHeight) {
|
|
isSizeChange = true;
|
|
if (sizeChangeBehavior === "deny") {
|
|
throw new Error(\`Video sample size must remain constant. Expected \${this.codedWidth}x\${this.codedHeight}, got \${videoSample.codedWidth}x\${videoSample.codedHeight}. To allow the sample size to change over time, set \\\`sizeChangeBehavior\\\` to a value other than 'deny' in the encoding options.\`);
|
|
}
|
|
}
|
|
} else {
|
|
this.codedWidth = videoSample.codedWidth;
|
|
this.codedHeight = videoSample.codedHeight;
|
|
}
|
|
const hasTransformConfig = config.transform?.width !== void 0 || config.transform?.height !== void 0 || config.transform?.rotate !== void 0 || config.transform?.crop !== void 0 || config.transform?.force === true;
|
|
const needsTransform = hasTransformConfig || isSizeChange && sizeChangeBehavior !== "passThrough";
|
|
if (needsTransform) {
|
|
let targetWidth = config.transform?.width;
|
|
let targetHeight = config.transform?.height;
|
|
let appliedFit = config.transform?.fit ?? "fill";
|
|
if (isSizeChange && sizeChangeBehavior !== "passThrough") {
|
|
assert(this.outputWidth);
|
|
assert(this.outputHeight);
|
|
assert(sizeChangeBehavior !== "deny");
|
|
targetWidth = this.outputWidth;
|
|
targetHeight = this.outputHeight;
|
|
appliedFit = sizeChangeBehavior;
|
|
}
|
|
const transformed = await videoSample.transform({
|
|
width: targetWidth,
|
|
height: targetHeight,
|
|
roundDimensionsTo: 2,
|
|
crop: config.transform?.crop,
|
|
rotate: config.transform?.rotate,
|
|
fit: appliedFit,
|
|
alpha: config.alpha
|
|
});
|
|
if (this.outputWidth === null || this.outputHeight === null) {
|
|
this.outputWidth = transformed.displayWidth;
|
|
this.outputHeight = transformed.displayHeight;
|
|
}
|
|
if (shouldClose) {
|
|
videoSample.close();
|
|
}
|
|
videoSample = transformed;
|
|
shouldClose = true;
|
|
} else {
|
|
if (this.outputWidth === null || this.outputHeight === null) {
|
|
this.outputWidth = videoSample.codedWidth;
|
|
this.outputHeight = videoSample.codedHeight;
|
|
}
|
|
}
|
|
const frameRate = config.transform?.frameRate;
|
|
if (frameRate !== void 0) {
|
|
const originalEndTimestamp = videoSample.timestamp + videoSample.duration;
|
|
const alignedTimestamp = floorToDivisor(videoSample.timestamp, frameRate);
|
|
if (this.frameRateLastSample !== null) {
|
|
if (alignedTimestamp <= this.frameRateLastTimestamp) {
|
|
this.frameRateLastSample.close();
|
|
this.frameRateLastSample = videoSample.clone();
|
|
this.frameRateLastEndTimestamp = originalEndTimestamp;
|
|
return;
|
|
} else {
|
|
await this.padFrameRate(alignedTimestamp, encodeOptions);
|
|
}
|
|
}
|
|
if (videoSample === originalSample) {
|
|
videoSample = videoSample.clone();
|
|
shouldClose = true;
|
|
}
|
|
videoSample.setTimestamp(alignedTimestamp);
|
|
videoSample.setDuration(1 / frameRate);
|
|
this.frameRateLastSample?.close();
|
|
this.frameRateLastSample = videoSample.clone();
|
|
this.frameRateLastTimestamp = alignedTimestamp;
|
|
this.frameRateLastEndTimestamp = originalEndTimestamp;
|
|
}
|
|
await this.processAndEncode(videoSample, encodeOptions);
|
|
} finally {
|
|
if (shouldClose) {
|
|
videoSample.close();
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Runs the process function (if any) and encodes the resulting samples.
|
|
*/
|
|
async processAndEncode(videoSample, encodeOptions) {
|
|
const config = this.encodingConfig;
|
|
let samplesToEncode;
|
|
if (config.transform?.process) {
|
|
let processed = config.transform.process(videoSample);
|
|
if (processed instanceof Promise) {
|
|
processed = await processed;
|
|
}
|
|
if (processed === null) {
|
|
return;
|
|
}
|
|
if (!Array.isArray(processed)) {
|
|
processed = [processed];
|
|
}
|
|
samplesToEncode = processed.map((x) => {
|
|
if (x instanceof VideoSample) {
|
|
return x;
|
|
}
|
|
if (typeof VideoFrame !== "undefined" && x instanceof VideoFrame) {
|
|
return new VideoSample(x);
|
|
}
|
|
return new VideoSample(x, {
|
|
timestamp: videoSample.timestamp,
|
|
duration: videoSample.duration
|
|
});
|
|
});
|
|
} else {
|
|
samplesToEncode = [videoSample];
|
|
}
|
|
try {
|
|
for (const sampleToEncode of samplesToEncode) {
|
|
if (!this.encoderInitialized) {
|
|
if (!this.ensureEncoderPromise) {
|
|
this.ensureEncoder(sampleToEncode);
|
|
}
|
|
if (!this.encoderInitialized) {
|
|
await this.ensureEncoderPromise;
|
|
}
|
|
}
|
|
assert(this.encoderInitialized);
|
|
if (this.closed) {
|
|
break;
|
|
}
|
|
const keyFrameInterval = this.encodingConfig.keyFrameInterval ?? 2;
|
|
const multipleOfKeyFrameInterval = Math.floor(sampleToEncode.timestamp / keyFrameInterval);
|
|
const mergedEncodeOptions = { ...sampleToEncode.encodeOptions, ...encodeOptions };
|
|
const finalEncodeOptions = {
|
|
...mergedEncodeOptions,
|
|
keyFrame: mergedEncodeOptions.keyFrame !== void 0 ? mergedEncodeOptions.keyFrame : keyFrameInterval === 0 || multipleOfKeyFrameInterval !== this.lastMultipleOfKeyFrameInterval
|
|
};
|
|
this.lastMultipleOfKeyFrameInterval = multipleOfKeyFrameInterval;
|
|
this.encodingConfig.onEncodedSample?.(sampleToEncode);
|
|
if (this.customEncoder) {
|
|
this.customEncoderQueueSize++;
|
|
const clonedSample = sampleToEncode.clone();
|
|
const promise = this.customEncoderCallSerializer.call(() => this.customEncoder.encode(clonedSample, finalEncodeOptions)).then(() => this.customEncoderQueueSize--).catch((error) => this.error ??= error).finally(() => {
|
|
clonedSample.close();
|
|
});
|
|
if (this.customEncoderQueueSize >= 4) {
|
|
await promise;
|
|
}
|
|
} else {
|
|
assert(this.encoder);
|
|
const videoFrame = sampleToEncode.toVideoFrame();
|
|
const preciseTimingIndex = binarySearchLessOrEqual(this.preciseTimings, videoFrame.timestamp, (x) => x.microsecondTimestamp);
|
|
const existingEntry = preciseTimingIndex !== -1 ? this.preciseTimings[preciseTimingIndex] : null;
|
|
if (existingEntry && existingEntry.microsecondTimestamp === videoFrame.timestamp) {
|
|
if (existingEntry.timestamp !== sampleToEncode.timestamp) {
|
|
existingEntry.timestampIsValid = false;
|
|
}
|
|
if (existingEntry.duration !== sampleToEncode.duration) {
|
|
existingEntry.durationIsValid = false;
|
|
}
|
|
} else {
|
|
this.preciseTimings.splice(preciseTimingIndex + 1, 0, {
|
|
microsecondTimestamp: videoFrame.timestamp,
|
|
timestamp: sampleToEncode.timestamp,
|
|
duration: sampleToEncode.duration,
|
|
timestampIsValid: true,
|
|
durationIsValid: true
|
|
});
|
|
if (this.preciseTimings.length > 128) {
|
|
this.preciseTimings.shift();
|
|
}
|
|
}
|
|
if (!this.alphaEncoder) {
|
|
this.encoder.encode(videoFrame, finalEncodeOptions);
|
|
videoFrame.close();
|
|
} else {
|
|
const frameDefinitelyHasNoAlpha = !!videoFrame.format && !videoFrame.format.includes("A");
|
|
if (frameDefinitelyHasNoAlpha || this.splitterCreationFailed) {
|
|
this.alphaFrameQueue.push(null);
|
|
this.encoder.encode(videoFrame, finalEncodeOptions);
|
|
videoFrame.close();
|
|
} else {
|
|
const width = videoFrame.displayWidth;
|
|
const height = videoFrame.displayHeight;
|
|
if (!this.splitter) {
|
|
this.splitter = new ColorAlphaSplitter(width, height);
|
|
}
|
|
const { colorFrame, alphaFrame } = await this.splitter.update(videoFrame);
|
|
this.alphaFrameQueue.push(alphaFrame);
|
|
this.encoder.encode(colorFrame, finalEncodeOptions);
|
|
colorFrame.close();
|
|
}
|
|
}
|
|
if (this.encoder.encodeQueueSize >= 4) {
|
|
await new Promise((resolve) => this.encoder.addEventListener("dequeue", resolve, { once: true }));
|
|
}
|
|
}
|
|
await this.lastMuxerPromise;
|
|
}
|
|
} finally {
|
|
for (const sample of samplesToEncode) {
|
|
if (sample !== videoSample) {
|
|
sample.close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/** Repeats the last frame rate sample to fill the gap up to the given timestamp. */
|
|
async padFrameRate(until, encodeOptions) {
|
|
const frameRate = this.encodingConfig.transform.frameRate;
|
|
assert(this.frameRateLastSample);
|
|
const frameDifference = Math.round((until - this.frameRateLastTimestamp) * frameRate);
|
|
for (let i2 = 1; i2 < frameDifference; i2++) {
|
|
const sample = this.frameRateLastSample.clone();
|
|
sample.setTimestamp(this.frameRateLastTimestamp + i2 / frameRate);
|
|
sample.setDuration(1 / frameRate);
|
|
await this.processAndEncode(sample, encodeOptions);
|
|
sample.close();
|
|
}
|
|
}
|
|
ensureEncoder(videoSample) {
|
|
this.ensureEncoderPromise = (async () => {
|
|
const encoderConfig = buildVideoEncoderConfig({
|
|
...this.encodingConfig,
|
|
width: videoSample.codedWidth,
|
|
height: videoSample.codedHeight,
|
|
squarePixelWidth: videoSample.squarePixelWidth,
|
|
squarePixelHeight: videoSample.squarePixelHeight,
|
|
framerate: this.source._connectedTrack?.metadata.frameRate
|
|
});
|
|
this.encodingConfig.onEncoderConfig?.(encoderConfig);
|
|
const MatchingCustomEncoder = customVideoEncoders.find((x) => x.supports(this.encodingConfig.codec, encoderConfig));
|
|
if (MatchingCustomEncoder) {
|
|
this.customEncoder = new MatchingCustomEncoder();
|
|
this.customEncoder.codec = this.encodingConfig.codec;
|
|
this.customEncoder.config = encoderConfig;
|
|
this.customEncoder.onPacket = (packet, meta) => {
|
|
if (!(packet instanceof EncodedPacket)) {
|
|
throw new TypeError("The first argument passed to onPacket must be an EncodedPacket.");
|
|
}
|
|
if (meta !== void 0 && (!meta || typeof meta !== "object")) {
|
|
throw new TypeError("The second argument passed to onPacket must be an object or undefined.");
|
|
}
|
|
maybeEnsureIsKeyPacket(this.source._connectedTrack, packet);
|
|
this.encodingConfig.onEncodedPacket?.(packet, meta);
|
|
this.lastMuxerPromise = this.muxer.addEncodedVideoPacket(this.source._connectedTrack, packet, meta).catch((error) => {
|
|
this.error ??= error;
|
|
});
|
|
};
|
|
await this.customEncoder.init();
|
|
} else {
|
|
if (typeof VideoEncoder === "undefined") {
|
|
throw new Error("VideoEncoder is not supported by this browser.");
|
|
}
|
|
encoderConfig.alpha = "discard";
|
|
if (this.encodingConfig.alpha === "keep") {
|
|
encoderConfig.latencyMode = "quality";
|
|
}
|
|
const hasOddDimension = encoderConfig.width % 2 === 1 || encoderConfig.height % 2 === 1;
|
|
if (hasOddDimension && (this.encodingConfig.codec === "avc" || this.encodingConfig.codec === "hevc")) {
|
|
throw new Error(\`The dimensions \${encoderConfig.width}x\${encoderConfig.height} are not supported for codec '\${this.encodingConfig.codec}'; both width and height must be even numbers. Make sure to round your dimensions to the nearest even number.\`);
|
|
}
|
|
const support = await VideoEncoder.isConfigSupported(encoderConfig);
|
|
if (!support.supported) {
|
|
throw new Error(\`This specific encoder configuration (\${encoderConfig.codec}, \${encoderConfig.bitrate} bps, \${encoderConfig.width}x\${encoderConfig.height}, hardware acceleration: \${encoderConfig.hardwareAcceleration ?? "no-preference"}) is not supported by this browser. Consider using another codec or changing your video parameters.\`);
|
|
}
|
|
const colorChunkQueue = [];
|
|
const nullAlphaChunkQueue = [];
|
|
let encodedAlphaChunkCount = 0;
|
|
let alphaEncoderQueue = 0;
|
|
const addPacket = (colorChunk, alphaChunk, meta) => {
|
|
const sideData = {};
|
|
if (alphaChunk) {
|
|
const alphaData = new Uint8Array(alphaChunk.byteLength);
|
|
alphaChunk.copyTo(alphaData);
|
|
sideData.alpha = alphaData;
|
|
}
|
|
let packet = EncodedPacket.fromEncodedChunk(colorChunk, sideData);
|
|
const preciseTimingIndex = binarySearchLessOrEqual(this.preciseTimings, colorChunk.timestamp, (x) => x.microsecondTimestamp);
|
|
const entry = preciseTimingIndex !== -1 ? this.preciseTimings[preciseTimingIndex] : null;
|
|
let actualType = null;
|
|
if (this.emittedEncoderPackets === 0 && packet.type === "delta" && meta?.decoderConfig) {
|
|
actualType = determineVideoPacketType(this.encodingConfig.codec, meta.decoderConfig, packet.data);
|
|
}
|
|
if (entry && entry.microsecondTimestamp === colorChunk.timestamp || actualType !== null) {
|
|
packet = packet.clone({
|
|
timestamp: entry?.timestampIsValid ? entry.timestamp : void 0,
|
|
duration: entry?.durationIsValid ? entry.duration : void 0,
|
|
type: actualType ?? void 0
|
|
});
|
|
}
|
|
maybeEnsureIsKeyPacket(this.source._connectedTrack, packet);
|
|
this.encodingConfig.onEncodedPacket?.(packet, meta);
|
|
this.lastMuxerPromise = this.muxer.addEncodedVideoPacket(this.source._connectedTrack, packet, meta).catch((error) => {
|
|
this.error ??= error;
|
|
});
|
|
this.emittedEncoderPackets++;
|
|
};
|
|
const stack = new Error("Encoding error").stack;
|
|
this.encoder = new VideoEncoder({
|
|
output: (chunk, meta) => {
|
|
if (!this.alphaEncoder) {
|
|
addPacket(chunk, null, meta);
|
|
return;
|
|
}
|
|
const alphaFrame = this.alphaFrameQueue.shift();
|
|
assert(alphaFrame !== void 0);
|
|
if (alphaFrame) {
|
|
this.alphaEncoder.encode(alphaFrame, {
|
|
// Crucial: The alpha frame is forced to be a key frame whenever the color frame
|
|
// also is. Without this, playback can glitch and even crash in some browsers.
|
|
// This is the reason why the two encoders are wired in series and not in parallel.
|
|
keyFrame: chunk.type === "key"
|
|
});
|
|
alphaEncoderQueue++;
|
|
alphaFrame.close();
|
|
colorChunkQueue.push({ chunk, meta });
|
|
} else {
|
|
if (alphaEncoderQueue === 0) {
|
|
addPacket(chunk, null, meta);
|
|
} else {
|
|
nullAlphaChunkQueue.push(encodedAlphaChunkCount + alphaEncoderQueue);
|
|
colorChunkQueue.push({ chunk, meta });
|
|
}
|
|
}
|
|
},
|
|
error: (error) => {
|
|
error.stack = stack;
|
|
this.error ??= error;
|
|
}
|
|
});
|
|
this.encoder.configure(encoderConfig);
|
|
if (this.encodingConfig.alpha === "keep") {
|
|
const stack2 = new Error("Encoding error").stack;
|
|
this.alphaEncoder = new VideoEncoder({
|
|
// We ignore the alpha chunk's metadata
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
output: (chunk, meta) => {
|
|
alphaEncoderQueue--;
|
|
const colorChunk = colorChunkQueue.shift();
|
|
assert(colorChunk !== void 0);
|
|
addPacket(colorChunk.chunk, chunk, colorChunk.meta);
|
|
encodedAlphaChunkCount++;
|
|
while (nullAlphaChunkQueue.length > 0 && nullAlphaChunkQueue[0] === encodedAlphaChunkCount) {
|
|
nullAlphaChunkQueue.shift();
|
|
const colorChunk2 = colorChunkQueue.shift();
|
|
assert(colorChunk2 !== void 0);
|
|
addPacket(colorChunk2.chunk, null, colorChunk2.meta);
|
|
}
|
|
},
|
|
error: (error) => {
|
|
error.stack = stack2;
|
|
this.error ??= error;
|
|
}
|
|
});
|
|
this.alphaEncoder.configure(encoderConfig);
|
|
}
|
|
}
|
|
assert(this.source._connectedTrack);
|
|
this.muxer = this.source._connectedTrack.output._muxer;
|
|
this.encoderInitialized = true;
|
|
})();
|
|
}
|
|
async flushAndClose(forceClose) {
|
|
if (!forceClose) {
|
|
this.checkForEncoderError();
|
|
}
|
|
if (!forceClose && this.frameRateLastSample) {
|
|
const frameRate = this.encodingConfig.transform.frameRate;
|
|
const alignedEnd = floorToDivisor(this.frameRateLastEndTimestamp, frameRate);
|
|
await this.padFrameRate(alignedEnd);
|
|
}
|
|
this.closed = true;
|
|
this.frameRateLastSample?.close();
|
|
this.frameRateLastSample = null;
|
|
if (this.customEncoder) {
|
|
if (!forceClose) {
|
|
void this.customEncoderCallSerializer.call(() => this.customEncoder.flush());
|
|
}
|
|
await this.customEncoderCallSerializer.call(() => this.customEncoder.close());
|
|
} else if (this.encoder) {
|
|
if (!forceClose) {
|
|
await this.encoder.flush();
|
|
await this.alphaEncoder?.flush();
|
|
}
|
|
if (this.encoder.state !== "closed") {
|
|
this.encoder.close();
|
|
}
|
|
if (this.alphaEncoder && this.alphaEncoder.state !== "closed") {
|
|
this.alphaEncoder.close();
|
|
}
|
|
this.alphaFrameQueue.forEach((x) => x?.close());
|
|
this.splitter?.close();
|
|
}
|
|
if (!forceClose) {
|
|
this.checkForEncoderError();
|
|
}
|
|
}
|
|
getQueueSize() {
|
|
if (this.customEncoder) {
|
|
return this.customEncoderQueueSize;
|
|
} else {
|
|
return this.encoder?.encodeQueueSize ?? 0;
|
|
}
|
|
}
|
|
checkForEncoderError() {
|
|
if (this.error) {
|
|
throw this.error;
|
|
}
|
|
}
|
|
};
|
|
var splitterGpuUnavailable = false;
|
|
var ColorAlphaSplitter = class _ColorAlphaSplitter {
|
|
constructor(initialWidth, initialHeight) {
|
|
this.canvas = null;
|
|
this.gl = null;
|
|
this.colorProgram = null;
|
|
this.alphaProgram = null;
|
|
this.vao = null;
|
|
this.sourceTexture = null;
|
|
this.alphaResolutionLocation = null;
|
|
this.worker = null;
|
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
this.nextRequestId = 0;
|
|
const canMakeCanvas = typeof OffscreenCanvas !== "undefined" || typeof document !== "undefined" && typeof document.createElement === "function";
|
|
if (!_ColorAlphaSplitter.forceCpu && canMakeCanvas && !splitterGpuUnavailable) {
|
|
try {
|
|
if (typeof OffscreenCanvas !== "undefined") {
|
|
this.canvas = new OffscreenCanvas(initialWidth, initialHeight);
|
|
} else {
|
|
this.canvas = document.createElement("canvas");
|
|
this.canvas.width = initialWidth;
|
|
this.canvas.height = initialHeight;
|
|
}
|
|
const gl = this.canvas.getContext("webgl2", {
|
|
alpha: true
|
|
// Needed due to the YUV thing we do for alpha
|
|
});
|
|
if (!gl) {
|
|
throw new Error("Couldn't acquire WebGL 2 context.");
|
|
}
|
|
this.gl = gl;
|
|
this.colorProgram = this.createColorProgram();
|
|
this.alphaProgram = this.createAlphaProgram();
|
|
this.vao = this.createVAO();
|
|
this.sourceTexture = this.createTexture();
|
|
this.alphaResolutionLocation = this.gl.getUniformLocation(this.alphaProgram, "u_resolution");
|
|
this.gl.useProgram(this.colorProgram);
|
|
this.gl.uniform1i(this.gl.getUniformLocation(this.colorProgram, "u_sourceTexture"), 0);
|
|
this.gl.useProgram(this.alphaProgram);
|
|
this.gl.uniform1i(this.gl.getUniformLocation(this.alphaProgram, "u_sourceTexture"), 0);
|
|
} catch (error) {
|
|
this.gl = null;
|
|
this.canvas = null;
|
|
splitterGpuUnavailable = true;
|
|
Logging._warn("Falling back to CPU for color/alpha splitting.", error);
|
|
}
|
|
}
|
|
}
|
|
async update(sourceFrame) {
|
|
if (this.gl) {
|
|
return this.updateGpu(sourceFrame);
|
|
} else {
|
|
return this.updateCpu(sourceFrame);
|
|
}
|
|
}
|
|
updateGpu(sourceFrame) {
|
|
assert(this.gl);
|
|
assert(this.canvas);
|
|
if (sourceFrame.displayWidth !== this.canvas.width || sourceFrame.displayHeight !== this.canvas.height) {
|
|
this.canvas.width = sourceFrame.displayWidth;
|
|
this.canvas.height = sourceFrame.displayHeight;
|
|
}
|
|
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
this.gl.bindTexture(this.gl.TEXTURE_2D, this.sourceTexture);
|
|
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, sourceFrame);
|
|
const colorFrame = this.runColorProgram(sourceFrame);
|
|
const alphaFrame = this.runAlphaProgram(sourceFrame);
|
|
sourceFrame.close();
|
|
return { colorFrame, alphaFrame };
|
|
}
|
|
createVertexShader() {
|
|
assert(this.gl);
|
|
return this.createShader(this.gl.VERTEX_SHADER, \`#version 300 es
|
|
in vec2 a_position;
|
|
in vec2 a_texCoord;
|
|
out vec2 v_texCoord;
|
|
|
|
void main() {
|
|
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
v_texCoord = a_texCoord;
|
|
}
|
|
\`);
|
|
}
|
|
createColorProgram() {
|
|
assert(this.gl);
|
|
const vertexShader = this.createVertexShader();
|
|
const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, \`#version 300 es
|
|
precision highp float;
|
|
|
|
uniform sampler2D u_sourceTexture;
|
|
in vec2 v_texCoord;
|
|
out vec4 fragColor;
|
|
|
|
void main() {
|
|
vec4 source = texture(u_sourceTexture, v_texCoord);
|
|
fragColor = vec4(source.rgb, 1.0);
|
|
}
|
|
\`);
|
|
const program = this.gl.createProgram();
|
|
this.gl.attachShader(program, vertexShader);
|
|
this.gl.attachShader(program, fragmentShader);
|
|
this.gl.linkProgram(program);
|
|
return program;
|
|
}
|
|
createAlphaProgram() {
|
|
assert(this.gl);
|
|
const vertexShader = this.createVertexShader();
|
|
const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, \`#version 300 es
|
|
precision highp float;
|
|
|
|
uniform sampler2D u_sourceTexture;
|
|
uniform vec2 u_resolution; // The width and height of the canvas
|
|
in vec2 v_texCoord;
|
|
out vec4 fragColor;
|
|
|
|
// This function determines the value for a single byte in the YUV stream
|
|
float getByteValue(float byteOffset) {
|
|
float width = u_resolution.x;
|
|
float height = u_resolution.y;
|
|
|
|
float yPlaneSize = width * height;
|
|
|
|
if (byteOffset < yPlaneSize) {
|
|
// This byte is in the luma plane. Find the corresponding pixel coordinates to sample from
|
|
float y = floor(byteOffset / width);
|
|
float x = mod(byteOffset, width);
|
|
|
|
// Add 0.5 to sample the center of the texel
|
|
vec2 sampleCoord = (vec2(x, y) + 0.5) / u_resolution;
|
|
|
|
// The luma value is the alpha from the source texture
|
|
return texture(u_sourceTexture, sampleCoord).a;
|
|
} else {
|
|
// Write a fixed value for chroma and beyond
|
|
return 128.0 / 255.0;
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
// Each fragment writes 4 bytes (R, G, B, A)
|
|
float pixelIndex = floor(gl_FragCoord.y) * u_resolution.x + floor(gl_FragCoord.x);
|
|
float baseByteOffset = pixelIndex * 4.0;
|
|
|
|
vec4 result;
|
|
for (int i = 0; i < 4; i++) {
|
|
float currentByteOffset = baseByteOffset + float(i);
|
|
result[i] = getByteValue(currentByteOffset);
|
|
}
|
|
|
|
fragColor = result;
|
|
}
|
|
\`);
|
|
const program = this.gl.createProgram();
|
|
this.gl.attachShader(program, vertexShader);
|
|
this.gl.attachShader(program, fragmentShader);
|
|
this.gl.linkProgram(program);
|
|
return program;
|
|
}
|
|
createShader(type, source) {
|
|
assert(this.gl);
|
|
const shader = this.gl.createShader(type);
|
|
this.gl.shaderSource(shader, source);
|
|
this.gl.compileShader(shader);
|
|
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
|
Logging._error("Shader compile error:", this.gl.getShaderInfoLog(shader));
|
|
}
|
|
return shader;
|
|
}
|
|
createVAO() {
|
|
assert(this.gl);
|
|
assert(this.colorProgram);
|
|
const vao = this.gl.createVertexArray();
|
|
this.gl.bindVertexArray(vao);
|
|
const vertices = new Float32Array([
|
|
-1,
|
|
-1,
|
|
0,
|
|
1,
|
|
1,
|
|
-1,
|
|
1,
|
|
1,
|
|
-1,
|
|
1,
|
|
0,
|
|
0,
|
|
1,
|
|
1,
|
|
1,
|
|
0
|
|
]);
|
|
const buffer = this.gl.createBuffer();
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, vertices, this.gl.STATIC_DRAW);
|
|
const positionLocation = this.gl.getAttribLocation(this.colorProgram, "a_position");
|
|
const texCoordLocation = this.gl.getAttribLocation(this.colorProgram, "a_texCoord");
|
|
this.gl.enableVertexAttribArray(positionLocation);
|
|
this.gl.vertexAttribPointer(positionLocation, 2, this.gl.FLOAT, false, 16, 0);
|
|
this.gl.enableVertexAttribArray(texCoordLocation);
|
|
this.gl.vertexAttribPointer(texCoordLocation, 2, this.gl.FLOAT, false, 16, 8);
|
|
return vao;
|
|
}
|
|
createTexture() {
|
|
assert(this.gl);
|
|
const texture = this.gl.createTexture();
|
|
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
|
|
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
|
|
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
|
|
return texture;
|
|
}
|
|
runColorProgram(sourceFrame) {
|
|
assert(this.gl);
|
|
assert(this.canvas);
|
|
this.gl.useProgram(this.colorProgram);
|
|
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
|
this.gl.bindVertexArray(this.vao);
|
|
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
|
|
return new VideoFrame(this.canvas, {
|
|
timestamp: sourceFrame.timestamp,
|
|
duration: sourceFrame.duration ?? void 0,
|
|
alpha: "discard"
|
|
});
|
|
}
|
|
runAlphaProgram(sourceFrame) {
|
|
assert(this.gl);
|
|
assert(this.canvas);
|
|
this.gl.useProgram(this.alphaProgram);
|
|
this.gl.uniform2f(this.alphaResolutionLocation, this.canvas.width, this.canvas.height);
|
|
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
|
this.gl.bindVertexArray(this.vao);
|
|
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
|
|
const { width, height } = this.canvas;
|
|
const chromaSamples = Math.ceil(width / 2) * Math.ceil(height / 2);
|
|
const yuvSize = width * height + chromaSamples * 2;
|
|
const requiredHeight = Math.ceil(yuvSize / (width * 4));
|
|
let yuv = new Uint8Array(4 * width * requiredHeight);
|
|
this.gl.readPixels(0, 0, width, requiredHeight, this.gl.RGBA, this.gl.UNSIGNED_BYTE, yuv);
|
|
yuv = yuv.subarray(0, yuvSize);
|
|
assert(yuv[width * height] === 128);
|
|
assert(yuv[yuv.length - 1] === 128);
|
|
const init = {
|
|
format: "I420",
|
|
codedWidth: width,
|
|
codedHeight: height,
|
|
timestamp: sourceFrame.timestamp,
|
|
duration: sourceFrame.duration ?? void 0,
|
|
transfer: [yuv.buffer]
|
|
};
|
|
return new VideoFrame(yuv, init);
|
|
}
|
|
updateCpu(sourceFrame) {
|
|
if (!this.worker) {
|
|
const blob = new Blob([\`(\${colorAlphaSplitterWorkerCode.toString()})()\`], { type: "application/javascript" });
|
|
const url2 = URL.createObjectURL(blob);
|
|
this.worker = new Worker(url2);
|
|
URL.revokeObjectURL(url2);
|
|
this.worker.addEventListener("message", (event) => {
|
|
const data = event.data;
|
|
const pending2 = this.pendingRequests.get(data.id);
|
|
if (!pending2) {
|
|
return;
|
|
}
|
|
this.pendingRequests.delete(data.id);
|
|
if ("error" in data) {
|
|
pending2.reject(new Error(data.error));
|
|
} else {
|
|
pending2.resolve({ colorFrame: data.colorFrame, alphaFrame: data.alphaFrame });
|
|
}
|
|
});
|
|
this.worker.addEventListener("error", (event) => {
|
|
const error = new Error(event.message || "Color/alpha splitter worker error.");
|
|
for (const pending2 of this.pendingRequests.values()) {
|
|
pending2.reject(error);
|
|
}
|
|
this.pendingRequests.clear();
|
|
});
|
|
}
|
|
const id = this.nextRequestId++;
|
|
const pending = promiseWithResolvers();
|
|
this.pendingRequests.set(id, pending);
|
|
this.worker.postMessage({ id, sourceFrame }, { transfer: [sourceFrame] });
|
|
return pending.promise;
|
|
}
|
|
close() {
|
|
this.gl?.getExtension("WEBGL_lose_context")?.loseContext();
|
|
this.gl = null;
|
|
this.canvas = null;
|
|
this.worker?.terminate();
|
|
this.worker = null;
|
|
const error = new Error("Color/alpha splitter closed.");
|
|
for (const pending of this.pendingRequests.values()) {
|
|
pending.reject(error);
|
|
}
|
|
this.pendingRequests.clear();
|
|
}
|
|
};
|
|
ColorAlphaSplitter.forceCpu = true;
|
|
var colorAlphaSplitterWorkerCode = () => {
|
|
let cpuSourceBuffer = null;
|
|
let chain = Promise.resolve();
|
|
self.addEventListener("message", (event) => {
|
|
const { id, sourceFrame } = event.data;
|
|
chain = chain.then(async () => {
|
|
try {
|
|
const { colorFrame, alphaFrame } = await split(sourceFrame);
|
|
self.postMessage({ id, colorFrame, alphaFrame }, { transfer: [colorFrame, alphaFrame] });
|
|
} catch (error) {
|
|
self.postMessage({ id, error: error.message });
|
|
} finally {
|
|
sourceFrame.close();
|
|
}
|
|
});
|
|
});
|
|
const split = async (sourceFrame) => {
|
|
const format = sourceFrame.format;
|
|
if (!format) {
|
|
throw new Error("CPU color/alpha splitting requires a known VideoFrame format.");
|
|
}
|
|
const width = sourceFrame.codedWidth;
|
|
const height = sourceFrame.codedHeight;
|
|
const sourceSize = sourceFrame.allocationSize();
|
|
if (!cpuSourceBuffer || cpuSourceBuffer.byteLength !== sourceSize) {
|
|
cpuSourceBuffer = new Uint8Array(sourceSize);
|
|
}
|
|
await sourceFrame.copyTo(cpuSourceBuffer);
|
|
if (format === "RGBA" || format === "BGRA") {
|
|
return splitInterleavedRgba(cpuSourceBuffer, width, height, format, sourceFrame);
|
|
} else if (format === "I420A" || format === "I420AP10" || format === "I420AP12" || format === "I422A" || format === "I422AP10" || format === "I422AP12" || format === "I444A" || format === "I444AP10" || format === "I444AP12") {
|
|
return splitPlanarYuvA(cpuSourceBuffer, width, height, format, sourceFrame);
|
|
}
|
|
throw new Error(\`CPU color/alpha splitting does not support format '\${format}'.\`);
|
|
};
|
|
const splitInterleavedRgba = (source, width, height, format, sourceFrame) => {
|
|
const pixelCount = width * height;
|
|
const chromaW = Math.ceil(width / 2);
|
|
const chromaH = Math.ceil(height / 2);
|
|
const alphaSize = pixelCount + chromaW * chromaH * 2;
|
|
const alphaBuffer = new Uint8Array(alphaSize);
|
|
for (let i2 = 0, j = 3; i2 < pixelCount; i2++, j += 4) {
|
|
alphaBuffer[i2] = source[j];
|
|
}
|
|
alphaBuffer.fill(128, pixelCount);
|
|
const colorFrame = new VideoFrame(source, {
|
|
format: format === "RGBA" ? "RGBX" : "BGRX",
|
|
codedWidth: width,
|
|
codedHeight: height,
|
|
timestamp: sourceFrame.timestamp,
|
|
duration: sourceFrame.duration ?? void 0
|
|
// No transfer!
|
|
});
|
|
const alphaInit = {
|
|
format: "I420",
|
|
codedWidth: width,
|
|
codedHeight: height,
|
|
timestamp: sourceFrame.timestamp,
|
|
duration: sourceFrame.duration ?? void 0,
|
|
transfer: [alphaBuffer.buffer]
|
|
};
|
|
const alphaFrame = new VideoFrame(alphaBuffer, alphaInit);
|
|
return { colorFrame, alphaFrame };
|
|
};
|
|
const splitPlanarYuvA = (source, width, height, format, sourceFrame) => {
|
|
const is10 = format.includes("P10");
|
|
const is12 = format.includes("P12");
|
|
const bytesPerSample = is10 || is12 ? 2 : 1;
|
|
let chromaW;
|
|
let chromaH;
|
|
if (format.startsWith("I420")) {
|
|
chromaW = Math.ceil(width / 2);
|
|
chromaH = Math.ceil(height / 2);
|
|
} else if (format.startsWith("I422")) {
|
|
chromaW = Math.ceil(width / 2);
|
|
chromaH = height;
|
|
} else {
|
|
chromaW = width;
|
|
chromaH = height;
|
|
}
|
|
const ySamples = width * height;
|
|
const uvSamples = chromaW * chromaH;
|
|
const yBytes = ySamples * bytesPerSample;
|
|
const uvBytes = uvSamples * bytesPerSample;
|
|
const aBytes = ySamples * bytesPerSample;
|
|
const colorBytes = yBytes + uvBytes * 2;
|
|
const colorFormat = format.replace("A", "");
|
|
const alphaChromaW = Math.ceil(width / 2);
|
|
const alphaChromaH = Math.ceil(height / 2);
|
|
const alphaUvSamples = alphaChromaW * alphaChromaH;
|
|
const alphaUvBytes = alphaUvSamples * bytesPerSample;
|
|
const alphaSize = aBytes + 2 * alphaUvBytes;
|
|
const alphaBuffer = new Uint8Array(alphaSize);
|
|
const aPlaneStart = colorBytes;
|
|
alphaBuffer.set(source.subarray(aPlaneStart, aPlaneStart + aBytes), 0);
|
|
const uvOffset = aBytes;
|
|
const neutralChroma = is10 ? 512 : is12 ? 2048 : 128;
|
|
if (bytesPerSample === 1) {
|
|
alphaBuffer.fill(neutralChroma, uvOffset);
|
|
} else {
|
|
const uvView = new Uint16Array(alphaBuffer.buffer, uvOffset, 2 * alphaUvSamples);
|
|
uvView.fill(neutralChroma);
|
|
}
|
|
const alphaFormat = is10 ? "I420P10" : is12 ? "I420P12" : "I420";
|
|
const colorFrame = new VideoFrame(source.subarray(0, colorBytes), {
|
|
format: colorFormat,
|
|
codedWidth: width,
|
|
codedHeight: height,
|
|
timestamp: sourceFrame.timestamp,
|
|
duration: sourceFrame.duration ?? void 0
|
|
});
|
|
const alphaInit = {
|
|
format: alphaFormat,
|
|
codedWidth: width,
|
|
codedHeight: height,
|
|
timestamp: sourceFrame.timestamp,
|
|
duration: sourceFrame.duration ?? void 0,
|
|
transfer: [alphaBuffer.buffer]
|
|
};
|
|
const alphaFrame = new VideoFrame(alphaBuffer, alphaInit);
|
|
return { colorFrame, alphaFrame };
|
|
};
|
|
};
|
|
var VideoSampleSource = class extends VideoSource {
|
|
/**
|
|
* Creates a new {@link VideoSampleSource} whose samples are encoded according to the specified
|
|
* {@link VideoEncodingConfig}.
|
|
*/
|
|
constructor(encodingConfig) {
|
|
validateVideoEncodingConfig(encodingConfig);
|
|
super(encodingConfig.codec);
|
|
this._encoder = new VideoEncoderWrapper(this, encodingConfig);
|
|
}
|
|
/**
|
|
* Encodes a video sample (frame) and then adds it to the output.
|
|
*
|
|
* @returns A Promise that resolves once the output is ready to receive more samples. You should await this Promise
|
|
* to respect writer and encoder backpressure.
|
|
*/
|
|
add(videoSample, encodeOptions) {
|
|
if (!(videoSample instanceof VideoSample)) {
|
|
throw new TypeError("videoSample must be a VideoSample.");
|
|
}
|
|
return this._encoder.add(videoSample, false, encodeOptions);
|
|
}
|
|
/** @internal */
|
|
_flushAndClose(forceClose) {
|
|
return this._encoder.flushAndClose(forceClose);
|
|
}
|
|
};
|
|
var AudioSource = class extends MediaSource {
|
|
/** Internal constructor. */
|
|
constructor(codec) {
|
|
super();
|
|
this._connectedTrack = null;
|
|
if (!AUDIO_CODECS.includes(codec)) {
|
|
throw new TypeError(\`Invalid audio codec '\${codec}'. Must be one of: \${AUDIO_CODECS.join(", ")}.\`);
|
|
}
|
|
this._codec = codec;
|
|
}
|
|
};
|
|
var SubtitleSource = class extends MediaSource {
|
|
/** Internal constructor. */
|
|
constructor(codec) {
|
|
super();
|
|
this._connectedTrack = null;
|
|
if (!SUBTITLE_CODECS.includes(codec)) {
|
|
throw new TypeError(\`Invalid subtitle codec '\${codec}'. Must be one of: \${SUBTITLE_CODECS.join(", ")}.\`);
|
|
}
|
|
this._codec = codec;
|
|
}
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/output-format.js
|
|
var OutputFormat = class {
|
|
/** Returns a list of video codecs that this output format can contain. */
|
|
getSupportedVideoCodecs() {
|
|
return this.getSupportedCodecs().filter((codec) => VIDEO_CODECS.includes(codec));
|
|
}
|
|
/** Returns a list of audio codecs that this output format can contain. */
|
|
getSupportedAudioCodecs() {
|
|
return this.getSupportedCodecs().filter((codec) => AUDIO_CODECS.includes(codec));
|
|
}
|
|
/** Returns a list of subtitle codecs that this output format can contain. */
|
|
getSupportedSubtitleCodecs() {
|
|
return this.getSupportedCodecs().filter((codec) => SUBTITLE_CODECS.includes(codec));
|
|
}
|
|
/** @internal */
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
_codecUnsupportedHint(codec) {
|
|
return "";
|
|
}
|
|
};
|
|
var IsobmffOutputFormat = class extends OutputFormat {
|
|
/** Internal constructor. */
|
|
constructor(options = {}) {
|
|
if (!options || typeof options !== "object") {
|
|
throw new TypeError("options must be an object.");
|
|
}
|
|
if (options.fastStart !== void 0 && ![false, "in-memory", "reserve", "fragmented"].includes(options.fastStart)) {
|
|
throw new TypeError("options.fastStart, when provided, must be false, 'in-memory', 'reserve', or 'fragmented'.");
|
|
}
|
|
if (options.minimumFragmentDuration !== void 0 && (!Number.isFinite(options.minimumFragmentDuration) || options.minimumFragmentDuration < 0)) {
|
|
throw new TypeError("options.minimumFragmentDuration, when provided, must be a non-negative number.");
|
|
}
|
|
if (options.onFtyp !== void 0 && typeof options.onFtyp !== "function") {
|
|
throw new TypeError("options.onFtyp, when provided, must be a function.");
|
|
}
|
|
if (options.onMoov !== void 0 && typeof options.onMoov !== "function") {
|
|
throw new TypeError("options.onMoov, when provided, must be a function.");
|
|
}
|
|
if (options.onMdat !== void 0 && typeof options.onMdat !== "function") {
|
|
throw new TypeError("options.onMdat, when provided, must be a function.");
|
|
}
|
|
if (options.onMoof !== void 0 && typeof options.onMoof !== "function") {
|
|
throw new TypeError("options.onMoof, when provided, must be a function.");
|
|
}
|
|
if (options.metadataFormat !== void 0 && !["mdir", "mdta", "udta", "auto"].includes(options.metadataFormat)) {
|
|
throw new TypeError("options.metadataFormat, when provided, must be either 'auto', 'mdir', 'mdta', or 'udta'.");
|
|
}
|
|
super();
|
|
this._options = options;
|
|
}
|
|
getSupportedTrackCounts() {
|
|
const max = 2 ** 32 - 1;
|
|
return {
|
|
video: { min: 0, max },
|
|
audio: { min: 0, max },
|
|
subtitle: { min: 0, max },
|
|
total: { min: 1, max }
|
|
};
|
|
}
|
|
get supportsVideoRotationMetadata() {
|
|
return true;
|
|
}
|
|
get supportsTimestampedMediaData() {
|
|
return true;
|
|
}
|
|
/** @internal */
|
|
_createMuxer(output) {
|
|
return new IsobmffMuxer(output, this);
|
|
}
|
|
};
|
|
var Mp4OutputFormat = class extends IsobmffOutputFormat {
|
|
/** Creates a new {@link Mp4OutputFormat} configured with the specified \`options\`. */
|
|
constructor(options) {
|
|
super(options);
|
|
}
|
|
/** @internal */
|
|
get _name() {
|
|
return "MP4";
|
|
}
|
|
get fileExtension() {
|
|
return ".mp4";
|
|
}
|
|
get mimeType() {
|
|
return "video/mp4";
|
|
}
|
|
getSupportedCodecs() {
|
|
return [
|
|
...VIDEO_CODECS,
|
|
...NON_PCM_AUDIO_CODECS,
|
|
// These are supported via ISO/IEC 23003-5:
|
|
"pcm-s16",
|
|
"pcm-s16be",
|
|
"pcm-s24",
|
|
"pcm-s24be",
|
|
"pcm-s32",
|
|
"pcm-s32be",
|
|
"pcm-f32",
|
|
"pcm-f32be",
|
|
"pcm-f64",
|
|
"pcm-f64be",
|
|
...SUBTITLE_CODECS
|
|
];
|
|
}
|
|
/** @internal */
|
|
_codecUnsupportedHint(codec) {
|
|
if (new MovOutputFormat().getSupportedCodecs().includes(codec)) {
|
|
return " Switching to MOV will grant support for this codec.";
|
|
}
|
|
return "";
|
|
}
|
|
};
|
|
var CmafOutputFormat = class extends IsobmffOutputFormat {
|
|
/** Creates a new {@link CmafOutputFormat} configured with the specified \`options\`. */
|
|
constructor(options) {
|
|
super(options);
|
|
}
|
|
/** @internal */
|
|
get _name() {
|
|
return "CMAF";
|
|
}
|
|
get fileExtension() {
|
|
return ".m4s";
|
|
}
|
|
get mimeType() {
|
|
return "video/mp4";
|
|
}
|
|
getSupportedCodecs() {
|
|
return [
|
|
...VIDEO_CODECS,
|
|
...NON_PCM_AUDIO_CODECS,
|
|
// These are supported via ISO/IEC 23003-5:
|
|
"pcm-s16",
|
|
"pcm-s16be",
|
|
"pcm-s24",
|
|
"pcm-s24be",
|
|
"pcm-s32",
|
|
"pcm-s32be",
|
|
"pcm-f32",
|
|
"pcm-f32be",
|
|
"pcm-f64",
|
|
"pcm-f64be",
|
|
...SUBTITLE_CODECS
|
|
];
|
|
}
|
|
};
|
|
var MovOutputFormat = class extends IsobmffOutputFormat {
|
|
/** Creates a new {@link MovOutputFormat} configured with the specified \`options\`. */
|
|
constructor(options) {
|
|
super(options);
|
|
}
|
|
/** @internal */
|
|
get _name() {
|
|
return "MOV";
|
|
}
|
|
get fileExtension() {
|
|
return ".mov";
|
|
}
|
|
get mimeType() {
|
|
return "video/quicktime";
|
|
}
|
|
getSupportedCodecs() {
|
|
return [
|
|
...VIDEO_CODECS,
|
|
...AUDIO_CODECS
|
|
];
|
|
}
|
|
/** @internal */
|
|
_codecUnsupportedHint(codec) {
|
|
if (new Mp4OutputFormat().getSupportedCodecs().includes(codec)) {
|
|
return " Switching to MP4 will grant support for this codec.";
|
|
}
|
|
return "";
|
|
}
|
|
};
|
|
var MkvOutputFormat = class extends OutputFormat {
|
|
/** Creates a new {@link MkvOutputFormat} configured with the specified \`options\`. */
|
|
constructor(options = {}) {
|
|
if (!options || typeof options !== "object") {
|
|
throw new TypeError("options must be an object.");
|
|
}
|
|
if (options.appendOnly !== void 0 && typeof options.appendOnly !== "boolean") {
|
|
throw new TypeError("options.appendOnly, when provided, must be a boolean.");
|
|
}
|
|
if (options.minimumClusterDuration !== void 0 && (!Number.isFinite(options.minimumClusterDuration) || options.minimumClusterDuration < 0)) {
|
|
throw new TypeError("options.minimumClusterDuration, when provided, must be a non-negative number.");
|
|
}
|
|
if (options.onEbmlHeader !== void 0 && typeof options.onEbmlHeader !== "function") {
|
|
throw new TypeError("options.onEbmlHeader, when provided, must be a function.");
|
|
}
|
|
if (options.onSegmentHeader !== void 0 && typeof options.onSegmentHeader !== "function") {
|
|
throw new TypeError("options.onHeader, when provided, must be a function.");
|
|
}
|
|
if (options.onCluster !== void 0 && typeof options.onCluster !== "function") {
|
|
throw new TypeError("options.onCluster, when provided, must be a function.");
|
|
}
|
|
super();
|
|
this._options = options;
|
|
}
|
|
/** @internal */
|
|
_createMuxer(output) {
|
|
return new MatroskaMuxer(output, this);
|
|
}
|
|
/** @internal */
|
|
get _name() {
|
|
return "Matroska";
|
|
}
|
|
getSupportedTrackCounts() {
|
|
const max = 127;
|
|
return {
|
|
video: { min: 0, max },
|
|
audio: { min: 0, max },
|
|
subtitle: { min: 0, max },
|
|
total: { min: 1, max }
|
|
};
|
|
}
|
|
get fileExtension() {
|
|
return ".mkv";
|
|
}
|
|
get mimeType() {
|
|
return "video/x-matroska";
|
|
}
|
|
getSupportedCodecs() {
|
|
return [
|
|
...VIDEO_CODECS,
|
|
...NON_PCM_AUDIO_CODECS,
|
|
...PCM_AUDIO_CODECS.filter((codec) => !["pcm-s8", "pcm-f32be", "pcm-f64be", "ulaw", "alaw"].includes(codec)),
|
|
...SUBTITLE_CODECS
|
|
];
|
|
}
|
|
get supportsVideoRotationMetadata() {
|
|
return false;
|
|
}
|
|
get supportsTimestampedMediaData() {
|
|
return true;
|
|
}
|
|
};
|
|
var WebMOutputFormat = class extends MkvOutputFormat {
|
|
/** Creates a new {@link WebMOutputFormat} configured with the specified \`options\`. */
|
|
constructor(options) {
|
|
super(options);
|
|
}
|
|
getSupportedCodecs() {
|
|
return [
|
|
...VIDEO_CODECS.filter((codec) => ["vp8", "vp9", "av1"].includes(codec)),
|
|
...AUDIO_CODECS.filter((codec) => ["opus", "vorbis"].includes(codec)),
|
|
...SUBTITLE_CODECS
|
|
];
|
|
}
|
|
/** @internal */
|
|
get _name() {
|
|
return "WebM";
|
|
}
|
|
get fileExtension() {
|
|
return ".webm";
|
|
}
|
|
get mimeType() {
|
|
return "video/webm";
|
|
}
|
|
/** @internal */
|
|
_codecUnsupportedHint(codec) {
|
|
if (new MkvOutputFormat().getSupportedCodecs().includes(codec)) {
|
|
return " Switching to MKV will grant support for this codec.";
|
|
}
|
|
return "";
|
|
}
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/output.js
|
|
var ALL_TRACK_TYPES = ["video", "audio", "subtitle"];
|
|
var OutputTrack = class _OutputTrack {
|
|
/** @internal */
|
|
constructor(id, output, type, source, metadata) {
|
|
this.id = id;
|
|
this.output = output;
|
|
this.type = type;
|
|
this.source = source;
|
|
this.metadata = metadata;
|
|
}
|
|
/** Returns true if and only if this track is a video track. */
|
|
isVideoTrack() {
|
|
return this.type === "video";
|
|
}
|
|
/** Returns true if and only if this track is an audio track. */
|
|
isAudioTrack() {
|
|
return this.type === "audio";
|
|
}
|
|
/** Returns true if and only if this track is a subtitle track. */
|
|
isSubtitleTrack() {
|
|
return this.type === "subtitle";
|
|
}
|
|
/**
|
|
* Returns true if and only if this track can be paired with the given other track. Pairability can be set using
|
|
* the {@link BaseTrackMetadata.group} option.
|
|
*/
|
|
canBePairedWith(other) {
|
|
if (!(other instanceof _OutputTrack)) {
|
|
throw new TypeError("other must be an OutputTrack.");
|
|
}
|
|
if (this === other) {
|
|
return false;
|
|
}
|
|
const thisGroups = toArray(this.metadata.group);
|
|
const otherGroups = toArray(other.metadata.group);
|
|
for (const aGroup of thisGroups) {
|
|
const pairableInSameGroup = this.type !== other.type && otherGroups.some((bGroup) => aGroup === bGroup);
|
|
if (pairableInSameGroup) {
|
|
return true;
|
|
}
|
|
const pairableAcrossGroups = otherGroups.some((bGroup) => aGroup._pairedGroups.has(bGroup));
|
|
if (pairableAcrossGroups) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
var OutputVideoTrack = class extends OutputTrack {
|
|
/** @internal */
|
|
constructor(id, output, source, metadata) {
|
|
super(id, output, "video", source, metadata);
|
|
}
|
|
};
|
|
var OutputAudioTrack = class extends OutputTrack {
|
|
/** @internal */
|
|
constructor(id, output, source, metadata) {
|
|
super(id, output, "audio", source, metadata);
|
|
}
|
|
};
|
|
var OutputSubtitleTrack = class extends OutputTrack {
|
|
/** @internal */
|
|
constructor(id, output, source, metadata) {
|
|
super(id, output, "subtitle", source, metadata);
|
|
}
|
|
};
|
|
var OutputTrackGroup = class _OutputTrackGroup {
|
|
/** Creates a new {@link OutputTrackGroup}. */
|
|
constructor() {
|
|
this._pairedGroups = /* @__PURE__ */ new Set();
|
|
}
|
|
/**
|
|
* Marks this group as being pairable with another group, symmetrically. Output tracks where each track is assigned
|
|
* to one half of a group pairing are then considered pairable.
|
|
*
|
|
* You cannot pair a group with itself.
|
|
*/
|
|
pairWith(other) {
|
|
if (!(other instanceof _OutputTrackGroup)) {
|
|
throw new TypeError("other must be an OutputTrackGroup.");
|
|
}
|
|
if (this === other) {
|
|
throw new TypeError("Cannot pair a group with itself.");
|
|
}
|
|
this._pairedGroups.add(other);
|
|
other._pairedGroups.add(this);
|
|
}
|
|
};
|
|
var validateBaseTrackMetadata = (metadata) => {
|
|
if (!metadata || typeof metadata !== "object") {
|
|
throw new TypeError("metadata must be an object.");
|
|
}
|
|
if (metadata.languageCode !== void 0 && !isIso639Dash2LanguageCode(metadata.languageCode)) {
|
|
throw new TypeError("metadata.languageCode, when provided, must be a three-letter, ISO 639-2/T language code.");
|
|
}
|
|
if (metadata.name !== void 0 && typeof metadata.name !== "string") {
|
|
throw new TypeError("metadata.name, when provided, must be a string.");
|
|
}
|
|
if (metadata.disposition !== void 0) {
|
|
validateTrackDisposition(metadata.disposition);
|
|
}
|
|
if (metadata.maximumPacketCount !== void 0 && (!Number.isInteger(metadata.maximumPacketCount) || metadata.maximumPacketCount < 0)) {
|
|
throw new TypeError("metadata.maximumPacketCount, when provided, must be a non-negative integer.");
|
|
}
|
|
if (metadata.group !== void 0 && !(metadata.group instanceof OutputTrackGroup) && (!Array.isArray(metadata.group) || metadata.group.some((group) => !(group instanceof OutputTrackGroup)))) {
|
|
throw new TypeError("metadata.group, when provided, must be an OutputTrackGroup instance or an array of OutputTrackGroup instances.");
|
|
}
|
|
};
|
|
var Output = class extends EventEmitter {
|
|
/**
|
|
* The target to which the root file will be written. Throws when using {@link PathedTarget} with an async callback;
|
|
* prefer the \`'target'\` event for those cases.
|
|
*/
|
|
get target() {
|
|
const errorMessage = "Output.target cannot be used when using PathedTarget with an async callback. Use the 'target' event instead.";
|
|
if (this._rootTargetPromise) {
|
|
throw new TypeError(errorMessage);
|
|
}
|
|
const rootTargetResult = this._getRootTarget();
|
|
if (rootTargetResult instanceof Promise) {
|
|
throw new TypeError(errorMessage);
|
|
}
|
|
return rootTargetResult;
|
|
}
|
|
/**
|
|
* Creates a new instance of {@link Output} which can then be used to create a new media file according to the
|
|
* specified {@link OutputOptions}.
|
|
*/
|
|
constructor(options) {
|
|
super();
|
|
this.state = "pending";
|
|
this.defaultTrackGroup = new OutputTrackGroup();
|
|
this._onFinalize = null;
|
|
this._unfinalizedTargets = /* @__PURE__ */ new Set();
|
|
this._rootWriterPromise = null;
|
|
this._tracks = [];
|
|
this._startPromise = null;
|
|
this._cancelPromise = null;
|
|
this._finalizePromise = null;
|
|
this._mutex = new AsyncMutex();
|
|
this._metadataTags = {};
|
|
this._rootTarget = null;
|
|
this._rootTargetPromise = null;
|
|
this._firstMediaStreamTimestamp = null;
|
|
if (!options || typeof options !== "object") {
|
|
throw new TypeError("options must be an object.");
|
|
}
|
|
if (!(options.format instanceof OutputFormat)) {
|
|
throw new TypeError("options.format must be an OutputFormat.");
|
|
}
|
|
if (!(options.target instanceof Target || options.target instanceof PathedTarget)) {
|
|
throw new TypeError("options.target must be a Target or a PathedTarget.");
|
|
}
|
|
if (options.target instanceof Target) {
|
|
this._rememberTarget(options.target);
|
|
}
|
|
if (options.initTarget !== void 0 && !(options.initTarget instanceof Target) && typeof options.initTarget !== "function") {
|
|
throw new Error("options.initTarget, when provided, must be a Target or a function that returns or resolves to a Target.");
|
|
}
|
|
if (options.onFinalize !== void 0 && typeof options.onFinalize !== "function") {
|
|
throw new TypeError("options.onFinalize, when provided, must be a function.");
|
|
}
|
|
this.format = options.format;
|
|
this._target = options.target;
|
|
this._onFinalize = options.onFinalize ?? null;
|
|
this._initTarget = options.initTarget ?? null;
|
|
if (this._initTarget instanceof Target) {
|
|
this._rememberTarget(this._initTarget);
|
|
}
|
|
this._muxer = options.format._createMuxer(this);
|
|
}
|
|
/** @internal */
|
|
_getTargetValidated(request) {
|
|
assert(this._target instanceof PathedTarget);
|
|
const result = this._target.getTarget(request);
|
|
const handleResult = (result2) => {
|
|
if (!(result2 instanceof Target)) {
|
|
throw new TypeError("getTarget must return a Target.");
|
|
}
|
|
return result2;
|
|
};
|
|
if (result instanceof Promise) {
|
|
return result.then(handleResult);
|
|
} else {
|
|
return handleResult(result);
|
|
}
|
|
}
|
|
/** @internal */
|
|
async _getTarget(request) {
|
|
assert(this._target instanceof PathedTarget);
|
|
const target = await this._getTargetValidated(request);
|
|
this._emit("target", { target, request, isRoot: request.isRoot });
|
|
if (this.state === "canceled") {
|
|
await target._close();
|
|
} else {
|
|
this._rememberTarget(target);
|
|
}
|
|
return target;
|
|
}
|
|
/** @internal */
|
|
_rememberTarget(target) {
|
|
this._unfinalizedTargets.add(target);
|
|
target.on("finalized", () => this._unfinalizedTargets.delete(target), { once: true });
|
|
}
|
|
/** @internal */
|
|
async _getInitTarget() {
|
|
assert(this._initTarget !== null);
|
|
if (this._initTarget instanceof Target) {
|
|
return this._initTarget;
|
|
}
|
|
const target = await this._initTarget();
|
|
if (this.state === "canceled") {
|
|
await target._close();
|
|
} else {
|
|
this._rememberTarget(target);
|
|
}
|
|
return target;
|
|
}
|
|
/** @internal */
|
|
_hasInitTarget() {
|
|
return this._initTarget !== null;
|
|
}
|
|
/** @internal */
|
|
_getRootTarget() {
|
|
if (this._rootTarget) {
|
|
return this._rootTarget;
|
|
}
|
|
if (this._rootTargetPromise) {
|
|
return this._rootTargetPromise;
|
|
}
|
|
if (this._target instanceof Target) {
|
|
this._emit("target", { target: this._target, request: null, isRoot: true });
|
|
this._rootTarget = this._target;
|
|
return this._target;
|
|
}
|
|
const request = {
|
|
path: this._target.rootPath,
|
|
isRoot: true,
|
|
mimeType: this.format.mimeType
|
|
};
|
|
const result = this._getTargetValidated(request);
|
|
const handleResult = (target) => {
|
|
if (this.state === "canceled") {
|
|
void target._close();
|
|
} else {
|
|
this._rememberTarget(target);
|
|
}
|
|
this._emit("target", { target, request, isRoot: true });
|
|
this._rootTarget = target;
|
|
return target;
|
|
};
|
|
if (result instanceof Promise) {
|
|
return this._rootTargetPromise = result.then(handleResult);
|
|
} else {
|
|
return handleResult(result);
|
|
}
|
|
}
|
|
/** @internal */
|
|
_getRootWriter(isMonotonic) {
|
|
return this._rootWriterPromise ??= (async () => {
|
|
const target = await this._getRootTarget();
|
|
const writer = new Writer(target, typeof isMonotonic === "boolean" ? isMonotonic : isMonotonic(target));
|
|
writer.start();
|
|
return writer;
|
|
})();
|
|
}
|
|
/** Adds a video track to the output with the given source. Can only be called before the output is started. */
|
|
addVideoTrack(source, metadata = {}) {
|
|
if (!(source instanceof VideoSource)) {
|
|
throw new TypeError("source must be a VideoSource.");
|
|
}
|
|
validateBaseTrackMetadata(metadata);
|
|
if (metadata.rotation !== void 0 && ![0, 90, 180, 270].includes(metadata.rotation)) {
|
|
throw new TypeError(\`Invalid video rotation: \${metadata.rotation}. Has to be 0, 90, 180 or 270.\`);
|
|
}
|
|
if (!this.format.supportsVideoRotationMetadata && metadata.rotation) {
|
|
throw new Error(\`\${this.format._name} does not support video rotation metadata.\`);
|
|
}
|
|
if (metadata.frameRate !== void 0 && (!Number.isFinite(metadata.frameRate) || metadata.frameRate <= 0)) {
|
|
throw new TypeError(\`Invalid video frame rate: \${metadata.frameRate}. Must be a positive number.\`);
|
|
}
|
|
const metadataCopy = { ...metadata };
|
|
metadataCopy.group ??= this.defaultTrackGroup;
|
|
return this._addTrack(new OutputVideoTrack(this._tracks.length + 1, this, source, metadataCopy));
|
|
}
|
|
/** Adds an audio track to the output with the given source. Can only be called before the output is started. */
|
|
addAudioTrack(source, metadata = {}) {
|
|
if (!(source instanceof AudioSource)) {
|
|
throw new TypeError("source must be an AudioSource.");
|
|
}
|
|
validateBaseTrackMetadata(metadata);
|
|
const metadataCopy = { ...metadata };
|
|
metadataCopy.group ??= this.defaultTrackGroup;
|
|
return this._addTrack(new OutputAudioTrack(this._tracks.length + 1, this, source, metadataCopy));
|
|
}
|
|
/** Adds a subtitle track to the output with the given source. Can only be called before the output is started. */
|
|
addSubtitleTrack(source, metadata = {}) {
|
|
if (!(source instanceof SubtitleSource)) {
|
|
throw new TypeError("source must be a SubtitleSource.");
|
|
}
|
|
validateBaseTrackMetadata(metadata);
|
|
const metadataCopy = { ...metadata };
|
|
metadataCopy.group ??= this.defaultTrackGroup;
|
|
return this._addTrack(new OutputSubtitleTrack(this._tracks.length + 1, this, source, metadataCopy));
|
|
}
|
|
/**
|
|
* Sets descriptive metadata tags about the media file, such as title, author, date, or cover art. When called
|
|
* multiple times, only the metadata from the last call will be used.
|
|
*
|
|
* Can only be called before the output is started.
|
|
*/
|
|
setMetadataTags(tags) {
|
|
validateMetadataTags(tags);
|
|
if (this.state !== "pending") {
|
|
throw new Error("Cannot set metadata tags after output has been started or canceled.");
|
|
}
|
|
this._metadataTags = tags;
|
|
}
|
|
/** @internal */
|
|
_addTrack(track) {
|
|
if (this.state !== "pending") {
|
|
throw new Error("Cannot add track after output has been started or canceled.");
|
|
}
|
|
if (track.source._connectedTrack) {
|
|
throw new Error("Source is already used for a track.");
|
|
}
|
|
const supportedTrackCounts = this.format.getSupportedTrackCounts();
|
|
const presentTracksOfThisType = this._tracks.reduce((count, t2) => count + (t2.type === track.type ? 1 : 0), 0);
|
|
const maxCount = supportedTrackCounts[track.type].max;
|
|
if (presentTracksOfThisType === maxCount) {
|
|
throw new Error(maxCount === 0 ? \`\${this.format._name} does not support \${track.type} tracks.\` : \`\${this.format._name} does not support more than \${maxCount} \${track.type} track\${maxCount === 1 ? "" : "s"}.\`);
|
|
}
|
|
const maxTotalCount = supportedTrackCounts.total.max;
|
|
if (this._tracks.length === maxTotalCount) {
|
|
throw new Error(\`\${this.format._name} does not support more than \${maxTotalCount} tracks\${maxTotalCount === 1 ? "" : "s"} in total.\`);
|
|
}
|
|
if (track.isVideoTrack()) {
|
|
const supportedVideoCodecs = this.format.getSupportedVideoCodecs();
|
|
if (supportedVideoCodecs.length === 0) {
|
|
throw new Error(\`\${this.format._name} does not support video tracks.\` + this.format._codecUnsupportedHint(track.source._codec));
|
|
} else if (!supportedVideoCodecs.includes(track.source._codec)) {
|
|
throw new Error(\`Codec '\${track.source._codec}' cannot be contained within \${this.format._name}. Supported video codecs are: \${supportedVideoCodecs.map((codec) => \`'\${codec}'\`).join(", ")}.\` + this.format._codecUnsupportedHint(track.source._codec));
|
|
}
|
|
} else if (track.isAudioTrack()) {
|
|
const supportedAudioCodecs = this.format.getSupportedAudioCodecs();
|
|
if (supportedAudioCodecs.length === 0) {
|
|
throw new Error(\`\${this.format._name} does not support audio tracks.\` + this.format._codecUnsupportedHint(track.source._codec));
|
|
} else if (!supportedAudioCodecs.includes(track.source._codec)) {
|
|
throw new Error(\`Codec '\${track.source._codec}' cannot be contained within \${this.format._name}. Supported audio codecs are: \${supportedAudioCodecs.map((codec) => \`'\${codec}'\`).join(", ")}.\` + this.format._codecUnsupportedHint(track.source._codec));
|
|
}
|
|
} else if (track.isSubtitleTrack()) {
|
|
const supportedSubtitleCodecs = this.format.getSupportedSubtitleCodecs();
|
|
if (supportedSubtitleCodecs.length === 0) {
|
|
throw new Error(\`\${this.format._name} does not support subtitle tracks.\` + this.format._codecUnsupportedHint(track.source._codec));
|
|
} else if (!supportedSubtitleCodecs.includes(track.source._codec)) {
|
|
throw new Error(\`Codec '\${track.source._codec}' cannot be contained within \${this.format._name}. Supported subtitle codecs are: \${supportedSubtitleCodecs.map((codec) => \`'\${codec}'\`).join(", ")}.\` + this.format._codecUnsupportedHint(track.source._codec));
|
|
}
|
|
}
|
|
this._tracks.push(track);
|
|
track.source._connectedTrack = track;
|
|
return track;
|
|
}
|
|
/**
|
|
* Starts the creation of the output file. This method should be called after all tracks have been added. Only after
|
|
* the output has started can media samples be added to the tracks.
|
|
*
|
|
* @returns A promise that resolves when the output has successfully started and is ready to receive media samples.
|
|
*/
|
|
async start() {
|
|
const supportedTrackCounts = this.format.getSupportedTrackCounts();
|
|
for (const trackType of ALL_TRACK_TYPES) {
|
|
const presentTracksOfThisType = this._tracks.reduce((count, track) => count + (track.type === trackType ? 1 : 0), 0);
|
|
const minCount = supportedTrackCounts[trackType].min;
|
|
if (presentTracksOfThisType < minCount) {
|
|
throw new Error(minCount === supportedTrackCounts[trackType].max ? \`\${this.format._name} requires exactly \${minCount} \${trackType} track\${minCount === 1 ? "" : "s"}.\` : \`\${this.format._name} requires at least \${minCount} \${trackType} track\${minCount === 1 ? "" : "s"}.\`);
|
|
}
|
|
}
|
|
const totalMinCount = supportedTrackCounts.total.min;
|
|
if (this._tracks.length < totalMinCount) {
|
|
throw new Error(totalMinCount === supportedTrackCounts.total.max ? \`\${this.format._name} requires exactly \${totalMinCount} track\${totalMinCount === 1 ? "" : "s"}.\` : \`\${this.format._name} requires at least \${totalMinCount} track\${totalMinCount === 1 ? "" : "s"}.\`);
|
|
}
|
|
if (this.state === "canceled") {
|
|
throw new Error("Output has been canceled.");
|
|
}
|
|
if (this._startPromise) {
|
|
Logging._warn("Output has already been started.");
|
|
return this._startPromise;
|
|
}
|
|
return this._startPromise = (async () => {
|
|
this.state = "started";
|
|
const release = await this._mutex.acquire();
|
|
try {
|
|
await this._muxer.start();
|
|
const promises = this._tracks.map((track) => track.source._start());
|
|
await Promise.all(promises);
|
|
} finally {
|
|
release();
|
|
}
|
|
})();
|
|
}
|
|
/**
|
|
* Resolves with the full MIME type of the output file, including track codecs.
|
|
*
|
|
* The returned promise will resolve only once the precise codec strings of all tracks are known.
|
|
*/
|
|
getMimeType() {
|
|
return this._muxer.getMimeType();
|
|
}
|
|
/**
|
|
* Cancels the creation of the output file, releasing internal resources like encoders and preventing further
|
|
* samples from being added.
|
|
*
|
|
* @returns A promise that resolves once all internal resources have been released.
|
|
*/
|
|
async cancel() {
|
|
if (this._cancelPromise) {
|
|
Logging._warn("Output has already been canceled.");
|
|
return this._cancelPromise;
|
|
} else if (this.state === "finalizing" || this.state === "finalized") {
|
|
if (this.state === "finalized") {
|
|
Logging._warn("Output has already been finalized.");
|
|
}
|
|
return;
|
|
}
|
|
return this._cancelPromise = (async () => {
|
|
this.state = "canceled";
|
|
const release = await this._mutex.acquire();
|
|
try {
|
|
const promises = this._tracks.map((x) => x.source._flushOrWaitForOngoingClose(true));
|
|
await Promise.all(promises);
|
|
await Promise.all([...this._unfinalizedTargets].map((target) => target._close()));
|
|
this._unfinalizedTargets.clear();
|
|
} finally {
|
|
release();
|
|
}
|
|
})();
|
|
}
|
|
/**
|
|
* Finalizes the output file. This method must be called after all media samples across all tracks have been added.
|
|
* Once the Promise returned by this method completes, the output file is ready.
|
|
*/
|
|
async finalize() {
|
|
if (this.state === "pending") {
|
|
throw new Error("Cannot finalize before starting.");
|
|
}
|
|
if (this.state === "canceled") {
|
|
throw new Error("Cannot finalize after canceling.");
|
|
}
|
|
if (this._finalizePromise) {
|
|
Logging._warn("Output has already been finalized.");
|
|
return this._finalizePromise;
|
|
}
|
|
return this._finalizePromise = (async () => {
|
|
this.state = "finalizing";
|
|
const release = await this._mutex.acquire();
|
|
try {
|
|
const promises = this._tracks.map((x) => x.source._flushOrWaitForOngoingClose(false));
|
|
await Promise.all(promises);
|
|
await this._muxer.finalize();
|
|
if (this._rootWriterPromise) {
|
|
const rootWriter = await this._rootWriterPromise;
|
|
if (!rootWriter.finalized) {
|
|
await rootWriter.flush();
|
|
await rootWriter.finalize();
|
|
}
|
|
}
|
|
if (this._onFinalize) {
|
|
await this._onFinalize();
|
|
}
|
|
this.state = "finalized";
|
|
} finally {
|
|
release();
|
|
}
|
|
})();
|
|
}
|
|
};
|
|
|
|
// node_modules/mediabunny/dist/modules/src/index.js
|
|
var MEDIABUNNY_LOADED_SYMBOL = /* @__PURE__ */ Symbol.for("mediabunny loaded");
|
|
if (globalThis[MEDIABUNNY_LOADED_SYMBOL]) {
|
|
Logging._error("[WARNING]\\nMediabunny was loaded twice. This will likely cause Mediabunny not to work correctly. Check if multiple dependencies are importing different versions of Mediabunny, or if something is being bundled incorrectly.");
|
|
}
|
|
globalThis[MEDIABUNNY_LOADED_SYMBOL] = true;
|
|
|
|
// packages/video-conversion/src/index.ts
|
|
var inProgressOperations = /* @__PURE__ */ new Set();
|
|
var GIF_DEFAULT_FRAME_DURATION_US = 1e5;
|
|
var UNSUPPORTED_ERROR_PREFIX = "Unsupported";
|
|
var SIZE_LIMIT_ERROR_PREFIX = \`\${UNSUPPORTED_ERROR_PREFIX}: GIF exceeds maximum conversion size\`;
|
|
var DEFAULT_MAX_TOTAL_PIXELS = 3e8;
|
|
var operationLock = Promise.resolve();
|
|
async function cancelOperations(id) {
|
|
return inProgressOperations.delete(id);
|
|
}
|
|
function padToEven(value) {
|
|
return value % 2 === 0 ? value : value + 1;
|
|
}
|
|
async function convertGifToVideo(id, gifSource, outputMimeType, maxDimensions, maxTotalPixels) {
|
|
inProgressOperations.add(id);
|
|
const previousLock = operationLock;
|
|
let releaseLock = () => {
|
|
};
|
|
operationLock = new Promise((resolve) => {
|
|
releaseLock = resolve;
|
|
});
|
|
try {
|
|
await previousLock;
|
|
if (!inProgressOperations.has(id)) {
|
|
throw new Error("Operation cancelled");
|
|
}
|
|
if (typeof ImageDecoder === "undefined" || typeof VideoEncoder === "undefined") {
|
|
throw new Error(
|
|
\`\${UNSUPPORTED_ERROR_PREFIX}: WebCodecs unavailable\`
|
|
);
|
|
}
|
|
const isWebm = outputMimeType === "video/webm";
|
|
const codec = isWebm ? "vp9" : "avc";
|
|
if (!await canEncodeVideo(codec)) {
|
|
throw new Error(
|
|
\`\${UNSUPPORTED_ERROR_PREFIX}: encoder codec not supported\`
|
|
);
|
|
}
|
|
if (!inProgressOperations.has(id)) {
|
|
throw new Error("Operation cancelled");
|
|
}
|
|
const data = gifSource instanceof ArrayBuffer ? gifSource : await gifSource.arrayBuffer();
|
|
if (!inProgressOperations.has(id)) {
|
|
throw new Error("Operation cancelled");
|
|
}
|
|
const decoder = new ImageDecoder({
|
|
data,
|
|
type: "image/gif"
|
|
});
|
|
try {
|
|
await decoder.tracks.ready;
|
|
if (!inProgressOperations.has(id)) {
|
|
throw new Error("Operation cancelled");
|
|
}
|
|
const track = decoder.tracks.selectedTrack;
|
|
const frameCount = track?.frameCount ?? 0;
|
|
if (frameCount === 0) {
|
|
throw new Error("GIF contains no decodable frames");
|
|
}
|
|
const pixelBudget = maxTotalPixels ?? DEFAULT_MAX_TOTAL_PIXELS;
|
|
if (pixelBudget > 0) {
|
|
const { image: probe } = await decoder.decode({
|
|
frameIndex: 0
|
|
});
|
|
const probeWidth = probe.displayWidth;
|
|
const probeHeight = probe.displayHeight;
|
|
probe.close();
|
|
if (!inProgressOperations.has(id)) {
|
|
throw new Error("Operation cancelled");
|
|
}
|
|
const totalPixels = probeWidth * probeHeight * frameCount;
|
|
if (totalPixels > pixelBudget) {
|
|
throw new Error(
|
|
\`\${SIZE_LIMIT_ERROR_PREFIX} (\${probeWidth}x\${probeHeight} x \${frameCount} frames = \${totalPixels} pixels; limit is \${pixelBudget})\`
|
|
);
|
|
}
|
|
}
|
|
const source = new VideoSampleSource({
|
|
codec,
|
|
bitrate: QUALITY_HIGH,
|
|
/*
|
|
* A sparser key frame cadence than mediabunny's 2s default
|
|
* roughly halves the output size for long GIFs at no
|
|
* encode-time or quality cost. These looping, autoplaying
|
|
* GIF replacements don't need fine seek granularity.
|
|
*/
|
|
keyFrameInterval: 10
|
|
});
|
|
const target = new BufferTarget();
|
|
const output = new Output({
|
|
format: isWebm ? new WebMOutputFormat() : new Mp4OutputFormat(),
|
|
target
|
|
});
|
|
output.addVideoTrack(source);
|
|
await output.start();
|
|
let timestampSec = 0;
|
|
for (let i2 = 0; i2 < frameCount; i2++) {
|
|
if (!inProgressOperations.has(id)) {
|
|
throw new Error("Operation cancelled");
|
|
}
|
|
const { image } = await decoder.decode({ frameIndex: i2 });
|
|
const durationUs = image.duration ?? GIF_DEFAULT_FRAME_DURATION_US;
|
|
const durationSec = durationUs / 1e6;
|
|
const srcW = image.displayWidth;
|
|
const srcH = image.displayHeight;
|
|
let targetW = srcW;
|
|
let targetH = srcH;
|
|
if (maxDimensions && (srcW > maxDimensions || srcH > maxDimensions)) {
|
|
const scale = Math.min(
|
|
maxDimensions / srcW,
|
|
maxDimensions / srcH
|
|
);
|
|
targetW = Math.round(srcW * scale);
|
|
targetH = Math.round(srcH * scale);
|
|
}
|
|
targetW = padToEven(targetW);
|
|
targetH = padToEven(targetH);
|
|
let frameForEncode = image;
|
|
if (targetW !== srcW || targetH !== srcH) {
|
|
const canvas = new OffscreenCanvas(targetW, targetH);
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) {
|
|
throw new Error("Failed to create 2D canvas context");
|
|
}
|
|
ctx.drawImage(image, 0, 0, targetW, targetH);
|
|
frameForEncode = new VideoFrame(canvas, {
|
|
timestamp: Math.round(timestampSec * 1e6),
|
|
duration: durationUs
|
|
});
|
|
image.close();
|
|
}
|
|
const sample = new VideoSample(frameForEncode, {
|
|
timestamp: timestampSec,
|
|
duration: durationSec
|
|
});
|
|
try {
|
|
await source.add(sample);
|
|
} finally {
|
|
sample.close();
|
|
frameForEncode.close();
|
|
}
|
|
timestampSec += durationSec;
|
|
}
|
|
await output.finalize();
|
|
const out = target.buffer;
|
|
if (!out || out.byteLength === 0) {
|
|
throw new Error("Encoder produced empty output");
|
|
}
|
|
return out;
|
|
} finally {
|
|
decoder.close();
|
|
}
|
|
} finally {
|
|
inProgressOperations.delete(id);
|
|
releaseLock();
|
|
}
|
|
}
|
|
|
|
// packages/video-conversion/src/worker.ts
|
|
var api = {
|
|
cancelOperations,
|
|
convertGifToVideo
|
|
};
|
|
expose(api);
|
|
/*! Bundled license information:
|
|
|
|
mediabunny/dist/modules/src/misc.js:
|
|
mediabunny/dist/modules/src/logging.js:
|
|
mediabunny/dist/modules/src/metadata.js:
|
|
mediabunny/dist/modules/shared/bitstream.js:
|
|
mediabunny/dist/modules/shared/aac-misc.js:
|
|
mediabunny/dist/modules/src/codec.js:
|
|
mediabunny/dist/modules/shared/ac3-misc.js:
|
|
mediabunny/dist/modules/src/codec-data.js:
|
|
mediabunny/dist/modules/src/packet.js:
|
|
mediabunny/dist/modules/src/isobmff/isobmff-misc.js:
|
|
mediabunny/dist/modules/src/isobmff/isobmff-reader.js:
|
|
mediabunny/dist/modules/src/matroska/ebml.js:
|
|
mediabunny/dist/modules/src/matroska/matroska-misc.js:
|
|
mediabunny/dist/modules/src/adts/adts-reader.js:
|
|
mediabunny/dist/modules/src/sample.js:
|
|
mediabunny/dist/modules/src/encode.js:
|
|
mediabunny/dist/modules/src/custom-coder.js:
|
|
mediabunny/dist/modules/src/reader.js:
|
|
mediabunny/dist/modules/src/muxer.js:
|
|
mediabunny/dist/modules/src/subtitles.js:
|
|
mediabunny/dist/modules/src/isobmff/isobmff-boxes.js:
|
|
mediabunny/dist/modules/src/writer.js:
|
|
mediabunny/dist/modules/src/target.js:
|
|
mediabunny/dist/modules/src/isobmff/isobmff-muxer.js:
|
|
mediabunny/dist/modules/src/matroska/matroska-muxer.js:
|
|
mediabunny/dist/modules/src/media-source.js:
|
|
mediabunny/dist/modules/src/output-format.js:
|
|
mediabunny/dist/modules/src/output.js:
|
|
mediabunny/dist/modules/src/index.js:
|
|
(*!
|
|
* Copyright (c) 2026-present, Vanilagy and contributors
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
*)
|
|
*/
|
|
`;var B,v,T;function F(){if(v===void 0){let e=new Blob([O],{type:"application/javascript"});T=URL.createObjectURL(e),B=new Worker(T,{type:"module"}),v=_(B)}return v}async function ce(e,n,t,a,i){return F().convertGifToVideo(e,n,t,a,i)}async function de(e){return F().cancelOperations(e)}function le(){v&&(I(v),v=void 0,B=void 0),T&&(URL.revokeObjectURL(T),T=void 0)}export{de as cancelGifToVideoOperations,ce as convertGifToVideo,le as terminateVideoConversionWorker};
|