Spaces:
Running
Running
taurus: 配乐混音、MP4 导出、页面、发布包与 8 张封面
Browse files- .gitattributes +9 -0
- taurus/audio/film.mp3 +3 -0
- taurus/covers/cover-bili-16x9.jpg +3 -0
- taurus/covers/cover-bili-4x3.jpg +3 -0
- taurus/covers/cover-channels-3x4.jpg +3 -0
- taurus/covers/cover-channels-4x3.jpg +3 -0
- taurus/covers/cover-douyin-3x4.jpg +3 -0
- taurus/covers/cover-douyin-4x3.jpg +3 -0
- taurus/covers/cover-xhs-3x4.jpg +3 -0
- taurus/covers/cover-xhs-4x3.jpg +3 -0
- taurus/index.html +82 -0
- taurus/js/encoder.js +79 -0
- taurus/js/main.js +142 -0
- taurus/js/music.js +133 -0
- taurus/js/release.js +66 -0
- taurus/style.css +87 -0
- taurus/tools/mix.mjs +17 -0
- taurus/vendor/mp4-muxer.mjs +1879 -0
.gitattributes
CHANGED
|
@@ -53,3 +53,12 @@ taurus/audio/v05.mp3 filter=lfs diff=lfs merge=lfs -text
|
|
| 53 |
taurus/audio/v01.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 54 |
taurus/audio/v09.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 55 |
taurus/audio/v03.mp3 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
taurus/audio/v01.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 54 |
taurus/audio/v09.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 55 |
taurus/audio/v03.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
taurus/audio/film.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
taurus/covers/cover-bili-16x9.jpg filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
taurus/covers/cover-xhs-3x4.jpg filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
taurus/covers/cover-xhs-4x3.jpg filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
taurus/covers/cover-channels-4x3.jpg filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
taurus/covers/cover-channels-3x4.jpg filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
taurus/covers/cover-bili-4x3.jpg filter=lfs diff=lfs merge=lfs -text
|
| 63 |
+
taurus/covers/cover-douyin-4x3.jpg filter=lfs diff=lfs merge=lfs -text
|
| 64 |
+
taurus/covers/cover-douyin-3x4.jpg filter=lfs diff=lfs merge=lfs -text
|
taurus/audio/film.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4fdb15ed4b044247f5e8edca1a1a2899ce1287533b641284704cd9e494097396
|
| 3 |
+
size 6028686
|
taurus/covers/cover-bili-16x9.jpg
ADDED
|
Git LFS Details
|
taurus/covers/cover-bili-4x3.jpg
ADDED
|
Git LFS Details
|
taurus/covers/cover-channels-3x4.jpg
ADDED
|
Git LFS Details
|
taurus/covers/cover-channels-4x3.jpg
ADDED
|
Git LFS Details
|
taurus/covers/cover-douyin-3x4.jpg
ADDED
|
Git LFS Details
|
taurus/covers/cover-douyin-4x3.jpg
ADDED
|
Git LFS Details
|
taurus/covers/cover-xhs-3x4.jpg
ADDED
|
Git LFS Details
|
taurus/covers/cover-xhs-4x3.jpg
ADDED
|
Git LFS Details
|
taurus/index.html
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
| 6 |
+
<title>金牛座 · 慢慢来,也能抵达美好 | 鎏金珍藏卡</title>
|
| 7 |
+
<meta name="description" content="金牛座原创动画:最古老的星座、毕宿五与昴星团、北宋天关客星、欧罗巴与白牛,以及一张属于你的鎏金珍藏卡。可在浏览器播放并本地导出 MP4。">
|
| 8 |
+
<meta name="theme-color" content="#0c1f1a">
|
| 9 |
+
<meta property="og:title" content="金牛座 · 慢慢来,也能抵达美好">
|
| 10 |
+
<meta property="og:image" content="covers/cover-bili-16x9.jpg">
|
| 11 |
+
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 12 |
+
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,600;0,700;1,700&family=Noto+Sans+SC:wght@400;600;700&family=Noto+Serif+SC:wght@700;900&display=swap" rel="stylesheet">
|
| 13 |
+
<link rel="stylesheet" href="style.css">
|
| 14 |
+
</head>
|
| 15 |
+
<body>
|
| 16 |
+
<header class="hero">
|
| 17 |
+
<a class="home" href="../index.html">← 返回作品集</a>
|
| 18 |
+
<p class="eyebrow">TAURUS · 4.20 — 5.20 · 星座文化动画</p>
|
| 19 |
+
<h1><span class="sig">♉</span> 金牛座 <small>慢慢来,也能抵达美好</small></h1>
|
| 20 |
+
<p class="lead">一万七千年前的壁画、公牛之眼毕宿五、七姐妹昴星团、北宋天文官笔下的「天关客星」、欧罗巴与白牛——<br>以及最后,一张只属于你的 <b>鎏金珍藏卡</b>。</p>
|
| 21 |
+
</header>
|
| 22 |
+
|
| 23 |
+
<main>
|
| 24 |
+
<section class="player" id="player">
|
| 25 |
+
<div class="stage-wrap" id="wrap">
|
| 26 |
+
<canvas id="stage"></canvas>
|
| 27 |
+
<button class="big-play" id="bigPlay" aria-label="播放"><span>▶</span><em>点击播放 · 带旁白与配乐</em></button>
|
| 28 |
+
<div class="loading" id="loading">正在载入音轨…</div>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="controls">
|
| 31 |
+
<button id="play" class="ic" aria-label="播放/暂停">▶</button>
|
| 32 |
+
<span class="time" id="time">0:00 / 4:10</span>
|
| 33 |
+
<div class="bar" id="bar"><div class="fill" id="fill"></div><div class="marks" id="marks"></div></div>
|
| 34 |
+
<div class="seg" role="group" aria-label="画幅">
|
| 35 |
+
<button data-o="landscape" class="on">横屏</button><button data-o="portrait">竖屏</button>
|
| 36 |
+
</div>
|
| 37 |
+
<button id="cc" class="ic on" title="字幕">字</button>
|
| 38 |
+
<button id="fs" class="ic" title="全屏">⛶</button>
|
| 39 |
+
</div>
|
| 40 |
+
<ol class="chapters" id="chapters"></ol>
|
| 41 |
+
</section>
|
| 42 |
+
|
| 43 |
+
<section class="panel card-panel" id="card">
|
| 44 |
+
<div class="panel-head"><h2>金牛座 · 鎏金珍藏卡</h2><p>写上名字与生日,影片第九幕与下载的卡片都会刻上它。</p></div>
|
| 45 |
+
<div class="card-grid">
|
| 46 |
+
<canvas id="cardView" width="900" height="1200"></canvas>
|
| 47 |
+
<form class="card-form" onsubmit="return false">
|
| 48 |
+
<label>珍藏人<input id="name" maxlength="10" placeholder="你的名字或昵称"></label>
|
| 49 |
+
<label>生日<span class="row"><select id="bm"><option value="">月</option><option>4</option><option>5</option></select><select id="bd"><option value="">日</option></select></span></label>
|
| 50 |
+
<p class="hint" id="bhint">金牛座:4 月 20 日 — 5 月 20 日</p>
|
| 51 |
+
<div class="btns">
|
| 52 |
+
<button data-r="9:16">下载壁纸 9:16</button><button data-r="1:1">下载头像 1:1</button><button data-r="3:4">下载卡片 3:4</button>
|
| 53 |
+
</div>
|
| 54 |
+
<button class="ghost" id="flipBtn">翻转看看背面</button>
|
| 55 |
+
</form>
|
| 56 |
+
</div>
|
| 57 |
+
</section>
|
| 58 |
+
|
| 59 |
+
<section class="panel" id="export">
|
| 60 |
+
<div class="panel-head"><h2>本地导出 MP4</h2><p>全部在你的浏览器里逐帧渲染,不上传任何数据。电脑端 Chrome / Edge 体验最佳。</p></div>
|
| 61 |
+
<div class="ex-grid">
|
| 62 |
+
<label>帧率<span class="seg" id="exFps"><button data-v="24">24</button><button data-v="30" class="on">30</button><button data-v="60">60</button></span></label>
|
| 63 |
+
<label>画幅<span class="seg" id="exO"><button data-v="landscape" class="on">横屏 16:9</button><button data-v="portrait">竖屏 9:16</button></span></label>
|
| 64 |
+
<label>分辨率<span class="seg" id="exR"><button data-v="1080" class="on">1080p</button><button data-v="720">720p</button></span></label>
|
| 65 |
+
<label>范围<span class="seg" id="exRange"><button data-v="all" class="on">完整影片</button><button data-v="act">当前幕</button></span></label>
|
| 66 |
+
</div>
|
| 67 |
+
<div class="ex-run"><button class="primary" id="exGo">开始导出</button><button class="ghost" id="exStop" hidden>取消</button>
|
| 68 |
+
<div class="prog"><div id="exFill"></div></div><span id="exMsg" class="msg">导出时会带上你在珍藏卡里填写的名字。</span></div>
|
| 69 |
+
<div id="exOut" class="ex-out"></div>
|
| 70 |
+
</section>
|
| 71 |
+
|
| 72 |
+
<section class="panel" id="publish">
|
| 73 |
+
<div class="panel-head"><h2>四平台发布包</h2><p>标题、简介、话题均已按平台限制校对;每个平台两张封面。</p></div>
|
| 74 |
+
<div class="tabs" id="tabs"></div>
|
| 75 |
+
<div id="pub"></div>
|
| 76 |
+
</section>
|
| 77 |
+
</main>
|
| 78 |
+
|
| 79 |
+
<footer>星座是一种有趣、浪漫的文化 · 愿你按照自己的节奏,稳稳地发光 · <a href="README.md">制作说明</a></footer>
|
| 80 |
+
<script type="module" src="js/main.js"></script>
|
| 81 |
+
</body>
|
| 82 |
+
</html>
|
taurus/js/encoder.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ♉ 金牛座 —— 本地导出 MP4(WebCodecs + mp4-muxer,逐帧离线渲染;不支持时回退 MediaRecorder 实时录制)
|
| 2 |
+
import { Muxer, ArrayBufferTarget } from '../vendor/mp4-muxer.mjs';
|
| 3 |
+
|
| 4 |
+
export const SIZES = { 1080: [1920, 1080], 720: [1280, 720] };
|
| 5 |
+
export const hasWebCodecs = () => typeof VideoEncoder === 'function' && typeof AudioEncoder === 'function' && typeof VideoFrame === 'function';
|
| 6 |
+
|
| 7 |
+
async function pickVideo(W, H, fps) {
|
| 8 |
+
const px = W * H, bitrate = Math.round((px >= 1920 * 1080 ? 10e6 : 5e6) * (fps > 30 ? 1.5 : fps < 25 ? .85 : 1));
|
| 9 |
+
for (const codec of ['avc1.640033', 'avc1.64002A', 'avc1.640028', 'avc1.4D0033', 'avc1.42E033', 'avc1.42001F'])
|
| 10 |
+
for (const hw of ['prefer-hardware', 'no-preference']) {
|
| 11 |
+
const c = { codec, width: W, height: H, bitrate, framerate: fps, hardwareAcceleration: hw, avc: { format: 'avc' } };
|
| 12 |
+
try { if ((await VideoEncoder.isConfigSupported(c)).supported) return c; } catch (e) { /* 尝试下一种 */ }
|
| 13 |
+
}
|
| 14 |
+
throw new Error('当前浏览器无法编码 H.264 视频');
|
| 15 |
+
}
|
| 16 |
+
async function pickAudio() {
|
| 17 |
+
for (const [codec, mux] of [['mp4a.40.2', 'aac'], ['opus', 'opus']]) {
|
| 18 |
+
const c = { codec, sampleRate: 48000, numberOfChannels: 2, bitrate: 192000 };
|
| 19 |
+
try { if ((await AudioEncoder.isConfigSupported(c)).supported) return [c, mux]; } catch (e) { /* 下一种 */ }
|
| 20 |
+
}
|
| 21 |
+
return [null, null];
|
| 22 |
+
}
|
| 23 |
+
// 把音轨切片并重采样到 48k 立体声
|
| 24 |
+
async function sliceAudio(buf, from, to) {
|
| 25 |
+
const len = Math.ceil((to - from) * 48000), ctx = new OfflineAudioContext(2, len, 48000), src = ctx.createBufferSource();
|
| 26 |
+
src.buffer = buf; src.connect(ctx.destination); src.start(0, from, to - from); return ctx.startRendering();
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/**
|
| 30 |
+
* @param o { fps, orient:'landscape'|'portrait', res:1080|720, from, to, audio:AudioBuffer, draw(g,t,W,H), onProgress(k,msg), signal }
|
| 31 |
+
*/
|
| 32 |
+
export async function exportMP4(o) {
|
| 33 |
+
let [W, H] = SIZES[o.res] || SIZES[1080]; if (o.orient === 'portrait') [W, H] = [H, W];
|
| 34 |
+
const fps = o.fps, from = o.from ?? 0, to = o.to, N = Math.round((to - from) * fps);
|
| 35 |
+
const vc = await pickVideo(W, H, fps), [ac, amux] = o.audio ? await pickAudio() : [null, null];
|
| 36 |
+
const muxer = new Muxer({ target: new ArrayBufferTarget(), fastStart: 'in-memory', firstTimestampBehavior: 'offset',
|
| 37 |
+
video: { codec: 'avc', width: W, height: H, frameRate: fps }, ...(ac ? { audio: { codec: amux, sampleRate: 48000, numberOfChannels: 2 } } : {}) });
|
| 38 |
+
let err = null;
|
| 39 |
+
const venc = new VideoEncoder({ output: (c, m) => muxer.addVideoChunk(c, m), error: e => err = e }); venc.configure(vc);
|
| 40 |
+
// 音频
|
| 41 |
+
if (ac) {
|
| 42 |
+
o.onProgress?.(0, '准备音轨…');
|
| 43 |
+
const ab = await sliceAudio(o.audio, from, to), aenc = new AudioEncoder({ output: (c, m) => muxer.addAudioChunk(c, m), error: e => err = e }); aenc.configure(ac);
|
| 44 |
+
const L = ab.getChannelData(0), R = ab.getChannelData(1), step = 4800;
|
| 45 |
+
for (let i = 0; i < ab.length; i += step) { const n = Math.min(step, ab.length - i), d = new Float32Array(n * 2); d.set(L.subarray(i, i + n), 0); d.set(R.subarray(i, i + n), n);
|
| 46 |
+
const a = new AudioData({ format: 'f32-planar', sampleRate: 48000, numberOfFrames: n, numberOfChannels: 2, timestamp: Math.round(i / 48000 * 1e6), data: d }); aenc.encode(a); a.close(); }
|
| 47 |
+
await aenc.flush(); aenc.close();
|
| 48 |
+
}
|
| 49 |
+
// 视频:逐帧离线渲染
|
| 50 |
+
const cv = typeof OffscreenCanvas === 'function' ? new OffscreenCanvas(W, H) : Object.assign(document.createElement('canvas'), { width: W, height: H });
|
| 51 |
+
const g = cv.getContext('2d', { alpha: false }); const t0 = performance.now();
|
| 52 |
+
for (let i = 0; i < N; i++) {
|
| 53 |
+
if (o.signal?.aborted) { venc.close(); throw new DOMException('已取消', 'AbortError'); }
|
| 54 |
+
if (err) throw err;
|
| 55 |
+
g.setTransform(1, 0, 0, 1, 0, 0); g.fillStyle = '#000'; g.fillRect(0, 0, W, H); o.draw(g, from + i / fps, W, H);
|
| 56 |
+
const f = new VideoFrame(cv, { timestamp: Math.round(i * 1e6 / fps), duration: Math.round(1e6 / fps) });
|
| 57 |
+
venc.encode(f, { keyFrame: i % (fps * 2) === 0 }); f.close();
|
| 58 |
+
while (venc.encodeQueueSize > 6) await new Promise(r => setTimeout(r, 2));
|
| 59 |
+
if (i % 6 === 0) { const k = (i + 1) / N, el = (performance.now() - t0) / 1000; o.onProgress?.(k, `渲染第 ${i + 1}/${N} 帧 · 剩余约 ${Math.max(0, Math.round(el / k - el))} 秒`); await new Promise(r => setTimeout(r, 0)); }
|
| 60 |
+
}
|
| 61 |
+
await venc.flush(); venc.close(); if (err) throw err;
|
| 62 |
+
muxer.finalize(); o.onProgress?.(1, '完成');
|
| 63 |
+
return { blob: new Blob([muxer.target.buffer], { type: 'video/mp4' }), W, H, fps, audio: amux || 'none', codec: vc.codec };
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/** 回退:实时录屏(时长 = 片长),格式取浏览器支持的 mp4 / webm */
|
| 67 |
+
export async function recordRealtime(o) {
|
| 68 |
+
let [W, H] = SIZES[o.res] || SIZES[1080]; if (o.orient === 'portrait') [W, H] = [H, W];
|
| 69 |
+
const cv = Object.assign(document.createElement('canvas'), { width: W, height: H }), g = cv.getContext('2d');
|
| 70 |
+
const stream = cv.captureStream(o.fps), ctx = new AudioContext(), dst = ctx.createMediaStreamDestination(), src = ctx.createBufferSource();
|
| 71 |
+
src.buffer = o.audio; src.connect(dst); dst.stream.getAudioTracks().forEach(t => stream.addTrack(t));
|
| 72 |
+
const type = ['video/mp4;codecs=avc1,mp4a.40.2', 'video/mp4', 'video/webm;codecs=vp9,opus', 'video/webm'].find(t => MediaRecorder.isTypeSupported(t));
|
| 73 |
+
const rec = new MediaRecorder(stream, { mimeType: type, videoBitsPerSecond: 10e6 }), parts = []; rec.ondataavailable = e => parts.push(e.data);
|
| 74 |
+
const from = o.from ?? 0, dur = o.to - from; await ctx.resume(); const start = ctx.currentTime + .1; src.start(start, from); rec.start(500);
|
| 75 |
+
await new Promise(res => { const tick = () => { const t = ctx.currentTime - start; if (t >= dur || o.signal?.aborted) return res();
|
| 76 |
+
g.fillStyle = '#000'; g.fillRect(0, 0, W, H); o.draw(g, from + Math.max(0, t), W, H); o.onProgress?.(t / dur, `实时录制中 ${Math.round(t)}/${Math.round(dur)} 秒`); requestAnimationFrame(tick); }; tick(); });
|
| 77 |
+
rec.stop(); src.stop(); await new Promise(r => rec.onstop = r); ctx.close();
|
| 78 |
+
return { blob: new Blob(parts, { type }), W, H, fps: o.fps, audio: 'realtime', ext: type.includes('mp4') ? 'mp4' : 'webm' };
|
| 79 |
+
}
|
taurus/js/main.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ♉ 金牛座 —— 页面:播放器 / 珍藏卡 / 导出 / 发布包(全新编写)
|
| 2 |
+
import { FILM } from './script.js';
|
| 3 |
+
import { createFilm, actAt } from './film.js';
|
| 4 |
+
import { drawCard, drawCardPoster } from './card.js';
|
| 5 |
+
import { exportMP4, recordRealtime, hasWebCodecs } from './encoder.js';
|
| 6 |
+
import { PLATFORMS, COVER_PX, drawCover, paintBg, charCount } from './release.js';
|
| 7 |
+
|
| 8 |
+
const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
|
| 9 |
+
const film = createFilm(), D = FILM.duration;
|
| 10 |
+
const fmt = t => `${Math.floor(t / 60)}:${String(Math.floor(t % 60)).padStart(2, '0')}`;
|
| 11 |
+
const store = { get name() { return localStorage.getItem('taurus.name') || ''; }, get bday() { return localStorage.getItem('taurus.bday') || ''; } };
|
| 12 |
+
const opts = () => ({ name: $('#name').value.trim(), birthday: bdayStr() });
|
| 13 |
+
|
| 14 |
+
/* ================= 播放器 ================= */
|
| 15 |
+
const cv = $('#stage'), g = cv.getContext('2d'), wrap = $('#wrap');
|
| 16 |
+
let orient = matchMedia('(max-width: 820px) and (orientation: portrait)').matches ? 'portrait' : 'landscape';
|
| 17 |
+
let audioCtx = null, track = null, src = null, playing = false, offset = 0, startedAt = 0, subs = true;
|
| 18 |
+
const now = () => playing ? Math.min(D, offset + audioCtx.currentTime - startedAt) : offset;
|
| 19 |
+
|
| 20 |
+
function fit() {
|
| 21 |
+
wrap.classList.toggle('portrait', orient === 'portrait');
|
| 22 |
+
$$('.controls .seg button').forEach(b => b.classList.toggle('on', b.dataset.o === orient));
|
| 23 |
+
const r = wrap.getBoundingClientRect(), dpr = Math.min(2, devicePixelRatio || 1);
|
| 24 |
+
let w = Math.round(r.width * dpr), h = Math.round(r.height * dpr); const k = Math.min(1, 1920 / Math.max(w, h)); w = Math.round(w * k); h = Math.round(h * k);
|
| 25 |
+
if (cv.width !== w || cv.height !== h) { cv.width = w; cv.height = h; }
|
| 26 |
+
}
|
| 27 |
+
async function loadAudio() {
|
| 28 |
+
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
| 29 |
+
const buf = await (await fetch('audio/film.mp3')).arrayBuffer();
|
| 30 |
+
track = await new Promise((ok, no) => { const p = audioCtx.decodeAudioData(buf, ok, no); if (p && p.then) p.then(ok, no); });
|
| 31 |
+
$('#loading').hidden = true; return track;
|
| 32 |
+
}
|
| 33 |
+
const ready = loadAudio().catch(e => { $('#loading').textContent = '音轨载入失败:' + e.message; });
|
| 34 |
+
async function play() {
|
| 35 |
+
await ready; if (!track) return; await audioCtx.resume();
|
| 36 |
+
if (offset >= D - .05) offset = 0;
|
| 37 |
+
src = audioCtx.createBufferSource(); src.buffer = track; src.connect(audioCtx.destination); startedAt = audioCtx.currentTime + .03; src.start(startedAt, offset);
|
| 38 |
+
playing = true; $('#bigPlay').hidden = true; $('#play').textContent = '❚❚';
|
| 39 |
+
}
|
| 40 |
+
function pause() { if (!playing) return; offset = now(); playing = false; try { src.stop(); } catch (e) {} src = null; $('#play').textContent = '▶'; }
|
| 41 |
+
function seek(t) { const was = playing; pause(); offset = Math.max(0, Math.min(D, t)); if (was) play(); }
|
| 42 |
+
$('#play').onclick = () => playing ? pause() : play();
|
| 43 |
+
$('#bigPlay').onclick = play;
|
| 44 |
+
cv.onclick = () => playing ? pause() : play();
|
| 45 |
+
$('#cc').onclick = e => { subs = !subs; e.currentTarget.classList.toggle('on', subs); };
|
| 46 |
+
$$('.controls .seg button').forEach(b => b.onclick = () => { orient = b.dataset.o; fit(); });
|
| 47 |
+
$('#fs').onclick = () => { const el = wrap; (document.fullscreenElement ? document.exitFullscreen() : (el.requestFullscreen || el.webkitRequestFullscreen || (() => {})).call(el)); };
|
| 48 |
+
document.addEventListener('keydown', e => { if (e.target.tagName === 'INPUT') return; if (e.code === 'Space') { e.preventDefault(); playing ? pause() : play(); } if (e.code === 'ArrowRight') seek(now() + 5); if (e.code === 'ArrowLeft') seek(now() - 5); });
|
| 49 |
+
// 进度条与章节
|
| 50 |
+
const bar = $('#bar');
|
| 51 |
+
FILM.acts.forEach((A, i) => {
|
| 52 |
+
if (i) { const m = document.createElement('i'); m.style.left = (A.start / D * 100) + '%'; $('#marks').append(m); }
|
| 53 |
+
const li = document.createElement('li'); li.innerHTML = `<b>${String(i + 1).padStart(2, '0')} · ${fmt(A.start)}</b>${A.title}`; li.onclick = () => { seek(A.start + .01); if (!playing) play(); }; $('#chapters').append(li);
|
| 54 |
+
});
|
| 55 |
+
const scrub = e => { const r = bar.getBoundingClientRect(); seek((e.clientX - r.left) / r.width * D); };
|
| 56 |
+
bar.addEventListener('pointerdown', e => { bar.setPointerCapture(e.pointerId); scrub(e); bar.onpointermove = scrub; });
|
| 57 |
+
bar.addEventListener('pointerup', () => bar.onpointermove = null);
|
| 58 |
+
let lastAct = -1, lastKey = '';
|
| 59 |
+
function frame() {
|
| 60 |
+
fit(); const t = now();
|
| 61 |
+
if (playing && t >= D) { pause(); offset = D; }
|
| 62 |
+
const o = opts(), key = [t.toFixed(3), cv.width, cv.height, o.name, o.birthday, subs].join('|');
|
| 63 |
+
if (key !== lastKey) { lastKey = key; film.draw(g, t, cv.width, cv.height, { ...o, subtitles: subs }); }
|
| 64 |
+
$('#fill').style.width = (t / D * 100) + '%'; $('#time').textContent = `${fmt(t)} / ${fmt(D)}`;
|
| 65 |
+
const a = actAt(t); if (a !== lastAct) { lastAct = a; $$('#chapters li').forEach((li, i) => li.classList.toggle('on', i === a)); }
|
| 66 |
+
requestAnimationFrame(frame);
|
| 67 |
+
}
|
| 68 |
+
addEventListener('resize', fit);
|
| 69 |
+
|
| 70 |
+
/* ================= 珍藏卡 ================= */
|
| 71 |
+
const bm = $('#bm'), bd = $('#bd');
|
| 72 |
+
function fillDays() { const m = +bm.value, v = bd.value; bd.innerHTML = '<option value="">日</option>';
|
| 73 |
+
const [a, b] = m === 4 ? [20, 30] : m === 5 ? [1, 20] : [0, -1]; for (let d = a; d <= b; d++) bd.add(new Option(d, d)); if ([...bd.options].some(o => o.value === v)) bd.value = v; }
|
| 74 |
+
function bdayStr() { return bm.value && bd.value ? `${bm.value}月${bd.value}日` : ''; }
|
| 75 |
+
bm.onchange = () => { fillDays(); save(); }; bd.onchange = save; $('#name').oninput = save;
|
| 76 |
+
function save() { localStorage.setItem('taurus.name', $('#name').value.trim()); localStorage.setItem('taurus.bday', bm.value && bd.value ? `${bm.value}-${bd.value}` : '');
|
| 77 |
+
$('#bhint').textContent = bdayStr() ? `已刻上生日:${bdayStr()} · 欢迎回家,金牛座` : '金牛座:4 月 20 日 — 5 月 20 日'; }
|
| 78 |
+
$('#name').value = store.name; if (store.bday) { const [m, d] = store.bday.split('-'); bm.value = m; fillDays(); bd.value = d; } else fillDays(); save();
|
| 79 |
+
const cardCv = $('#cardView'), cg = cardCv.getContext('2d'); let showBack = false, flipK = 1, cardT0 = performance.now();
|
| 80 |
+
$('#flipBtn').onclick = () => { showBack = !showBack; $('#flipBtn').textContent = showBack ? '翻回正面' : '翻转看看背面'; };
|
| 81 |
+
let cardVisible = false, cardDrawn = false; new IntersectionObserver(es => es.forEach(e => cardVisible = e.isIntersecting)).observe(cardCv);
|
| 82 |
+
function cardLoop() {
|
| 83 |
+
if ((!cardVisible || document.hidden) && cardDrawn) return requestAnimationFrame(cardLoop);
|
| 84 |
+
cardDrawn = true;
|
| 85 |
+
const t = (performance.now() - cardT0) / 1000, W = cardCv.width, H = cardCv.height;
|
| 86 |
+
flipK += ((showBack ? 0 : 1) - flipK) * .12;
|
| 87 |
+
paintBg(cg, W, H, 1);
|
| 88 |
+
drawCard(cg, W / 2, H / 2, H * .86, { t, flip: flipK, fields: 1, shine: ((t * .22) % 1.4) / 1.4, holo: .8, stamp: 1, holder: opts().name, birthday: opts().birthday, tilt: Math.sin(t * .7) * .015 });
|
| 89 |
+
requestAnimationFrame(cardLoop);
|
| 90 |
+
}
|
| 91 |
+
const POSTER = { '9:16': [1080, 1920], '1:1': [1440, 1440], '3:4': [1200, 1600] };
|
| 92 |
+
$$('.btns button').forEach(b => b.onclick = () => {
|
| 93 |
+
const [W, H] = POSTER[b.dataset.r], c = document.createElement('canvas'); c.width = W; c.height = H;
|
| 94 |
+
drawCardPoster(c.getContext('2d'), W, H, opts(), (g2, w, h) => paintBg(g2, w, h, 1));
|
| 95 |
+
c.toBlob(bl => download(bl, `金牛座鎏金珍藏卡-${opts().name || 'Taurus'}-${b.dataset.r.replace(':', 'x')}.png`), 'image/png');
|
| 96 |
+
});
|
| 97 |
+
function download(blob, name) { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; document.body.append(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 60000); }
|
| 98 |
+
|
| 99 |
+
/* ================= 导出 ================= */
|
| 100 |
+
const pick = id => $(`${id} button.on`).dataset.v;
|
| 101 |
+
['#exFps', '#exO', '#exR', '#exRange'].forEach(id => $$(`${id} button`).forEach(b => b.onclick = () => { $$(`${id} button`).forEach(x => x.classList.toggle('on', x === b)); }));
|
| 102 |
+
let ctl = null;
|
| 103 |
+
async function runExport(cfg = {}) {
|
| 104 |
+
await ready; if (!track) throw new Error('音轨未载入');
|
| 105 |
+
const fps = +(cfg.fps ?? pick('#exFps')), o = cfg.orient ?? pick('#exO'), res = +(cfg.res ?? pick('#exR'));
|
| 106 |
+
let from = 0, to = D; if (cfg.from !== undefined) { from = cfg.from; to = cfg.to; } else if (pick('#exRange') === 'act') { const A = FILM.acts[actAt(now())]; from = A.start; to = A.end; }
|
| 107 |
+
ctl = new AbortController(); $('#exGo').disabled = true; $('#exStop').hidden = false; $('#exOut').innerHTML = '';
|
| 108 |
+
const o2 = opts(), job = { fps, orient: o, res, from, to, audio: track, signal: ctl.signal, draw: (g2, t, W, H) => film.draw(g2, t, W, H, o2),
|
| 109 |
+
onProgress: (k, m) => { $('#exFill').style.width = (k * 100).toFixed(1) + '%'; $('#exMsg').textContent = m; } };
|
| 110 |
+
try {
|
| 111 |
+
const r = hasWebCodecs() ? await exportMP4(job) : await recordRealtime(job), ext = r.ext || 'mp4';
|
| 112 |
+
const name = `金牛座-慢慢来-${o === 'portrait' ? '竖屏' : '横屏'}-${res}p${fps}.${ext}`, url = URL.createObjectURL(r.blob);
|
| 113 |
+
$('#exOut').innerHTML = `<a href="${url}" download="${name}">⬇ 下载 ${name}(${(r.blob.size / 1048576).toFixed(1)} MB)</a><video src="${url}" controls playsinline></video>`;
|
| 114 |
+
$('#exMsg').textContent = `完成:${r.W}×${r.H} · ${r.fps} fps · 音频 ${r.audio}`; window.__lastExport = r; return r;
|
| 115 |
+
} catch (e) { $('#exMsg').textContent = e.name === 'AbortError' ? '已取消' : '导出失败:' + e.message; throw e; }
|
| 116 |
+
finally { $('#exGo').disabled = false; $('#exStop').hidden = true; }
|
| 117 |
+
}
|
| 118 |
+
$('#exGo').onclick = () => runExport().catch(() => {});
|
| 119 |
+
$('#exStop').onclick = () => ctl?.abort();
|
| 120 |
+
if (!hasWebCodecs()) $('#exMsg').textContent = '当前浏览器不支持逐帧编码,将以实时录制方式导出(耗时等于片长)。推荐使用电脑版 Chrome / Edge。';
|
| 121 |
+
|
| 122 |
+
/* ================= 发布包 ================= */
|
| 123 |
+
const esc = s => s.replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
| 124 |
+
function showPlatform(id) {
|
| 125 |
+
const p = PLATFORMS.find(x => x.id === id); $$('#tabs button').forEach(b => b.classList.toggle('on', b.dataset.id === id));
|
| 126 |
+
const tags = p.tags.map(t => '#' + t).join(' '), n = charCount(p.title), lim = p.tagMax > 50 ? '不限' : p.tagMax;
|
| 127 |
+
$('#pub').innerHTML = `<div class="pub"><div>
|
| 128 |
+
<div class="field"><h4>标题 <small class="${n <= p.titleMax ? 'ok' : ''}">${n} / ${p.titleMax} 字<button class="copy" data-c="title">复制</button></small></h4><pre>${esc(p.title)}</pre></div>
|
| 129 |
+
<div class="field"><h4>简介 <small><button class="copy" data-c="desc">复制</button></small></h4><pre>${esc(p.desc)}</pre></div>
|
| 130 |
+
<div class="field"><h4>话题 <small class="${p.tags.length <= p.tagMax ? 'ok' : ''}">${p.tags.length} / ${lim}<button class="copy" data-c="tags">复制</button></small></h4><pre>${esc(tags)}</pre></div>
|
| 131 |
+
</div><div class="covers">${p.covers.map(r => { const f = `covers/cover-${p.id}-${r.replace(':', 'x')}.jpg`, [w, h] = COVER_PX[r];
|
| 132 |
+
return `<figure><img src="${f}" alt="${p.name}封面 ${r}" loading="lazy"><figcaption><span>${r} · ${w}×${h}</span><a href="${f}" download>下载</a></figcaption></figure>`; }).join('')}</div></div>`;
|
| 133 |
+
$$('#pub .copy').forEach(b => b.onclick = async () => { const v = { title: p.title, desc: p.desc, tags }[b.dataset.c]; try { await navigator.clipboard.writeText(v); b.textContent = '已复制'; } catch (e) { b.textContent = '请手动复制'; } setTimeout(() => b.textContent = '复制', 1500); });
|
| 134 |
+
}
|
| 135 |
+
PLATFORMS.forEach(p => { const b = document.createElement('button'); b.textContent = p.name; b.dataset.id = p.id; b.onclick = () => showPlatform(p.id); $('#tabs').append(b); });
|
| 136 |
+
showPlatform('xhs');
|
| 137 |
+
|
| 138 |
+
/* ================= 启动 ================= */
|
| 139 |
+
(async () => { try { await Promise.race([Promise.all(['900 40px "Noto Serif SC"', '700 40px "Noto Serif SC"', '600 40px "Noto Sans SC"', '700 40px "Cormorant Garamond"'].map(f => document.fonts.load(f, '金牛座慢慢来TAURUS0123'))), new Promise(r => setTimeout(r, 2500))]); } catch (e) {}
|
| 140 |
+
fit(); film.backdrop.warm(); frame(); cardLoop(); })();
|
| 141 |
+
|
| 142 |
+
window.TAURUS = { FILM, film, runExport, drawCover, PLATFORMS, COVER_PX, charCount, ready, seek, play, pause, now: () => now() };
|
taurus/js/music.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ♉ 金牛座 —— 配乐与音效合成(全新编写,纯 JS PCM,浏览器 / Node 通用,确定性输出)
|
| 2 |
+
// F 大调田园风:F – Dm – B♭ – C;电钢琴分解和弦 · 大提琴 · 八音盒 · 圆号 · 定音鼓 · 古筝拨弦(天关客星一幕)
|
| 3 |
+
export const RATE = 44100;
|
| 4 |
+
const TAU = Math.PI * 2;
|
| 5 |
+
const BAR = 3.2; // 75 BPM,4/4
|
| 6 |
+
const hz = m => 440 * Math.pow(2, (m - 69) / 12);
|
| 7 |
+
let seed = 7; const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
|
| 8 |
+
|
| 9 |
+
// 和弦(MIDI 根音 + 和弦音)
|
| 10 |
+
const CH = { F: [53, [65, 69, 72]], Dm: [50, [62, 65, 69]], Bb: [46, [62, 65, 70]], C: [48, [64, 67, 72]], Gm: [43, [62, 67, 70]], Am: [45, [64, 69, 72]], Csus: [48, [65, 67, 72]] };
|
| 11 |
+
const PROG = {
|
| 12 |
+
bloom: ['F', 'Dm', 'Bb', 'C'], ancient: ['Dm', 'Bb', 'F', 'C'], locate: ['F', 'Am', 'Bb', 'C'], stars: ['Bb', 'F', 'Gm', 'Csus'],
|
| 13 |
+
guest: ['Dm', 'C', 'Bb', 'C'], europa: ['F', 'Dm', 'Gm', 'C'], sigil: ['Bb', 'C', 'F', 'F'], venus: ['F', 'Am', 'Bb', 'C'],
|
| 14 |
+
card: ['Bb', 'C', 'Am', 'Dm', 'Gm', 'C', 'F', 'F'], coda: ['F', 'Bb', 'F', 'C', 'F', 'Bb', 'F', 'F'],
|
| 15 |
+
};
|
| 16 |
+
// 各幕编制强度:[钢琴, 弦乐垫, 大提琴, 八音盒, 圆号, 古筝]
|
| 17 |
+
const MIX = {
|
| 18 |
+
bloom: [.9, .5, .3, .6, 0, 0], ancient: [.5, .6, .8, 0, 0, 0], locate: [.6, .6, .2, .8, 0, 0], stars: [.7, .7, .3, .7, 0, 0], guest: [.3, .6, .7, 0, 0, .9],
|
| 19 |
+
europa: [.8, .7, .5, .3, .7, 0], sigil: [.5, .6, .3, .9, 0, 0], venus: [.9, .6, .6, .4, .2, 0], card: [.8, .9, .7, .5, 1, 0], coda: [.8, .7, .6, .6, .5, 0],
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
/* ---------- 乐器:每个返回往 L/R 累加 ---------- */
|
| 23 |
+
function put(L, R, t0, n, pan, fn) {
|
| 24 |
+
const i0 = Math.floor(t0 * RATE); if (i0 >= L.length) return;
|
| 25 |
+
const gl = Math.cos((pan + 1) * Math.PI / 4), gr = Math.sin((pan + 1) * Math.PI / 4);
|
| 26 |
+
for (let i = Math.max(0, -i0); i < n && i0 + i < L.length; i++) { const v = fn(i / RATE); L[i0 + i] += v * gl; R[i0 + i] += v * gr; }
|
| 27 |
+
}
|
| 28 |
+
const epiano = (L, R, t, m, v, d, pan = 0) => { const f = hz(m); put(L, R, t, (d + 1.5) * RATE, pan, s => {
|
| 29 |
+
const e = Math.min(1, s * 300) * Math.exp(-s * 1.6) * (s < d ? 1 : Math.exp(-(s - d) * 6));
|
| 30 |
+
return v * e * (Math.sin(TAU * f * s + 1.3 * Math.exp(-s * 5) * Math.sin(TAU * f * 2 * s)) + .18 * Math.sin(TAU * f * 3.01 * s) * Math.exp(-s * 4)); }); };
|
| 31 |
+
const pad = (L, R, t, m, v, d, pan = 0) => { const f = hz(m); put(L, R, t, (d + 2) * RATE, pan, s => {
|
| 32 |
+
const e = Math.min(1, s / 1.2) * (s < d ? 1 : Math.max(0, 1 - (s - d) / 2));
|
| 33 |
+
let x = 0; for (const dt of [-.12, 0, .13]) { const ff = f * Math.pow(2, dt / 12); x += Math.sin(TAU * ff * s) + .35 * Math.sin(TAU * 2 * ff * s) + .12 * Math.sin(TAU * 3 * ff * s); }
|
| 34 |
+
return v * e * x * .3; }); };
|
| 35 |
+
const cello = (L, R, t, m, v, d, pan = -.3) => { const f = hz(m); put(L, R, t, (d + 1) * RATE, pan, s => {
|
| 36 |
+
const e = Math.min(1, s / .35) * (s < d ? 1 : Math.max(0, 1 - (s - d))), vib = 1 + .004 * Math.sin(TAU * 5.2 * s) * Math.min(1, s);
|
| 37 |
+
let x = 0; const a = [1, .6, .45, .3, .2, .12, .08]; for (let k = 0; k < a.length; k++) x += a[k] * Math.sin(TAU * f * (k + 1) * vib * s + k);
|
| 38 |
+
return v * e * x * .32; }); };
|
| 39 |
+
const box = (L, R, t, m, v, pan = .3) => { const f = hz(m); put(L, R, t, 2.6 * RATE, pan, s => v * Math.exp(-s * 2.4) * (Math.sin(TAU * f * s) + .3 * Math.sin(TAU * f * 4.02 * s) * Math.exp(-s * 6) + .1 * Math.sin(TAU * f * 6.3 * s) * Math.exp(-s * 9))); };
|
| 40 |
+
const horn = (L, R, t, m, v, d, pan = -.15) => { const f = hz(m); put(L, R, t, (d + .8) * RATE, pan, s => {
|
| 41 |
+
const e = Math.min(1, s / .18) * (s < d ? 1 : Math.max(0, 1 - (s - d) / .8)), br = .5 + .5 * Math.min(1, s / .5), vib = 1 + .003 * Math.sin(TAU * 4.8 * s) * Math.min(1, s / .8);
|
| 42 |
+
let x = 0; for (let k = 1; k <= 8; k++) x += Math.pow(br * .55, k - 1) * Math.sin(TAU * f * k * vib * s);
|
| 43 |
+
return v * e * x * .35; }); };
|
| 44 |
+
function zheng(L, R, t, m, v, pan = .2) { // Karplus-Strong 拨弦
|
| 45 |
+
const f = hz(m), N = Math.round(RATE / f), buf = new Float32Array(N); for (let i = 0; i < N; i++) buf[i] = rnd() * 2 - 1;
|
| 46 |
+
let p = 0, last = 0; const out = new Float32Array(Math.floor(3 * RATE));
|
| 47 |
+
for (let i = 0; i < out.length; i++) { const cur = buf[p]; const nx = .996 * .5 * (cur + last); last = cur; buf[p] = nx; p = (p + 1) % N; out[i] = cur * Math.exp(-i / RATE * .8); }
|
| 48 |
+
put(L, R, t, out.length, pan, s => v * .5 * out[Math.min(out.length - 1, Math.floor(s * RATE))]);
|
| 49 |
+
}
|
| 50 |
+
const timp = (L, R, t, v, m = 41) => { const f = hz(m); put(L, R, t, 3 * RATE, 0, s => v * (Math.exp(-s * 1.8) * Math.sin(TAU * f * (1 + .15 * Math.exp(-s * 12)) * s) + .25 * (rnd() * 2 - 1) * Math.exp(-s * 25))); };
|
| 51 |
+
const bell = (L, R, t, m, v, pan = 0) => { const f = hz(m); put(L, R, t, 4 * RATE, pan, s => v * Math.exp(-s * 1.2) * (Math.sin(TAU * f * s) + .5 * Math.sin(TAU * f * 2.76 * s) * Math.exp(-s * 2) + .3 * Math.sin(TAU * f * 5.4 * s) * Math.exp(-s * 4))); };
|
| 52 |
+
function whoosh(L, R, t, v, d = 1.4, up = true) {
|
| 53 |
+
let lp = 0; put(L, R, t, d * RATE, 0, s => { const k = s / d, cut = up ? .02 + .25 * k : .27 - .25 * k; lp += cut * ((rnd() * 2 - 1) - lp); return v * Math.sin(Math.PI * k) ** 2 * lp * 2.4; });
|
| 54 |
+
}
|
| 55 |
+
function shimmer(L, R, t, v, d = 2) { for (let i = 0; i < 14; i++) box(L, R, t + i * d / 14 * (.6 + .4 * rnd()), 84 + [0, 2, 4, 7, 9, 12, 14][i % 7] + (i > 6 ? 12 : 0) - 12, v * (1 - i / 18), (rnd() - .5) * 1.4); }
|
| 56 |
+
const thud = (L, R, t, v) => { put(L, R, t, 1.2 * RATE, 0, s => v * Math.exp(-s * 7) * Math.sin(TAU * 58 * (1 + Math.exp(-s * 20)) * s)); put(L, R, t, .25 * RATE, 0, s => v * .35 * (rnd() * 2 - 1) * Math.exp(-s * 30)); };
|
| 57 |
+
|
| 58 |
+
/* ---------- 混响(Schroeder) ---------- */
|
| 59 |
+
function reverb(x, mix = .22, room = .82) {
|
| 60 |
+
const combs = [1557, 1617, 1491, 1422].map(n => ({ b: new Float32Array(n), p: 0, f: 0 })), aps = [225, 556].map(n => ({ b: new Float32Array(n), p: 0 }));
|
| 61 |
+
const y = new Float32Array(x.length);
|
| 62 |
+
for (let i = 0; i < x.length; i++) {
|
| 63 |
+
let s = 0; for (const c of combs) { const o = c.b[c.p]; c.f = o * .7 + c.f * .3; c.b[c.p] = x[i] * .015 + c.f * room; c.p = (c.p + 1) % c.b.length; s += o; }
|
| 64 |
+
for (const a of aps) { const o = a.b[a.p]; const v = -s + o; a.b[a.p] = s + o * .5; a.p = (a.p + 1) % a.b.length; s = v; }
|
| 65 |
+
y[i] = x[i] + s * mix * 4;
|
| 66 |
+
}
|
| 67 |
+
return y;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/* ---------- 作曲 ---------- */
|
| 71 |
+
export function composeScore(FILM) {
|
| 72 |
+
seed = 7;
|
| 73 |
+
const n = Math.ceil((FILM.duration + 1) * RATE), L = new Float32Array(n), R = new Float32Array(n);
|
| 74 |
+
const HORN = [[69, 1], [72, 1], [77, 1.5], [76, .5], [74, 1], [72, 1], [70, 1], [69, 1], [67, 1], [69, 1], [70, 1], [74, 1], [72, 4]];
|
| 75 |
+
for (const A of FILM.acts) {
|
| 76 |
+
const prog = PROG[A.id] || PROG.bloom, mx = MIX[A.id] || MIX.bloom, at = k => A.subs[Math.min(k, A.subs.length - 1)].t0;
|
| 77 |
+
const t0 = A.start, t1 = A.id === 'coda' ? FILM.duration - 2.5 : A.end;
|
| 78 |
+
let bar = 0;
|
| 79 |
+
for (let t = t0; t < t1 - .2; t += BAR, bar++) {
|
| 80 |
+
const [root, tones] = CH[prog[bar % prog.length]], d = Math.min(BAR, t1 - t), fadeIn = A.id === 'bloom' ? Math.min(1, (t - t0) / 6 + .2) : 1;
|
| 81 |
+
if (mx[1]) { tones.forEach((m, i) => pad(L, R, t, m - 12, .05 * mx[1] * fadeIn, d, (i - 1) * .5)); pad(L, R, t, root, .04 * mx[1] * fadeIn, d, 0); }
|
| 82 |
+
if (mx[2]) cello(L, R, t, root - (root > 50 ? 12 : 0), .09 * mx[2] * fadeIn, d);
|
| 83 |
+
if (mx[0]) { const arp = [root + 12, tones[0], tones[1], tones[2], tones[1] + 12, tones[2], tones[1], tones[0]];
|
| 84 |
+
for (let k = 0; k < 8; k++) if (t + k * BAR / 8 < t1) epiano(L, R, t + k * BAR / 8, arp[k], .07 * mx[0] * fadeIn * (k % 2 ? .75 : 1), BAR / 8 * 1.6, (k % 4 - 1.5) * .25); }
|
| 85 |
+
if (mx[3] && bar % 2 === 0) { const mel = [tones[2] + 12, tones[1] + 12, tones[0] + 12, tones[1] + 12, tones[2] + 12, tones[2] + 14];
|
| 86 |
+
for (let k = 0; k < 6; k++) if (rnd() < .8 && t + k * BAR / 4 < t1) box(L, R, t + k * BAR / 4 + BAR / 8, mel[k], .05 * mx[3], .4); }
|
| 87 |
+
if (mx[5]) { const pent = [65, 67, 69, 72, 74, 77, 79, 81]; for (let k = 0; k < 6; k++) if (rnd() < .6) zheng(L, R, t + k * BAR / 6 + rnd() * .05, pent[Math.floor(rnd() * pent.length)] - 12, .25 * mx[5], (rnd() - .5)); }
|
| 88 |
+
}
|
| 89 |
+
if (mx[4]) { let ht = A.id === 'card' ? at(1) : A.id === 'europa' ? at(8) : t0 + BAR * 2; for (const [m, b] of HORN) { if (ht > t1 - .5) break; horn(L, R, ht, m - (A.id === 'coda' ? 0 : 0), .11 * mx[4], b * BAR / 4 * .95); ht += b * BAR / 4; } }
|
| 90 |
+
/* ---- 音效 ---- */
|
| 91 |
+
if (A.start > 0) whoosh(L, R, A.start - .7, .5, 1.6);
|
| 92 |
+
switch (A.id) {
|
| 93 |
+
case 'bloom': shimmer(L, R, at(9) - .3, .14, 1.6); timp(L, R, at(9), .35); bell(L, R, at(10), 77, .1); break;
|
| 94 |
+
case 'ancient': thud(L, R, at(5) - .2, .5); timp(L, R, at(7) - .3, .25, 38); break;
|
| 95 |
+
case 'locate': bell(L, R, at(4), 81, .09, .3); bell(L, R, at(7) - .1, 76, .06, -.2); shimmer(L, R, at(11) - .2, .09, 1.4); break;
|
| 96 |
+
case 'stars': bell(L, R, at(0), 72, .08); whoosh(L, R, at(10) - .8, .35, 1.4); shimmer(L, R, at(10), .1, 2); break;
|
| 97 |
+
case 'guest': timp(L, R, at(5) - .1, .7, 36); timp(L, R, at(5) + .4, .4, 41); whoosh(L, R, at(5) - 1.2, .5, 1.2); shimmer(L, R, at(5) + .2, .12, 2.5); thud(L, R, at(7) - .05, .45);
|
| 98 |
+
for (let k = 0; k < 10; k++) bell(L, R, at(12) + k * .32, 96, .03 * (1 - k / 12), k % 2 ? .5 : -.5); break;
|
| 99 |
+
case 'europa': shimmer(L, R, at(3) - .2, .1, 1.6); whoosh(L, R, at(7) + .2, .25, 2.4, false); shimmer(L, R, at(13) - .2, .13, 2.4); timp(L, R, at(13), .3); break;
|
| 100 |
+
case 'sigil': shimmer(L, R, at(1) - .2, .07, 1.2); shimmer(L, R, at(2) - .1, .07, 1.2); bell(L, R, at(5), 77, .09); break;
|
| 101 |
+
case 'venus': [at(5), at(5) + .9, at(5) + 2.2, at(6), at(6) + 1.2, at(10) - .4].forEach((x, i) => bell(L, R, x, 79 + [0, 2, 4, 7, 9, 12][i], .04, (i % 3 - 1) * .5)); thud(L, R, at(10), .4); break;
|
| 102 |
+
case 'card': whoosh(L, R, at(0) - 1.2, .45, 1.6, false); timp(L, R, at(1) + .5, .45); shimmer(L, R, at(1) + .3, .15, 2.4); thud(L, R, at(7) + .05, .7); bell(L, R, at(7) + .1, 77, .12); bell(L, R, at(7) + .1, 84, .07); break;
|
| 103 |
+
case 'coda': { const te = FILM.duration - 6.5; pad(L, R, te, 53, .08, 4.5); pad(L, R, te, 65, .06, 4.5); pad(L, R, te, 69, .05, 4.5); cello(L, R, te, 41, .12, 4.5); bell(L, R, te, 77, .1); bell(L, R, te + .4, 81, .06); timp(L, R, te, .3); break; }
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
// 首尾淡入淡出
|
| 107 |
+
for (let i = 0; i < n; i++) { const s = i / RATE, g = Math.min(1, s / 2.5) * Math.min(1, Math.max(0, (FILM.duration - s) / 3)); L[i] *= g; R[i] *= g; }
|
| 108 |
+
return [reverb(L, .2), reverb(R, .2, .84)];
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
/* ---------- 旁白闪避 + 混音 ---------- */
|
| 112 |
+
export function duckCurve(FILM, n, depth = .45) {
|
| 113 |
+
const g = new Float32Array(n).fill(1);
|
| 114 |
+
for (const A of FILM.acts) for (const s of A.subs) { const a = Math.floor((s.t0 - .25) * RATE), b = Math.floor((s.t1 + .35) * RATE); for (let i = Math.max(0, a); i < Math.min(n, b); i++) g[i] = depth; }
|
| 115 |
+
const k = 1 / (RATE * .18); let y = 1; for (let i = 0; i < n; i++) { y += (g[i] - y) * k * 4; g[i] = y; }
|
| 116 |
+
return g;
|
| 117 |
+
}
|
| 118 |
+
export function mixdown(FILM, [ML, MR], voices, musicGain = .5) {
|
| 119 |
+
const n = ML.length, d = duckCurve(FILM, n), L = new Float32Array(n), R = new Float32Array(n);
|
| 120 |
+
for (let i = 0; i < n; i++) { L[i] = ML[i] * d[i] * musicGain; R[i] = MR[i] * d[i] * musicGain; }
|
| 121 |
+
FILM.acts.forEach((A, j) => { const v = voices[j]; if (!v) return; const o = Math.floor(A.start * RATE + A.voice.at * RATE);
|
| 122 |
+
for (let i = 0; i < v.length && o + i < n; i++) { L[o + i] += v[i]; R[o + i] += v[i]; } });
|
| 123 |
+
let pk = 0; for (let i = 0; i < n; i++) pk = Math.max(pk, Math.abs(L[i]), Math.abs(R[i]));
|
| 124 |
+
const k = pk > .89 ? .89 / pk : 1; for (let i = 0; i < n; i++) { L[i] *= k; R[i] *= k; }
|
| 125 |
+
return [L, R];
|
| 126 |
+
}
|
| 127 |
+
export function toWav([L, R]) {
|
| 128 |
+
const n = L.length, b = new ArrayBuffer(44 + n * 4), v = new DataView(b), w = (o, s) => [...s].forEach((c, i) => v.setUint8(o + i, c.charCodeAt(0)));
|
| 129 |
+
w(0, 'RIFF'); v.setUint32(4, 36 + n * 4, true); w(8, 'WAVEfmt '); v.setUint32(16, 16, true); v.setUint16(20, 1, true); v.setUint16(22, 2, true);
|
| 130 |
+
v.setUint32(24, RATE, true); v.setUint32(28, RATE * 4, true); v.setUint16(32, 4, true); v.setUint16(34, 16, true); w(36, 'data'); v.setUint32(40, n * 4, true);
|
| 131 |
+
for (let i = 0; i < n; i++) { v.setInt16(44 + i * 4, Math.max(-1, Math.min(1, L[i])) * 32767, true); v.setInt16(46 + i * 4, Math.max(-1, Math.min(1, R[i])) * 32767, true); }
|
| 132 |
+
return b;
|
| 133 |
+
}
|
taurus/js/release.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ♉ 金牛座 —— 四平台发布文案与封面(全新编写)
|
| 2 |
+
import { clamp, h1, col, C, FONT, gilt, halo, text, divider, flower, petals, bullSide, taurusSign, sparkle, dust, ornamentFrame } from './canvas-kit.js';
|
| 3 |
+
import { drawCard } from './card.js';
|
| 4 |
+
|
| 5 |
+
export const charCount = s => [...s].length;
|
| 6 |
+
export const COVER_PX = { '4:3': [1440, 1080], '3:4': [1080, 1440], '16:9': [1920, 1080] };
|
| 7 |
+
|
| 8 |
+
const TAGS_ALL = ['金牛座', '星座', '十二星座', '星座文化', '金牛座女生', '金牛座男生', '天文', '昴星团', '星空', '治愈系', '浪漫', '古代天文', '希腊神话', '动画'];
|
| 9 |
+
export const PLATFORMS = [
|
| 10 |
+
{ id: 'xhs', name: '小红书', titleMax: 20, tagMax: 10, covers: ['3:4', '4:3'],
|
| 11 |
+
title: '金牛座的鎏金珍藏卡,请收下✨',
|
| 12 |
+
desc: '4 月 20 日—5 月 20 日出生的你,这条视频是写给你的。\n\n🌾 五千年前,春分的太阳落在金牛座,那是播种的季节\n⭐ 公牛之眼毕宿五、七姐妹昴星团,冬夜抬头就能找到\n🏮 1054 年北宋天文官记下“天关客星”,白昼可见 23 天——它就是今天的蟹状星云\n💐 欧罗巴与白牛的神话,让“欧洲”有了名字\n\n结尾有一张属于你的「金牛座 · 鎏金珍藏卡」,可以写上名字和生日,保存成壁纸或头像。\n慢慢来,也能抵达所有的美好。',
|
| 13 |
+
tags: ['金牛座', '星座', '十二星座', '星座文化', '金牛座女生', '星空', '昴星团', '治愈系', '浪漫', '天文'] },
|
| 14 |
+
{ id: 'douyin', name: '抖音', titleMax: 30, tagMax: 5, covers: ['3:4', '4:3'],
|
| 15 |
+
title: '金牛座:最古老的星座,藏着中国人的千年骄傲',
|
| 16 |
+
desc: '4.20—5.20 的金牛座看过来🐂 一万七千年前的壁画、北宋的天关客星、欧罗巴的白牛……最后送你一张鎏金珍藏卡,评论区晒出你的名字!',
|
| 17 |
+
tags: ['金牛座', '星座', '十二星座', '星座文化', '星空'] },
|
| 18 |
+
{ id: 'bili', name: 'B站', titleMax: 80, tagMax: 10, covers: ['16:9', '4:3'],
|
| 19 |
+
title: '【星座文化】金牛座全解:从一万七千年前的洞穴壁画、北宋“天关客星”到欧罗巴神话|附鎏金珍藏卡',
|
| 20 |
+
desc: '一支关于金牛座的原创动画短片(约 4 分 10 秒)。\n\n00:00 序章 · 繁花里的公牛\n00:27 最古老的星座\n00:53 找到它:猎户腰带 → 毕宿五 → 毕星团 → 昴星团\n01:23 毕宿五与昴星团\n01:53 天关客星:1054 年的超新星与蟹状星云\n02:23 欧罗巴与白牛\n02:52 ♉ 的含义\n03:08 土象 · 金星\n03:32 鎏金珍藏卡\n03:52 尾声 · 慢慢来\n\n星座是一种有趣、浪漫的文化。网页版可以输入名字与生日,生成并下载属于自己的珍藏卡。',
|
| 21 |
+
tags: ['金牛座', '星座', '十二星座', '星座文化', '天文', '古代天文', '希腊神话', '昴星团', '超新星', '动画短片'] },
|
| 22 |
+
{ id: 'channels', name: '视频号', titleMax: 16, tagMax: 99, covers: ['3:4', '4:3'],
|
| 23 |
+
title: '金牛座,慢慢来也很美好',
|
| 24 |
+
desc: '写给每一位金牛座:稳稳的你,值得一张鎏金珍藏卡。转给你身边那个 4 月 20 日—5 月 20 日出生的朋友吧。',
|
| 25 |
+
tags: ['金牛座', '星座', '十二星座', '星座文化', '星空', '天文', '治愈', '浪漫', '金牛座女生', '金牛座男生'] },
|
| 26 |
+
];
|
| 27 |
+
// 每个平台封面主副标题(不同平台给出不同的钩子)
|
| 28 |
+
const HOOK = {
|
| 29 |
+
xhs: ['金牛座', '这张鎏金珍藏卡\n属于你'], douyin: ['金牛座', '最古老的星座'], bili: ['金牛座', '从壁画到超新星'], channels: ['金牛座', '慢慢来 也很美好'],
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
function paintBg(g, W, H, warm = 0) {
|
| 33 |
+
const gr = g.createLinearGradient(0, 0, W * .3, H); gr.addColorStop(0, warm ? '#3a1a2c' : '#0d2a24'); gr.addColorStop(.55, warm ? '#2a1424' : '#1a1c26'); gr.addColorStop(1, '#0b0a0e');
|
| 34 |
+
g.fillStyle = gr; g.fillRect(0, 0, W, H);
|
| 35 |
+
const S = Math.min(W, H) / 1080;
|
| 36 |
+
for (let i = 0; i < 14; i++) halo(g, W * h1(i * 3.1), H * h1(i * 5.7), (120 + h1(i) * 260) * S, i % 2 ? [220, 120, 150] : [60, 150, 120], .16);
|
| 37 |
+
for (let i = 0; i < 180; i++) { g.fillStyle = col([255, 245, 230], .2 + .6 * h1(i * 9)); g.beginPath(); g.arc(W * h1(i * 1.3), H * h1(i * 2.9) * .8, (.6 + 1.8 * Math.pow(h1(i * 4), 3)) * S, 0, Math.PI * 2); g.fill(); }
|
| 38 |
+
const v = g.createRadialGradient(W / 2, H / 2, Math.min(W, H) * .3, W / 2, H / 2, Math.max(W, H) * .75); v.addColorStop(0, 'rgba(0,0,0,0)'); v.addColorStop(1, 'rgba(0,0,0,.55)'); g.fillStyle = v; g.fillRect(0, 0, W, H);
|
| 39 |
+
}
|
| 40 |
+
export { paintBg };
|
| 41 |
+
|
| 42 |
+
export function drawCover(g, W, H, pid, name = '') {
|
| 43 |
+
const S = Math.min(W, H) / 1080, P = H > W, [h1t, h2t] = HOOK[pid] || HOOK.xhs;
|
| 44 |
+
paintBg(g, W, H, pid === 'xhs' || pid === 'channels');
|
| 45 |
+
petals(g, 3.3, W, H, { n: 34, alpha: .8, seed: 5 });
|
| 46 |
+
// 四角花
|
| 47 |
+
[[0, 0], [W, 0], [0, H], [W, H]].forEach(([x, y], i) => { for (let k = 0; k < 4; k++) flower(g, x + (x ? -1 : 1) * (40 + k * 70 + h1(i * 7 + k) * 30) * S, y + (y ? -1 : 1) * (40 + h1(i + k * 3) * 120) * S, (34 + h1(k + i) * 30) * S, 1, { rot: i + k, inner: k % 2 ? [220, 100, 130] : [240, 170, 120], outer: [255, 236, 230] }); });
|
| 48 |
+
ornamentFrame(g, 26 * S, 26 * S, W - 52 * S, H - 52 * S, { S: S * .9, alpha: .9, metal: 'gold' });
|
| 49 |
+
// 卡片
|
| 50 |
+
const wide = W / H > 1.5, ch = P ? H * .5 : H * (wide ? .8 : .7), cx = P ? W / 2 : W * (wide ? .7 : .71), cy = P ? H * .64 : H * .52;
|
| 51 |
+
halo(g, cx, cy, ch * .9, [255, 200, 150], .7);
|
| 52 |
+
drawCard(g, cx, cy, ch, { t: 1.6, flip: 1, fields: 1, shine: .58, holo: .6, stamp: 1, holder: name, tilt: P ? 0 : -.05 });
|
| 53 |
+
for (let i = 0; i < 16; i++) sparkle(g, cx + (h1(i * 3) - .5) * ch * 1.1, cy + (h1(i * 7) - .5) * ch * 1.1, (2 + h1(i) * 4) * S, C.ivory, .9, .8);
|
| 54 |
+
// 标题区
|
| 55 |
+
const tx = P ? W / 2 : W * (wide ? .3 : .28), ty = P ? H * .19 : H * .33, big = (P ? 180 : wide ? 190 : 140) * S;
|
| 56 |
+
taurusSign(g, tx, ty - big * .95, (P ? .55 : .5) * S, 1, { width: 12 });
|
| 57 |
+
text(g, h1t, tx, ty, { size: big, gilt: 'gold', weight: 900, spacing: 10 * S, glow: 'rgba(255,170,120,.55)', blur: 30 * S });
|
| 58 |
+
const lines = h2t.split('\n'), sz = (P ? 72 : wide ? 68 : 58) * S;
|
| 59 |
+
if (P) text(g, lines.join(''), tx, ty + big * .78, { size: sz * (lines.join('').length > 8 ? .82 : 1), gilt: 'rose', weight: 900, spacing: 4 * S, outline: 'rgba(20,8,12,.7)', outlineW: 8 * S });
|
| 60 |
+
else lines.forEach((l, i) => text(g, l, tx, ty + big * .8 + i * sz * 1.25, { size: sz, gilt: 'rose', weight: 900, spacing: 4 * S, outline: 'rgba(20,8,12,.7)', outlineW: 8 * S }));
|
| 61 |
+
const by = P ? ty + big * .78 + 74 * S : ty + big * .8 + lines.length * sz * 1.25 + 30 * S;
|
| 62 |
+
divider(g, tx, by, 360 * S, 1, S);
|
| 63 |
+
text(g, 'TAURUS · 4.20 — 5.20', tx, by + 44 * S, { size: 30 * S, font: FONT.latin, weight: 700, color: col(C.gold), spacing: 4 * S });
|
| 64 |
+
if (!P) { bullSide(g, tx, H * .8, (wide ? .55 : .5) * S, { t: 2 }); }
|
| 65 |
+
dust(g, 2, W, H, { n: 40, alpha: .8 });
|
| 66 |
+
}
|
taurus/style.css
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root{--bg:#0b1613;--ink:#fff7e8;--mut:#cdbfae;--gold:#f3d38b;--rose:#ecaaa0;--em:#1f5c4a;--line:rgba(243,211,139,.28);--card:rgba(18,34,30,.72)}
|
| 2 |
+
*{box-sizing:border-box}
|
| 3 |
+
html{background:var(--bg)}
|
| 4 |
+
body{margin:0;color:var(--ink);font:16px/1.7 "Noto Sans SC","PingFang SC","Microsoft YaHei",sans-serif;
|
| 5 |
+
background:radial-gradient(1200px 700px at 15% -10%,rgba(40,128,98,.35),transparent 60%),radial-gradient(900px 600px at 100% 10%,rgba(200,110,140,.25),transparent 60%),linear-gradient(#0b1613,#120d12 70%);min-height:100vh}
|
| 6 |
+
a{color:var(--gold)}
|
| 7 |
+
button{font:inherit;color:inherit;cursor:pointer}
|
| 8 |
+
.hero{max-width:1180px;margin:0 auto;padding:28px 22px 8px;text-align:center}
|
| 9 |
+
.home{position:absolute;left:18px;top:14px;font-size:14px;text-decoration:none;opacity:.8}
|
| 10 |
+
.eyebrow{font:700 14px/1 "Cormorant Garamond",serif;letter-spacing:.32em;color:var(--rose);margin:22px 0 6px}
|
| 11 |
+
h1{font:900 clamp(38px,6vw,68px)/1.15 "Noto Serif SC",serif;margin:0;background:linear-gradient(100deg,#fff4d2,#f5d58a 25%,#c99a45 45%,#fbe3a6 60%,#b9873a 80%,#f1cf86);-webkit-background-clip:text;background-clip:text;color:transparent;letter-spacing:.06em}
|
| 12 |
+
h1 small{display:block;font-size:.38em;letter-spacing:.3em;margin-top:6px}
|
| 13 |
+
h1 .sig{font-family:serif}
|
| 14 |
+
.lead{color:var(--mut);max-width:780px;margin:14px auto 0}
|
| 15 |
+
.lead b{color:var(--gold)}
|
| 16 |
+
main{max-width:1180px;margin:0 auto;padding:12px 16px 40px}
|
| 17 |
+
.player{margin-top:18px}
|
| 18 |
+
.stage-wrap{position:relative;margin:0 auto;border-radius:18px;overflow:hidden;box-shadow:0 30px 80px rgba(0,0,0,.55),0 0 0 1px var(--line);background:#000;aspect-ratio:16/9;max-height:78vh}
|
| 19 |
+
.stage-wrap.portrait{aspect-ratio:9/16;height:min(78vh,1100px);max-width:100%}
|
| 20 |
+
#stage{display:block;width:100%;height:100%}
|
| 21 |
+
.big-play{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;background:radial-gradient(circle,rgba(0,0,0,.15),rgba(0,0,0,.5));border:0}
|
| 22 |
+
.big-play span{width:96px;height:96px;border-radius:50%;display:grid;place-items:center;font-size:34px;padding-left:6px;background:rgba(12,40,32,.7);box-shadow:0 0 0 2px var(--gold),0 0 40px rgba(243,211,139,.45)}
|
| 23 |
+
.big-play em{font-style:normal;color:var(--gold);letter-spacing:.2em;font-size:15px}
|
| 24 |
+
.big-play[hidden]{display:none}
|
| 25 |
+
.loading{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);font-size:13px;color:var(--mut);background:rgba(0,0,0,.5);padding:4px 12px;border-radius:99px}
|
| 26 |
+
.loading[hidden]{display:none}
|
| 27 |
+
.controls{display:flex;align-items:center;gap:10px;margin:12px auto 0;max-width:100%;flex-wrap:wrap}
|
| 28 |
+
.ic{width:42px;height:42px;border-radius:12px;border:1px solid var(--line);background:var(--card)}
|
| 29 |
+
.ic.on{color:var(--gold);border-color:var(--gold)}
|
| 30 |
+
.time{font:600 14px "Noto Sans SC";color:var(--mut);min-width:96px;font-variant-numeric:tabular-nums}
|
| 31 |
+
.bar{position:relative;flex:1;min-width:160px;height:10px;border-radius:9px;background:rgba(255,255,255,.1);cursor:pointer;touch-action:none}
|
| 32 |
+
.bar .fill{position:absolute;left:0;top:0;bottom:0;border-radius:9px;background:linear-gradient(90deg,#c98774,#f5d58a)}
|
| 33 |
+
.marks i{position:absolute;top:-3px;width:2px;height:16px;background:rgba(255,247,232,.45)}
|
| 34 |
+
.seg{display:inline-flex;border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--card)}
|
| 35 |
+
.seg button{border:0;background:none;padding:8px 14px;font-size:14px;color:var(--mut)}
|
| 36 |
+
.seg button.on{background:linear-gradient(120deg,#f5d58a,#c99a45);color:#1d1406;font-weight:700}
|
| 37 |
+
.chapters{list-style:none;display:grid;grid-template-columns:repeat(5,1fr);gap:8px;padding:0;margin:14px 0 0}
|
| 38 |
+
.chapters li{border:1px solid var(--line);border-radius:12px;padding:8px 10px;font-size:13px;color:var(--mut);cursor:pointer;background:var(--card);transition:.2s}
|
| 39 |
+
.chapters li b{display:block;color:var(--gold);font:700 12px "Cormorant Garamond",serif;letter-spacing:.15em}
|
| 40 |
+
.chapters li.on{border-color:var(--gold);color:var(--ink);box-shadow:0 0 18px rgba(243,211,139,.2)}
|
| 41 |
+
.panel{margin-top:34px;padding:26px;border-radius:22px;background:linear-gradient(160deg,rgba(24,52,44,.75),rgba(30,16,26,.75));box-shadow:0 0 0 1px var(--line),0 20px 60px rgba(0,0,0,.35)}
|
| 42 |
+
.panel-head h2{margin:0;font:900 28px "Noto Serif SC",serif;color:var(--gold);letter-spacing:.08em}
|
| 43 |
+
.panel-head p{margin:4px 0 18px;color:var(--mut);font-size:14px}
|
| 44 |
+
.card-grid{display:grid;grid-template-columns:minmax(0,420px) 1fr;gap:28px;align-items:center}
|
| 45 |
+
#cardView{width:100%;height:auto;border-radius:16px;background:#0b0a0e}
|
| 46 |
+
.card-form label{display:block;margin-bottom:14px;color:var(--mut);font-size:14px}
|
| 47 |
+
.card-form input,.card-form select{display:block;width:100%;margin-top:6px;padding:12px 14px;border-radius:12px;border:1px solid var(--line);background:rgba(0,0,0,.35);color:var(--ink);font:inherit;font-size:17px}
|
| 48 |
+
.card-form .row{display:flex;gap:10px}.card-form .row select{flex:1}
|
| 49 |
+
.hint{font-size:13px;color:var(--rose);margin:-4px 0 14px}
|
| 50 |
+
.btns{display:flex;gap:10px;flex-wrap:wrap}
|
| 51 |
+
.btns button,.primary{border:0;border-radius:12px;padding:12px 18px;font-weight:700;background:linear-gradient(120deg,#fff0d0,#f5d58a 40%,#c99a45);color:#1d1406;box-shadow:0 6px 20px rgba(243,211,139,.25)}
|
| 52 |
+
.ghost{margin-top:12px;border:1px solid var(--line);background:none;border-radius:12px;padding:10px 16px;color:var(--gold)}
|
| 53 |
+
.ex-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px 22px}
|
| 54 |
+
.ex-grid label{display:flex;align-items:center;gap:12px;color:var(--mut);font-size:14px;flex-wrap:wrap}
|
| 55 |
+
.ex-run{display:flex;align-items:center;gap:14px;margin-top:20px;flex-wrap:wrap}
|
| 56 |
+
.ex-run .ghost{margin:0}
|
| 57 |
+
.prog{flex:1;min-width:180px;height:8px;border-radius:9px;background:rgba(255,255,255,.1);overflow:hidden}
|
| 58 |
+
.prog div{height:100%;width:0;background:linear-gradient(90deg,#c98774,#f5d58a)}
|
| 59 |
+
.msg{font-size:13px;color:var(--mut);width:100%}
|
| 60 |
+
.ex-out a{display:inline-block;margin-top:10px;margin-right:10px;padding:10px 16px;border-radius:12px;border:1px solid var(--gold);text-decoration:none}
|
| 61 |
+
.ex-out video{display:block;max-width:100%;max-height:60vh;margin-top:12px;border-radius:12px}
|
| 62 |
+
.tabs{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px}
|
| 63 |
+
.tabs button{border:1px solid var(--line);background:var(--card);border-radius:99px;padding:8px 18px;color:var(--mut)}
|
| 64 |
+
.tabs button.on{border-color:var(--gold);color:#1d1406;background:linear-gradient(120deg,#f5d58a,#c99a45);font-weight:700}
|
| 65 |
+
.pub{display:grid;grid-template-columns:1.1fr 1fr;gap:24px}
|
| 66 |
+
.field{margin-bottom:14px}
|
| 67 |
+
.field h4{display:flex;justify-content:space-between;align-items:center;margin:0 0 6px;font-size:14px;color:var(--gold)}
|
| 68 |
+
.field h4 small{color:var(--mut);font-weight:400}
|
| 69 |
+
.field h4 small.ok::before{content:"✓ ";color:#8fd18a}
|
| 70 |
+
.field pre{white-space:pre-wrap;margin:0;padding:12px 14px;border-radius:12px;background:rgba(0,0,0,.3);border:1px solid var(--line);font:15px/1.7 "Noto Sans SC",sans-serif}
|
| 71 |
+
.copy{font-size:12px;border:1px solid var(--line);background:none;border-radius:8px;padding:2px 10px;color:var(--gold);margin-left:8px}
|
| 72 |
+
.covers{display:grid;grid-template-columns:1fr 1fr;gap:12px;align-items:start}
|
| 73 |
+
.covers figure{margin:0}
|
| 74 |
+
.covers img{width:100%;border-radius:12px;box-shadow:0 0 0 1px var(--line)}
|
| 75 |
+
.covers figcaption{font-size:12px;color:var(--mut);display:flex;justify-content:space-between}
|
| 76 |
+
footer{text-align:center;color:var(--mut);font-size:13px;padding:24px 16px 40px}
|
| 77 |
+
@media (max-width:820px){
|
| 78 |
+
.hero{padding-top:40px}
|
| 79 |
+
.lead br{display:none}
|
| 80 |
+
main{padding:8px 10px 30px}
|
| 81 |
+
.stage-wrap.portrait{height:auto;width:100%;max-height:none}
|
| 82 |
+
.chapters{grid-template-columns:repeat(2,1fr)}
|
| 83 |
+
.card-grid,.pub{grid-template-columns:1fr}
|
| 84 |
+
.ex-grid{grid-template-columns:1fr}
|
| 85 |
+
.panel{padding:18px;border-radius:18px}
|
| 86 |
+
.controls .time{order:5;width:100%;text-align:center}
|
| 87 |
+
}
|
taurus/tools/mix.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 离线渲染配乐 + 旁白混音:node tools/mix.mjs (需要环境变量 FF 指向 ffmpeg)
|
| 2 |
+
import fs from 'fs'; import path from 'path'; import { execFileSync } from 'child_process'; import { fileURLToPath } from 'url';
|
| 3 |
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
| 4 |
+
const src = f => 'data:text/javascript;base64,' + Buffer.from(fs.readFileSync(path.join(ROOT, f), 'utf8')).toString('base64');
|
| 5 |
+
const { FILM } = await import(src('js/script.js'));
|
| 6 |
+
const M = await import(src('js/music.js'));
|
| 7 |
+
const FF = process.env.FF || 'ffmpeg';
|
| 8 |
+
console.time('score'); const score = M.composeScore(FILM); console.timeEnd('score');
|
| 9 |
+
const voices = FILM.acts.map(A => { const raw = execFileSync(FF, ['-v', 'error', '-i', path.join(ROOT, A.voice.file), '-f', 'f32le', '-ac', '1', '-ar', String(M.RATE), '-'], { maxBuffer: 1 << 28 });
|
| 10 |
+
return new Float32Array(raw.buffer, raw.byteOffset, raw.length / 4); });
|
| 11 |
+
const enc = (wav, out, br) => { const tmp = out + '.wav'; fs.writeFileSync(tmp, Buffer.from(wav)); execFileSync(FF, ['-v', 'error', '-y', '-i', tmp, '-af', 'loudnorm=I=-16:TP=-1.5:LRA=11', '-ar', '44100', '-c:a', 'libmp3lame', '-b:a', br, out]); fs.unlinkSync(tmp); };
|
| 12 |
+
const peak = ([L, R]) => { let p = 0, s = 0; for (let i = 0; i < L.length; i++) { p = Math.max(p, Math.abs(L[i])); s += L[i] * L[i]; } return [p.toFixed(3), Math.sqrt(s / L.length).toFixed(4)]; };
|
| 13 |
+
console.log('score peak/rms', peak(score));
|
| 14 |
+
const mix = M.mixdown(FILM, score, voices, +(process.env.MG || .7));
|
| 15 |
+
console.log('mix peak/rms', peak(mix));
|
| 16 |
+
enc(M.toWav(mix), path.join(ROOT, 'audio/film.mp3'), '192k');
|
| 17 |
+
console.log('ok', (mix[0].length / M.RATE).toFixed(2) + 's');
|
taurus/vendor/mp4-muxer.mjs
ADDED
|
@@ -0,0 +1,1879 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
var __accessCheck = (obj, member, msg) => {
|
| 2 |
+
if (!member.has(obj))
|
| 3 |
+
throw TypeError("Cannot " + msg);
|
| 4 |
+
};
|
| 5 |
+
var __privateGet = (obj, member, getter) => {
|
| 6 |
+
__accessCheck(obj, member, "read from private field");
|
| 7 |
+
return getter ? getter.call(obj) : member.get(obj);
|
| 8 |
+
};
|
| 9 |
+
var __privateAdd = (obj, member, value) => {
|
| 10 |
+
if (member.has(obj))
|
| 11 |
+
throw TypeError("Cannot add the same private member more than once");
|
| 12 |
+
member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
| 13 |
+
};
|
| 14 |
+
var __privateSet = (obj, member, value, setter) => {
|
| 15 |
+
__accessCheck(obj, member, "write to private field");
|
| 16 |
+
setter ? setter.call(obj, value) : member.set(obj, value);
|
| 17 |
+
return value;
|
| 18 |
+
};
|
| 19 |
+
var __privateWrapper = (obj, member, setter, getter) => ({
|
| 20 |
+
set _(value) {
|
| 21 |
+
__privateSet(obj, member, value, setter);
|
| 22 |
+
},
|
| 23 |
+
get _() {
|
| 24 |
+
return __privateGet(obj, member, getter);
|
| 25 |
+
}
|
| 26 |
+
});
|
| 27 |
+
var __privateMethod = (obj, member, method) => {
|
| 28 |
+
__accessCheck(obj, member, "access private method");
|
| 29 |
+
return method;
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
// src/misc.ts
|
| 33 |
+
var bytes = new Uint8Array(8);
|
| 34 |
+
var view = new DataView(bytes.buffer);
|
| 35 |
+
var u8 = (value) => {
|
| 36 |
+
return [(value % 256 + 256) % 256];
|
| 37 |
+
};
|
| 38 |
+
var u16 = (value) => {
|
| 39 |
+
view.setUint16(0, value, false);
|
| 40 |
+
return [bytes[0], bytes[1]];
|
| 41 |
+
};
|
| 42 |
+
var i16 = (value) => {
|
| 43 |
+
view.setInt16(0, value, false);
|
| 44 |
+
return [bytes[0], bytes[1]];
|
| 45 |
+
};
|
| 46 |
+
var u24 = (value) => {
|
| 47 |
+
view.setUint32(0, value, false);
|
| 48 |
+
return [bytes[1], bytes[2], bytes[3]];
|
| 49 |
+
};
|
| 50 |
+
var u32 = (value) => {
|
| 51 |
+
view.setUint32(0, value, false);
|
| 52 |
+
return [bytes[0], bytes[1], bytes[2], bytes[3]];
|
| 53 |
+
};
|
| 54 |
+
var i32 = (value) => {
|
| 55 |
+
view.setInt32(0, value, false);
|
| 56 |
+
return [bytes[0], bytes[1], bytes[2], bytes[3]];
|
| 57 |
+
};
|
| 58 |
+
var u64 = (value) => {
|
| 59 |
+
view.setUint32(0, Math.floor(value / 2 ** 32), false);
|
| 60 |
+
view.setUint32(4, value, false);
|
| 61 |
+
return [bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]];
|
| 62 |
+
};
|
| 63 |
+
var fixed_8_8 = (value) => {
|
| 64 |
+
view.setInt16(0, 2 ** 8 * value, false);
|
| 65 |
+
return [bytes[0], bytes[1]];
|
| 66 |
+
};
|
| 67 |
+
var fixed_16_16 = (value) => {
|
| 68 |
+
view.setInt32(0, 2 ** 16 * value, false);
|
| 69 |
+
return [bytes[0], bytes[1], bytes[2], bytes[3]];
|
| 70 |
+
};
|
| 71 |
+
var fixed_2_30 = (value) => {
|
| 72 |
+
view.setInt32(0, 2 ** 30 * value, false);
|
| 73 |
+
return [bytes[0], bytes[1], bytes[2], bytes[3]];
|
| 74 |
+
};
|
| 75 |
+
var ascii = (text, nullTerminated = false) => {
|
| 76 |
+
let bytes2 = Array(text.length).fill(null).map((_, i) => text.charCodeAt(i));
|
| 77 |
+
if (nullTerminated)
|
| 78 |
+
bytes2.push(0);
|
| 79 |
+
return bytes2;
|
| 80 |
+
};
|
| 81 |
+
var last = (arr) => {
|
| 82 |
+
return arr && arr[arr.length - 1];
|
| 83 |
+
};
|
| 84 |
+
var lastPresentedSample = (samples) => {
|
| 85 |
+
let result = void 0;
|
| 86 |
+
for (let sample of samples) {
|
| 87 |
+
if (!result || sample.presentationTimestamp > result.presentationTimestamp) {
|
| 88 |
+
result = sample;
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
return result;
|
| 92 |
+
};
|
| 93 |
+
var intoTimescale = (timeInSeconds, timescale, round = true) => {
|
| 94 |
+
let value = timeInSeconds * timescale;
|
| 95 |
+
return round ? Math.round(value) : value;
|
| 96 |
+
};
|
| 97 |
+
var rotationMatrix = (rotationInDegrees) => {
|
| 98 |
+
let theta = rotationInDegrees * (Math.PI / 180);
|
| 99 |
+
let cosTheta = Math.cos(theta);
|
| 100 |
+
let sinTheta = Math.sin(theta);
|
| 101 |
+
return [
|
| 102 |
+
cosTheta,
|
| 103 |
+
sinTheta,
|
| 104 |
+
0,
|
| 105 |
+
-sinTheta,
|
| 106 |
+
cosTheta,
|
| 107 |
+
0,
|
| 108 |
+
0,
|
| 109 |
+
0,
|
| 110 |
+
1
|
| 111 |
+
];
|
| 112 |
+
};
|
| 113 |
+
var IDENTITY_MATRIX = rotationMatrix(0);
|
| 114 |
+
var matrixToBytes = (matrix) => {
|
| 115 |
+
return [
|
| 116 |
+
fixed_16_16(matrix[0]),
|
| 117 |
+
fixed_16_16(matrix[1]),
|
| 118 |
+
fixed_2_30(matrix[2]),
|
| 119 |
+
fixed_16_16(matrix[3]),
|
| 120 |
+
fixed_16_16(matrix[4]),
|
| 121 |
+
fixed_2_30(matrix[5]),
|
| 122 |
+
fixed_16_16(matrix[6]),
|
| 123 |
+
fixed_16_16(matrix[7]),
|
| 124 |
+
fixed_2_30(matrix[8])
|
| 125 |
+
];
|
| 126 |
+
};
|
| 127 |
+
var deepClone = (x) => {
|
| 128 |
+
if (!x)
|
| 129 |
+
return x;
|
| 130 |
+
if (typeof x !== "object")
|
| 131 |
+
return x;
|
| 132 |
+
if (Array.isArray(x))
|
| 133 |
+
return x.map(deepClone);
|
| 134 |
+
return Object.fromEntries(Object.entries(x).map(([key, value]) => [key, deepClone(value)]));
|
| 135 |
+
};
|
| 136 |
+
var isU32 = (value) => {
|
| 137 |
+
return value >= 0 && value < 2 ** 32;
|
| 138 |
+
};
|
| 139 |
+
|
| 140 |
+
// src/box.ts
|
| 141 |
+
var box = (type, contents, children) => ({
|
| 142 |
+
type,
|
| 143 |
+
contents: contents && new Uint8Array(contents.flat(10)),
|
| 144 |
+
children
|
| 145 |
+
});
|
| 146 |
+
var fullBox = (type, version, flags, contents, children) => box(
|
| 147 |
+
type,
|
| 148 |
+
[u8(version), u24(flags), contents ?? []],
|
| 149 |
+
children
|
| 150 |
+
);
|
| 151 |
+
var ftyp = (details) => {
|
| 152 |
+
let minorVersion = 512;
|
| 153 |
+
if (details.fragmented)
|
| 154 |
+
return box("ftyp", [
|
| 155 |
+
ascii("iso5"),
|
| 156 |
+
// Major brand
|
| 157 |
+
u32(minorVersion),
|
| 158 |
+
// Minor version
|
| 159 |
+
// Compatible brands
|
| 160 |
+
ascii("iso5"),
|
| 161 |
+
ascii("iso6"),
|
| 162 |
+
ascii("mp41")
|
| 163 |
+
]);
|
| 164 |
+
return box("ftyp", [
|
| 165 |
+
ascii("isom"),
|
| 166 |
+
// Major brand
|
| 167 |
+
u32(minorVersion),
|
| 168 |
+
// Minor version
|
| 169 |
+
// Compatible brands
|
| 170 |
+
ascii("isom"),
|
| 171 |
+
details.holdsAvc ? ascii("avc1") : [],
|
| 172 |
+
ascii("mp41")
|
| 173 |
+
]);
|
| 174 |
+
};
|
| 175 |
+
var mdat = (reserveLargeSize) => ({ type: "mdat", largeSize: reserveLargeSize });
|
| 176 |
+
var free = (size) => ({ type: "free", size });
|
| 177 |
+
var moov = (tracks, creationTime, fragmented = false) => box("moov", null, [
|
| 178 |
+
mvhd(creationTime, tracks),
|
| 179 |
+
...tracks.map((x) => trak(x, creationTime)),
|
| 180 |
+
fragmented ? mvex(tracks) : null
|
| 181 |
+
]);
|
| 182 |
+
var mvhd = (creationTime, tracks) => {
|
| 183 |
+
let duration = intoTimescale(Math.max(
|
| 184 |
+
0,
|
| 185 |
+
...tracks.filter((x) => x.samples.length > 0).map((x) => {
|
| 186 |
+
const lastSample = lastPresentedSample(x.samples);
|
| 187 |
+
return lastSample.presentationTimestamp + lastSample.duration;
|
| 188 |
+
})
|
| 189 |
+
), GLOBAL_TIMESCALE);
|
| 190 |
+
let nextTrackId = Math.max(...tracks.map((x) => x.id)) + 1;
|
| 191 |
+
let needsU64 = !isU32(creationTime) || !isU32(duration);
|
| 192 |
+
let u32OrU64 = needsU64 ? u64 : u32;
|
| 193 |
+
return fullBox("mvhd", +needsU64, 0, [
|
| 194 |
+
u32OrU64(creationTime),
|
| 195 |
+
// Creation time
|
| 196 |
+
u32OrU64(creationTime),
|
| 197 |
+
// Modification time
|
| 198 |
+
u32(GLOBAL_TIMESCALE),
|
| 199 |
+
// Timescale
|
| 200 |
+
u32OrU64(duration),
|
| 201 |
+
// Duration
|
| 202 |
+
fixed_16_16(1),
|
| 203 |
+
// Preferred rate
|
| 204 |
+
fixed_8_8(1),
|
| 205 |
+
// Preferred volume
|
| 206 |
+
Array(10).fill(0),
|
| 207 |
+
// Reserved
|
| 208 |
+
matrixToBytes(IDENTITY_MATRIX),
|
| 209 |
+
// Matrix
|
| 210 |
+
Array(24).fill(0),
|
| 211 |
+
// Pre-defined
|
| 212 |
+
u32(nextTrackId)
|
| 213 |
+
// Next track ID
|
| 214 |
+
]);
|
| 215 |
+
};
|
| 216 |
+
var trak = (track, creationTime) => box("trak", null, [
|
| 217 |
+
tkhd(track, creationTime),
|
| 218 |
+
mdia(track, creationTime)
|
| 219 |
+
]);
|
| 220 |
+
var tkhd = (track, creationTime) => {
|
| 221 |
+
let lastSample = lastPresentedSample(track.samples);
|
| 222 |
+
let durationInGlobalTimescale = intoTimescale(
|
| 223 |
+
lastSample ? lastSample.presentationTimestamp + lastSample.duration : 0,
|
| 224 |
+
GLOBAL_TIMESCALE
|
| 225 |
+
);
|
| 226 |
+
let needsU64 = !isU32(creationTime) || !isU32(durationInGlobalTimescale);
|
| 227 |
+
let u32OrU64 = needsU64 ? u64 : u32;
|
| 228 |
+
let matrix;
|
| 229 |
+
if (track.info.type === "video") {
|
| 230 |
+
matrix = typeof track.info.rotation === "number" ? rotationMatrix(track.info.rotation) : track.info.rotation;
|
| 231 |
+
} else {
|
| 232 |
+
matrix = IDENTITY_MATRIX;
|
| 233 |
+
}
|
| 234 |
+
return fullBox("tkhd", +needsU64, 3, [
|
| 235 |
+
u32OrU64(creationTime),
|
| 236 |
+
// Creation time
|
| 237 |
+
u32OrU64(creationTime),
|
| 238 |
+
// Modification time
|
| 239 |
+
u32(track.id),
|
| 240 |
+
// Track ID
|
| 241 |
+
u32(0),
|
| 242 |
+
// Reserved
|
| 243 |
+
u32OrU64(durationInGlobalTimescale),
|
| 244 |
+
// Duration
|
| 245 |
+
Array(8).fill(0),
|
| 246 |
+
// Reserved
|
| 247 |
+
u16(0),
|
| 248 |
+
// Layer
|
| 249 |
+
u16(0),
|
| 250 |
+
// Alternate group
|
| 251 |
+
fixed_8_8(track.info.type === "audio" ? 1 : 0),
|
| 252 |
+
// Volume
|
| 253 |
+
u16(0),
|
| 254 |
+
// Reserved
|
| 255 |
+
matrixToBytes(matrix),
|
| 256 |
+
// Matrix
|
| 257 |
+
fixed_16_16(track.info.type === "video" ? track.info.width : 0),
|
| 258 |
+
// Track width
|
| 259 |
+
fixed_16_16(track.info.type === "video" ? track.info.height : 0)
|
| 260 |
+
// Track height
|
| 261 |
+
]);
|
| 262 |
+
};
|
| 263 |
+
var mdia = (track, creationTime) => box("mdia", null, [
|
| 264 |
+
mdhd(track, creationTime),
|
| 265 |
+
hdlr(track.info.type === "video" ? "vide" : "soun"),
|
| 266 |
+
minf(track)
|
| 267 |
+
]);
|
| 268 |
+
var mdhd = (track, creationTime) => {
|
| 269 |
+
let lastSample = lastPresentedSample(track.samples);
|
| 270 |
+
let localDuration = intoTimescale(
|
| 271 |
+
lastSample ? lastSample.presentationTimestamp + lastSample.duration : 0,
|
| 272 |
+
track.timescale
|
| 273 |
+
);
|
| 274 |
+
let needsU64 = !isU32(creationTime) || !isU32(localDuration);
|
| 275 |
+
let u32OrU64 = needsU64 ? u64 : u32;
|
| 276 |
+
return fullBox("mdhd", +needsU64, 0, [
|
| 277 |
+
u32OrU64(creationTime),
|
| 278 |
+
// Creation time
|
| 279 |
+
u32OrU64(creationTime),
|
| 280 |
+
// Modification time
|
| 281 |
+
u32(track.timescale),
|
| 282 |
+
// Timescale
|
| 283 |
+
u32OrU64(localDuration),
|
| 284 |
+
// Duration
|
| 285 |
+
u16(21956),
|
| 286 |
+
// Language ("und", undetermined)
|
| 287 |
+
u16(0)
|
| 288 |
+
// Quality
|
| 289 |
+
]);
|
| 290 |
+
};
|
| 291 |
+
var hdlr = (componentSubtype) => fullBox("hdlr", 0, 0, [
|
| 292 |
+
ascii("mhlr"),
|
| 293 |
+
// Component type
|
| 294 |
+
ascii(componentSubtype),
|
| 295 |
+
// Component subtype
|
| 296 |
+
u32(0),
|
| 297 |
+
// Component manufacturer
|
| 298 |
+
u32(0),
|
| 299 |
+
// Component flags
|
| 300 |
+
u32(0),
|
| 301 |
+
// Component flags mask
|
| 302 |
+
ascii("mp4-muxer-hdlr", true)
|
| 303 |
+
// Component name
|
| 304 |
+
]);
|
| 305 |
+
var minf = (track) => box("minf", null, [
|
| 306 |
+
track.info.type === "video" ? vmhd() : smhd(),
|
| 307 |
+
dinf(),
|
| 308 |
+
stbl(track)
|
| 309 |
+
]);
|
| 310 |
+
var vmhd = () => fullBox("vmhd", 0, 1, [
|
| 311 |
+
u16(0),
|
| 312 |
+
// Graphics mode
|
| 313 |
+
u16(0),
|
| 314 |
+
// Opcolor R
|
| 315 |
+
u16(0),
|
| 316 |
+
// Opcolor G
|
| 317 |
+
u16(0)
|
| 318 |
+
// Opcolor B
|
| 319 |
+
]);
|
| 320 |
+
var smhd = () => fullBox("smhd", 0, 0, [
|
| 321 |
+
u16(0),
|
| 322 |
+
// Balance
|
| 323 |
+
u16(0)
|
| 324 |
+
// Reserved
|
| 325 |
+
]);
|
| 326 |
+
var dinf = () => box("dinf", null, [
|
| 327 |
+
dref()
|
| 328 |
+
]);
|
| 329 |
+
var dref = () => fullBox("dref", 0, 0, [
|
| 330 |
+
u32(1)
|
| 331 |
+
// Entry count
|
| 332 |
+
], [
|
| 333 |
+
url()
|
| 334 |
+
]);
|
| 335 |
+
var url = () => fullBox("url ", 0, 1);
|
| 336 |
+
var stbl = (track) => {
|
| 337 |
+
const needsCtts = track.compositionTimeOffsetTable.length > 1 || track.compositionTimeOffsetTable.some((x) => x.sampleCompositionTimeOffset !== 0);
|
| 338 |
+
return box("stbl", null, [
|
| 339 |
+
stsd(track),
|
| 340 |
+
stts(track),
|
| 341 |
+
stss(track),
|
| 342 |
+
stsc(track),
|
| 343 |
+
stsz(track),
|
| 344 |
+
stco(track),
|
| 345 |
+
needsCtts ? ctts(track) : null
|
| 346 |
+
]);
|
| 347 |
+
};
|
| 348 |
+
var stsd = (track) => fullBox("stsd", 0, 0, [
|
| 349 |
+
u32(1)
|
| 350 |
+
// Entry count
|
| 351 |
+
], [
|
| 352 |
+
track.info.type === "video" ? videoSampleDescription(
|
| 353 |
+
VIDEO_CODEC_TO_BOX_NAME[track.info.codec],
|
| 354 |
+
track
|
| 355 |
+
) : soundSampleDescription(
|
| 356 |
+
AUDIO_CODEC_TO_BOX_NAME[track.info.codec],
|
| 357 |
+
track
|
| 358 |
+
)
|
| 359 |
+
]);
|
| 360 |
+
var videoSampleDescription = (compressionType, track) => box(compressionType, [
|
| 361 |
+
Array(6).fill(0),
|
| 362 |
+
// Reserved
|
| 363 |
+
u16(1),
|
| 364 |
+
// Data reference index
|
| 365 |
+
u16(0),
|
| 366 |
+
// Pre-defined
|
| 367 |
+
u16(0),
|
| 368 |
+
// Reserved
|
| 369 |
+
Array(12).fill(0),
|
| 370 |
+
// Pre-defined
|
| 371 |
+
u16(track.info.width),
|
| 372 |
+
// Width
|
| 373 |
+
u16(track.info.height),
|
| 374 |
+
// Height
|
| 375 |
+
u32(4718592),
|
| 376 |
+
// Horizontal resolution
|
| 377 |
+
u32(4718592),
|
| 378 |
+
// Vertical resolution
|
| 379 |
+
u32(0),
|
| 380 |
+
// Reserved
|
| 381 |
+
u16(1),
|
| 382 |
+
// Frame count
|
| 383 |
+
Array(32).fill(0),
|
| 384 |
+
// Compressor name
|
| 385 |
+
u16(24),
|
| 386 |
+
// Depth
|
| 387 |
+
i16(65535)
|
| 388 |
+
// Pre-defined
|
| 389 |
+
], [
|
| 390 |
+
VIDEO_CODEC_TO_CONFIGURATION_BOX[track.info.codec](track),
|
| 391 |
+
track.info.decoderConfig.colorSpace ? colr(track) : null
|
| 392 |
+
]);
|
| 393 |
+
var COLOR_PRIMARIES_MAP = {
|
| 394 |
+
"bt709": 1,
|
| 395 |
+
// ITU-R BT.709
|
| 396 |
+
"bt470bg": 5,
|
| 397 |
+
// ITU-R BT.470BG
|
| 398 |
+
"smpte170m": 6
|
| 399 |
+
// ITU-R BT.601 525 - SMPTE 170M
|
| 400 |
+
};
|
| 401 |
+
var TRANSFER_CHARACTERISTICS_MAP = {
|
| 402 |
+
"bt709": 1,
|
| 403 |
+
// ITU-R BT.709
|
| 404 |
+
"smpte170m": 6,
|
| 405 |
+
// SMPTE 170M
|
| 406 |
+
"iec61966-2-1": 13
|
| 407 |
+
// IEC 61966-2-1
|
| 408 |
+
};
|
| 409 |
+
var MATRIX_COEFFICIENTS_MAP = {
|
| 410 |
+
"rgb": 0,
|
| 411 |
+
// Identity
|
| 412 |
+
"bt709": 1,
|
| 413 |
+
// ITU-R BT.709
|
| 414 |
+
"bt470bg": 5,
|
| 415 |
+
// ITU-R BT.470BG
|
| 416 |
+
"smpte170m": 6
|
| 417 |
+
// SMPTE 170M
|
| 418 |
+
};
|
| 419 |
+
var colr = (track) => box("colr", [
|
| 420 |
+
ascii("nclx"),
|
| 421 |
+
// Colour type
|
| 422 |
+
u16(COLOR_PRIMARIES_MAP[track.info.decoderConfig.colorSpace.primaries]),
|
| 423 |
+
// Colour primaries
|
| 424 |
+
u16(TRANSFER_CHARACTERISTICS_MAP[track.info.decoderConfig.colorSpace.transfer]),
|
| 425 |
+
// Transfer characteristics
|
| 426 |
+
u16(MATRIX_COEFFICIENTS_MAP[track.info.decoderConfig.colorSpace.matrix]),
|
| 427 |
+
// Matrix coefficients
|
| 428 |
+
u8((track.info.decoderConfig.colorSpace.fullRange ? 1 : 0) << 7)
|
| 429 |
+
// Full range flag
|
| 430 |
+
]);
|
| 431 |
+
var avcC = (track) => track.info.decoderConfig && box("avcC", [
|
| 432 |
+
// For AVC, description is an AVCDecoderConfigurationRecord, so nothing else to do here
|
| 433 |
+
...new Uint8Array(track.info.decoderConfig.description)
|
| 434 |
+
]);
|
| 435 |
+
var hvcC = (track) => track.info.decoderConfig && box("hvcC", [
|
| 436 |
+
// For HEVC, description is a HEVCDecoderConfigurationRecord, so nothing else to do here
|
| 437 |
+
...new Uint8Array(track.info.decoderConfig.description)
|
| 438 |
+
]);
|
| 439 |
+
var vpcC = (track) => {
|
| 440 |
+
if (!track.info.decoderConfig) {
|
| 441 |
+
return null;
|
| 442 |
+
}
|
| 443 |
+
let decoderConfig = track.info.decoderConfig;
|
| 444 |
+
if (!decoderConfig.colorSpace) {
|
| 445 |
+
throw new Error(`'colorSpace' is required in the decoder config for VP9.`);
|
| 446 |
+
}
|
| 447 |
+
let parts = decoderConfig.codec.split(".");
|
| 448 |
+
let profile = Number(parts[1]);
|
| 449 |
+
let level = Number(parts[2]);
|
| 450 |
+
let bitDepth = Number(parts[3]);
|
| 451 |
+
let chromaSubsampling = 0;
|
| 452 |
+
let thirdByte = (bitDepth << 4) + (chromaSubsampling << 1) + Number(decoderConfig.colorSpace.fullRange);
|
| 453 |
+
let colourPrimaries = 2;
|
| 454 |
+
let transferCharacteristics = 2;
|
| 455 |
+
let matrixCoefficients = 2;
|
| 456 |
+
return fullBox("vpcC", 1, 0, [
|
| 457 |
+
u8(profile),
|
| 458 |
+
// Profile
|
| 459 |
+
u8(level),
|
| 460 |
+
// Level
|
| 461 |
+
u8(thirdByte),
|
| 462 |
+
// Bit depth, chroma subsampling, full range
|
| 463 |
+
u8(colourPrimaries),
|
| 464 |
+
// Colour primaries
|
| 465 |
+
u8(transferCharacteristics),
|
| 466 |
+
// Transfer characteristics
|
| 467 |
+
u8(matrixCoefficients),
|
| 468 |
+
// Matrix coefficients
|
| 469 |
+
u16(0)
|
| 470 |
+
// Codec initialization data size
|
| 471 |
+
]);
|
| 472 |
+
};
|
| 473 |
+
var av1C = () => {
|
| 474 |
+
let marker = 1;
|
| 475 |
+
let version = 1;
|
| 476 |
+
let firstByte = (marker << 7) + version;
|
| 477 |
+
return box("av1C", [
|
| 478 |
+
firstByte,
|
| 479 |
+
0,
|
| 480 |
+
0,
|
| 481 |
+
0
|
| 482 |
+
]);
|
| 483 |
+
};
|
| 484 |
+
var soundSampleDescription = (compressionType, track) => box(compressionType, [
|
| 485 |
+
Array(6).fill(0),
|
| 486 |
+
// Reserved
|
| 487 |
+
u16(1),
|
| 488 |
+
// Data reference index
|
| 489 |
+
u16(0),
|
| 490 |
+
// Version
|
| 491 |
+
u16(0),
|
| 492 |
+
// Revision level
|
| 493 |
+
u32(0),
|
| 494 |
+
// Vendor
|
| 495 |
+
u16(track.info.numberOfChannels),
|
| 496 |
+
// Number of channels
|
| 497 |
+
u16(16),
|
| 498 |
+
// Sample size (bits)
|
| 499 |
+
u16(0),
|
| 500 |
+
// Compression ID
|
| 501 |
+
u16(0),
|
| 502 |
+
// Packet size
|
| 503 |
+
fixed_16_16(track.info.sampleRate)
|
| 504 |
+
// Sample rate
|
| 505 |
+
], [
|
| 506 |
+
AUDIO_CODEC_TO_CONFIGURATION_BOX[track.info.codec](track)
|
| 507 |
+
]);
|
| 508 |
+
var esds = (track) => {
|
| 509 |
+
let description = new Uint8Array(track.info.decoderConfig.description);
|
| 510 |
+
return fullBox("esds", 0, 0, [
|
| 511 |
+
// https://stackoverflow.com/a/54803118
|
| 512 |
+
u32(58753152),
|
| 513 |
+
// TAG(3) = Object Descriptor ([2])
|
| 514 |
+
u8(32 + description.byteLength),
|
| 515 |
+
// length of this OD (which includes the next 2 tags)
|
| 516 |
+
u16(1),
|
| 517 |
+
// ES_ID = 1
|
| 518 |
+
u8(0),
|
| 519 |
+
// flags etc = 0
|
| 520 |
+
u32(75530368),
|
| 521 |
+
// TAG(4) = ES Descriptor ([2]) embedded in above OD
|
| 522 |
+
u8(18 + description.byteLength),
|
| 523 |
+
// length of this ESD
|
| 524 |
+
u8(64),
|
| 525 |
+
// MPEG-4 Audio
|
| 526 |
+
u8(21),
|
| 527 |
+
// stream type(6bits)=5 audio, flags(2bits)=1
|
| 528 |
+
u24(0),
|
| 529 |
+
// 24bit buffer size
|
| 530 |
+
u32(130071),
|
| 531 |
+
// max bitrate
|
| 532 |
+
u32(130071),
|
| 533 |
+
// avg bitrate
|
| 534 |
+
u32(92307584),
|
| 535 |
+
// TAG(5) = ASC ([2],[3]) embedded in above OD
|
| 536 |
+
u8(description.byteLength),
|
| 537 |
+
// length
|
| 538 |
+
...description,
|
| 539 |
+
u32(109084800),
|
| 540 |
+
// TAG(6)
|
| 541 |
+
u8(1),
|
| 542 |
+
// length
|
| 543 |
+
u8(2)
|
| 544 |
+
// data
|
| 545 |
+
]);
|
| 546 |
+
};
|
| 547 |
+
var dOps = (track) => {
|
| 548 |
+
let preskip = 3840;
|
| 549 |
+
let gain = 0;
|
| 550 |
+
const description = track.info.decoderConfig?.description;
|
| 551 |
+
if (description) {
|
| 552 |
+
if (description.byteLength < 18) {
|
| 553 |
+
throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long.");
|
| 554 |
+
}
|
| 555 |
+
const view2 = ArrayBuffer.isView(description) ? new DataView(description.buffer, description.byteOffset, description.byteLength) : new DataView(description);
|
| 556 |
+
preskip = view2.getUint16(10, true);
|
| 557 |
+
gain = view2.getInt16(14, true);
|
| 558 |
+
}
|
| 559 |
+
return box("dOps", [
|
| 560 |
+
u8(0),
|
| 561 |
+
// Version
|
| 562 |
+
u8(track.info.numberOfChannels),
|
| 563 |
+
// OutputChannelCount
|
| 564 |
+
u16(preskip),
|
| 565 |
+
u32(track.info.sampleRate),
|
| 566 |
+
// InputSampleRate
|
| 567 |
+
fixed_8_8(gain),
|
| 568 |
+
// OutputGain
|
| 569 |
+
u8(0)
|
| 570 |
+
// ChannelMappingFamily
|
| 571 |
+
]);
|
| 572 |
+
};
|
| 573 |
+
var stts = (track) => {
|
| 574 |
+
return fullBox("stts", 0, 0, [
|
| 575 |
+
u32(track.timeToSampleTable.length),
|
| 576 |
+
// Number of entries
|
| 577 |
+
track.timeToSampleTable.map((x) => [
|
| 578 |
+
// Time-to-sample table
|
| 579 |
+
u32(x.sampleCount),
|
| 580 |
+
// Sample count
|
| 581 |
+
u32(x.sampleDelta)
|
| 582 |
+
// Sample duration
|
| 583 |
+
])
|
| 584 |
+
]);
|
| 585 |
+
};
|
| 586 |
+
var stss = (track) => {
|
| 587 |
+
if (track.samples.every((x) => x.type === "key"))
|
| 588 |
+
return null;
|
| 589 |
+
let keySamples = [...track.samples.entries()].filter(([, sample]) => sample.type === "key");
|
| 590 |
+
return fullBox("stss", 0, 0, [
|
| 591 |
+
u32(keySamples.length),
|
| 592 |
+
// Number of entries
|
| 593 |
+
keySamples.map(([index]) => u32(index + 1))
|
| 594 |
+
// Sync sample table
|
| 595 |
+
]);
|
| 596 |
+
};
|
| 597 |
+
var stsc = (track) => {
|
| 598 |
+
return fullBox("stsc", 0, 0, [
|
| 599 |
+
u32(track.compactlyCodedChunkTable.length),
|
| 600 |
+
// Number of entries
|
| 601 |
+
track.compactlyCodedChunkTable.map((x) => [
|
| 602 |
+
// Sample-to-chunk table
|
| 603 |
+
u32(x.firstChunk),
|
| 604 |
+
// First chunk
|
| 605 |
+
u32(x.samplesPerChunk),
|
| 606 |
+
// Samples per chunk
|
| 607 |
+
u32(1)
|
| 608 |
+
// Sample description index
|
| 609 |
+
])
|
| 610 |
+
]);
|
| 611 |
+
};
|
| 612 |
+
var stsz = (track) => fullBox("stsz", 0, 0, [
|
| 613 |
+
u32(0),
|
| 614 |
+
// Sample size (0 means non-constant size)
|
| 615 |
+
u32(track.samples.length),
|
| 616 |
+
// Number of entries
|
| 617 |
+
track.samples.map((x) => u32(x.size))
|
| 618 |
+
// Sample size table
|
| 619 |
+
]);
|
| 620 |
+
var stco = (track) => {
|
| 621 |
+
if (track.finalizedChunks.length > 0 && last(track.finalizedChunks).offset >= 2 ** 32) {
|
| 622 |
+
return fullBox("co64", 0, 0, [
|
| 623 |
+
u32(track.finalizedChunks.length),
|
| 624 |
+
// Number of entries
|
| 625 |
+
track.finalizedChunks.map((x) => u64(x.offset))
|
| 626 |
+
// Chunk offset table
|
| 627 |
+
]);
|
| 628 |
+
}
|
| 629 |
+
return fullBox("stco", 0, 0, [
|
| 630 |
+
u32(track.finalizedChunks.length),
|
| 631 |
+
// Number of entries
|
| 632 |
+
track.finalizedChunks.map((x) => u32(x.offset))
|
| 633 |
+
// Chunk offset table
|
| 634 |
+
]);
|
| 635 |
+
};
|
| 636 |
+
var ctts = (track) => {
|
| 637 |
+
return fullBox("ctts", 0, 0, [
|
| 638 |
+
u32(track.compositionTimeOffsetTable.length),
|
| 639 |
+
// Number of entries
|
| 640 |
+
track.compositionTimeOffsetTable.map((x) => [
|
| 641 |
+
// Time-to-sample table
|
| 642 |
+
u32(x.sampleCount),
|
| 643 |
+
// Sample count
|
| 644 |
+
u32(x.sampleCompositionTimeOffset)
|
| 645 |
+
// Sample offset
|
| 646 |
+
])
|
| 647 |
+
]);
|
| 648 |
+
};
|
| 649 |
+
var mvex = (tracks) => {
|
| 650 |
+
return box("mvex", null, tracks.map(trex));
|
| 651 |
+
};
|
| 652 |
+
var trex = (track) => {
|
| 653 |
+
return fullBox("trex", 0, 0, [
|
| 654 |
+
u32(track.id),
|
| 655 |
+
// Track ID
|
| 656 |
+
u32(1),
|
| 657 |
+
// Default sample description index
|
| 658 |
+
u32(0),
|
| 659 |
+
// Default sample duration
|
| 660 |
+
u32(0),
|
| 661 |
+
// Default sample size
|
| 662 |
+
u32(0)
|
| 663 |
+
// Default sample flags
|
| 664 |
+
]);
|
| 665 |
+
};
|
| 666 |
+
var moof = (sequenceNumber, tracks) => {
|
| 667 |
+
return box("moof", null, [
|
| 668 |
+
mfhd(sequenceNumber),
|
| 669 |
+
...tracks.map(traf)
|
| 670 |
+
]);
|
| 671 |
+
};
|
| 672 |
+
var mfhd = (sequenceNumber) => {
|
| 673 |
+
return fullBox("mfhd", 0, 0, [
|
| 674 |
+
u32(sequenceNumber)
|
| 675 |
+
// Sequence number
|
| 676 |
+
]);
|
| 677 |
+
};
|
| 678 |
+
var fragmentSampleFlags = (sample) => {
|
| 679 |
+
let byte1 = 0;
|
| 680 |
+
let byte2 = 0;
|
| 681 |
+
let byte3 = 0;
|
| 682 |
+
let byte4 = 0;
|
| 683 |
+
let sampleIsDifferenceSample = sample.type === "delta";
|
| 684 |
+
byte2 |= +sampleIsDifferenceSample;
|
| 685 |
+
if (sampleIsDifferenceSample) {
|
| 686 |
+
byte1 |= 1;
|
| 687 |
+
} else {
|
| 688 |
+
byte1 |= 2;
|
| 689 |
+
}
|
| 690 |
+
return byte1 << 24 | byte2 << 16 | byte3 << 8 | byte4;
|
| 691 |
+
};
|
| 692 |
+
var traf = (track) => {
|
| 693 |
+
return box("traf", null, [
|
| 694 |
+
tfhd(track),
|
| 695 |
+
tfdt(track),
|
| 696 |
+
trun(track)
|
| 697 |
+
]);
|
| 698 |
+
};
|
| 699 |
+
var tfhd = (track) => {
|
| 700 |
+
let tfFlags = 0;
|
| 701 |
+
tfFlags |= 8;
|
| 702 |
+
tfFlags |= 16;
|
| 703 |
+
tfFlags |= 32;
|
| 704 |
+
tfFlags |= 131072;
|
| 705 |
+
let referenceSample = track.currentChunk.samples[1] ?? track.currentChunk.samples[0];
|
| 706 |
+
let referenceSampleInfo = {
|
| 707 |
+
duration: referenceSample.timescaleUnitsToNextSample,
|
| 708 |
+
size: referenceSample.size,
|
| 709 |
+
flags: fragmentSampleFlags(referenceSample)
|
| 710 |
+
};
|
| 711 |
+
return fullBox("tfhd", 0, tfFlags, [
|
| 712 |
+
u32(track.id),
|
| 713 |
+
// Track ID
|
| 714 |
+
u32(referenceSampleInfo.duration),
|
| 715 |
+
// Default sample duration
|
| 716 |
+
u32(referenceSampleInfo.size),
|
| 717 |
+
// Default sample size
|
| 718 |
+
u32(referenceSampleInfo.flags)
|
| 719 |
+
// Default sample flags
|
| 720 |
+
]);
|
| 721 |
+
};
|
| 722 |
+
var tfdt = (track) => {
|
| 723 |
+
return fullBox("tfdt", 1, 0, [
|
| 724 |
+
u64(intoTimescale(track.currentChunk.startTimestamp, track.timescale))
|
| 725 |
+
// Base Media Decode Time
|
| 726 |
+
]);
|
| 727 |
+
};
|
| 728 |
+
var trun = (track) => {
|
| 729 |
+
let allSampleDurations = track.currentChunk.samples.map((x) => x.timescaleUnitsToNextSample);
|
| 730 |
+
let allSampleSizes = track.currentChunk.samples.map((x) => x.size);
|
| 731 |
+
let allSampleFlags = track.currentChunk.samples.map(fragmentSampleFlags);
|
| 732 |
+
let allSampleCompositionTimeOffsets = track.currentChunk.samples.map((x) => intoTimescale(x.presentationTimestamp - x.decodeTimestamp, track.timescale));
|
| 733 |
+
let uniqueSampleDurations = new Set(allSampleDurations);
|
| 734 |
+
let uniqueSampleSizes = new Set(allSampleSizes);
|
| 735 |
+
let uniqueSampleFlags = new Set(allSampleFlags);
|
| 736 |
+
let uniqueSampleCompositionTimeOffsets = new Set(allSampleCompositionTimeOffsets);
|
| 737 |
+
let firstSampleFlagsPresent = uniqueSampleFlags.size === 2 && allSampleFlags[0] !== allSampleFlags[1];
|
| 738 |
+
let sampleDurationPresent = uniqueSampleDurations.size > 1;
|
| 739 |
+
let sampleSizePresent = uniqueSampleSizes.size > 1;
|
| 740 |
+
let sampleFlagsPresent = !firstSampleFlagsPresent && uniqueSampleFlags.size > 1;
|
| 741 |
+
let sampleCompositionTimeOffsetsPresent = uniqueSampleCompositionTimeOffsets.size > 1 || [...uniqueSampleCompositionTimeOffsets].some((x) => x !== 0);
|
| 742 |
+
let flags = 0;
|
| 743 |
+
flags |= 1;
|
| 744 |
+
flags |= 4 * +firstSampleFlagsPresent;
|
| 745 |
+
flags |= 256 * +sampleDurationPresent;
|
| 746 |
+
flags |= 512 * +sampleSizePresent;
|
| 747 |
+
flags |= 1024 * +sampleFlagsPresent;
|
| 748 |
+
flags |= 2048 * +sampleCompositionTimeOffsetsPresent;
|
| 749 |
+
return fullBox("trun", 1, flags, [
|
| 750 |
+
u32(track.currentChunk.samples.length),
|
| 751 |
+
// Sample count
|
| 752 |
+
u32(track.currentChunk.offset - track.currentChunk.moofOffset || 0),
|
| 753 |
+
// Data offset
|
| 754 |
+
firstSampleFlagsPresent ? u32(allSampleFlags[0]) : [],
|
| 755 |
+
track.currentChunk.samples.map((_, i) => [
|
| 756 |
+
sampleDurationPresent ? u32(allSampleDurations[i]) : [],
|
| 757 |
+
// Sample duration
|
| 758 |
+
sampleSizePresent ? u32(allSampleSizes[i]) : [],
|
| 759 |
+
// Sample size
|
| 760 |
+
sampleFlagsPresent ? u32(allSampleFlags[i]) : [],
|
| 761 |
+
// Sample flags
|
| 762 |
+
// Sample composition time offsets
|
| 763 |
+
sampleCompositionTimeOffsetsPresent ? i32(allSampleCompositionTimeOffsets[i]) : []
|
| 764 |
+
])
|
| 765 |
+
]);
|
| 766 |
+
};
|
| 767 |
+
var mfra = (tracks) => {
|
| 768 |
+
return box("mfra", null, [
|
| 769 |
+
...tracks.map(tfra),
|
| 770 |
+
mfro()
|
| 771 |
+
]);
|
| 772 |
+
};
|
| 773 |
+
var tfra = (track, trackIndex) => {
|
| 774 |
+
let version = 1;
|
| 775 |
+
return fullBox("tfra", version, 0, [
|
| 776 |
+
u32(track.id),
|
| 777 |
+
// Track ID
|
| 778 |
+
u32(63),
|
| 779 |
+
// This specifies that traf number, trun number and sample number are 32-bit ints
|
| 780 |
+
u32(track.finalizedChunks.length),
|
| 781 |
+
// Number of entries
|
| 782 |
+
track.finalizedChunks.map((chunk) => [
|
| 783 |
+
u64(intoTimescale(chunk.startTimestamp, track.timescale)),
|
| 784 |
+
// Time
|
| 785 |
+
u64(chunk.moofOffset),
|
| 786 |
+
// moof offset
|
| 787 |
+
u32(trackIndex + 1),
|
| 788 |
+
// traf number
|
| 789 |
+
u32(1),
|
| 790 |
+
// trun number
|
| 791 |
+
u32(1)
|
| 792 |
+
// Sample number
|
| 793 |
+
])
|
| 794 |
+
]);
|
| 795 |
+
};
|
| 796 |
+
var mfro = () => {
|
| 797 |
+
return fullBox("mfro", 0, 0, [
|
| 798 |
+
// This value needs to be overwritten manually from the outside, where the actual size of the enclosing mfra box
|
| 799 |
+
// is known
|
| 800 |
+
u32(0)
|
| 801 |
+
// Size
|
| 802 |
+
]);
|
| 803 |
+
};
|
| 804 |
+
var VIDEO_CODEC_TO_BOX_NAME = {
|
| 805 |
+
"avc": "avc1",
|
| 806 |
+
"hevc": "hvc1",
|
| 807 |
+
"vp9": "vp09",
|
| 808 |
+
"av1": "av01"
|
| 809 |
+
};
|
| 810 |
+
var VIDEO_CODEC_TO_CONFIGURATION_BOX = {
|
| 811 |
+
"avc": avcC,
|
| 812 |
+
"hevc": hvcC,
|
| 813 |
+
"vp9": vpcC,
|
| 814 |
+
"av1": av1C
|
| 815 |
+
};
|
| 816 |
+
var AUDIO_CODEC_TO_BOX_NAME = {
|
| 817 |
+
"aac": "mp4a",
|
| 818 |
+
"opus": "Opus"
|
| 819 |
+
};
|
| 820 |
+
var AUDIO_CODEC_TO_CONFIGURATION_BOX = {
|
| 821 |
+
"aac": esds,
|
| 822 |
+
"opus": dOps
|
| 823 |
+
};
|
| 824 |
+
|
| 825 |
+
// src/target.ts
|
| 826 |
+
var isTarget = Symbol("isTarget");
|
| 827 |
+
var Target = class {
|
| 828 |
+
};
|
| 829 |
+
isTarget;
|
| 830 |
+
var ArrayBufferTarget = class extends Target {
|
| 831 |
+
constructor() {
|
| 832 |
+
super(...arguments);
|
| 833 |
+
this.buffer = null;
|
| 834 |
+
}
|
| 835 |
+
};
|
| 836 |
+
var StreamTarget = class extends Target {
|
| 837 |
+
constructor(options) {
|
| 838 |
+
super();
|
| 839 |
+
this.options = options;
|
| 840 |
+
if (typeof options !== "object") {
|
| 841 |
+
throw new TypeError("StreamTarget requires an options object to be passed to its constructor.");
|
| 842 |
+
}
|
| 843 |
+
if (options.onData) {
|
| 844 |
+
if (typeof options.onData !== "function") {
|
| 845 |
+
throw new TypeError("options.onData, when provided, must be a function.");
|
| 846 |
+
}
|
| 847 |
+
if (options.onData.length < 2) {
|
| 848 |
+
throw new TypeError(
|
| 849 |
+
"options.onData, when provided, must be a function that takes in at least two arguments (data and position). Ignoring the position argument, which specifies the byte offset at which the data is to be written, can lead to broken outputs."
|
| 850 |
+
);
|
| 851 |
+
}
|
| 852 |
+
}
|
| 853 |
+
if (options.chunked !== void 0 && typeof options.chunked !== "boolean") {
|
| 854 |
+
throw new TypeError("options.chunked, when provided, must be a boolean.");
|
| 855 |
+
}
|
| 856 |
+
if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize < 1024)) {
|
| 857 |
+
throw new TypeError("options.chunkSize, when provided, must be an integer and not smaller than 1024.");
|
| 858 |
+
}
|
| 859 |
+
}
|
| 860 |
+
};
|
| 861 |
+
var FileSystemWritableFileStreamTarget = class extends Target {
|
| 862 |
+
constructor(stream, options) {
|
| 863 |
+
super();
|
| 864 |
+
this.stream = stream;
|
| 865 |
+
this.options = options;
|
| 866 |
+
if (!(stream instanceof FileSystemWritableFileStream)) {
|
| 867 |
+
throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance.");
|
| 868 |
+
}
|
| 869 |
+
if (options !== void 0 && typeof options !== "object") {
|
| 870 |
+
throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object.");
|
| 871 |
+
}
|
| 872 |
+
if (options) {
|
| 873 |
+
if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize <= 0)) {
|
| 874 |
+
throw new TypeError("options.chunkSize, when provided, must be a positive integer");
|
| 875 |
+
}
|
| 876 |
+
}
|
| 877 |
+
}
|
| 878 |
+
};
|
| 879 |
+
|
| 880 |
+
// src/writer.ts
|
| 881 |
+
var _helper, _helperView;
|
| 882 |
+
var Writer = class {
|
| 883 |
+
constructor() {
|
| 884 |
+
this.pos = 0;
|
| 885 |
+
__privateAdd(this, _helper, new Uint8Array(8));
|
| 886 |
+
__privateAdd(this, _helperView, new DataView(__privateGet(this, _helper).buffer));
|
| 887 |
+
/**
|
| 888 |
+
* Stores the position from the start of the file to where boxes elements have been written. This is used to
|
| 889 |
+
* rewrite/edit elements that were already added before, and to measure sizes of things.
|
| 890 |
+
*/
|
| 891 |
+
this.offsets = /* @__PURE__ */ new WeakMap();
|
| 892 |
+
}
|
| 893 |
+
/** Sets the current position for future writes to a new one. */
|
| 894 |
+
seek(newPos) {
|
| 895 |
+
this.pos = newPos;
|
| 896 |
+
}
|
| 897 |
+
writeU32(value) {
|
| 898 |
+
__privateGet(this, _helperView).setUint32(0, value, false);
|
| 899 |
+
this.write(__privateGet(this, _helper).subarray(0, 4));
|
| 900 |
+
}
|
| 901 |
+
writeU64(value) {
|
| 902 |
+
__privateGet(this, _helperView).setUint32(0, Math.floor(value / 2 ** 32), false);
|
| 903 |
+
__privateGet(this, _helperView).setUint32(4, value, false);
|
| 904 |
+
this.write(__privateGet(this, _helper).subarray(0, 8));
|
| 905 |
+
}
|
| 906 |
+
writeAscii(text) {
|
| 907 |
+
for (let i = 0; i < text.length; i++) {
|
| 908 |
+
__privateGet(this, _helperView).setUint8(i % 8, text.charCodeAt(i));
|
| 909 |
+
if (i % 8 === 7)
|
| 910 |
+
this.write(__privateGet(this, _helper));
|
| 911 |
+
}
|
| 912 |
+
if (text.length % 8 !== 0) {
|
| 913 |
+
this.write(__privateGet(this, _helper).subarray(0, text.length % 8));
|
| 914 |
+
}
|
| 915 |
+
}
|
| 916 |
+
writeBox(box2) {
|
| 917 |
+
this.offsets.set(box2, this.pos);
|
| 918 |
+
if (box2.contents && !box2.children) {
|
| 919 |
+
this.writeBoxHeader(box2, box2.size ?? box2.contents.byteLength + 8);
|
| 920 |
+
this.write(box2.contents);
|
| 921 |
+
} else {
|
| 922 |
+
let startPos = this.pos;
|
| 923 |
+
this.writeBoxHeader(box2, 0);
|
| 924 |
+
if (box2.contents)
|
| 925 |
+
this.write(box2.contents);
|
| 926 |
+
if (box2.children) {
|
| 927 |
+
for (let child of box2.children)
|
| 928 |
+
if (child)
|
| 929 |
+
this.writeBox(child);
|
| 930 |
+
}
|
| 931 |
+
let endPos = this.pos;
|
| 932 |
+
let size = box2.size ?? endPos - startPos;
|
| 933 |
+
this.seek(startPos);
|
| 934 |
+
this.writeBoxHeader(box2, size);
|
| 935 |
+
this.seek(endPos);
|
| 936 |
+
}
|
| 937 |
+
}
|
| 938 |
+
writeBoxHeader(box2, size) {
|
| 939 |
+
this.writeU32(box2.largeSize ? 1 : size);
|
| 940 |
+
this.writeAscii(box2.type);
|
| 941 |
+
if (box2.largeSize)
|
| 942 |
+
this.writeU64(size);
|
| 943 |
+
}
|
| 944 |
+
measureBoxHeader(box2) {
|
| 945 |
+
return 8 + (box2.largeSize ? 8 : 0);
|
| 946 |
+
}
|
| 947 |
+
patchBox(box2) {
|
| 948 |
+
let endPos = this.pos;
|
| 949 |
+
this.seek(this.offsets.get(box2));
|
| 950 |
+
this.writeBox(box2);
|
| 951 |
+
this.seek(endPos);
|
| 952 |
+
}
|
| 953 |
+
measureBox(box2) {
|
| 954 |
+
if (box2.contents && !box2.children) {
|
| 955 |
+
let headerSize = this.measureBoxHeader(box2);
|
| 956 |
+
return headerSize + box2.contents.byteLength;
|
| 957 |
+
} else {
|
| 958 |
+
let result = this.measureBoxHeader(box2);
|
| 959 |
+
if (box2.contents)
|
| 960 |
+
result += box2.contents.byteLength;
|
| 961 |
+
if (box2.children) {
|
| 962 |
+
for (let child of box2.children)
|
| 963 |
+
if (child)
|
| 964 |
+
result += this.measureBox(child);
|
| 965 |
+
}
|
| 966 |
+
return result;
|
| 967 |
+
}
|
| 968 |
+
}
|
| 969 |
+
};
|
| 970 |
+
_helper = new WeakMap();
|
| 971 |
+
_helperView = new WeakMap();
|
| 972 |
+
var _target, _buffer, _bytes, _maxPos, _ensureSize, ensureSize_fn;
|
| 973 |
+
var ArrayBufferTargetWriter = class extends Writer {
|
| 974 |
+
constructor(target) {
|
| 975 |
+
super();
|
| 976 |
+
__privateAdd(this, _ensureSize);
|
| 977 |
+
__privateAdd(this, _target, void 0);
|
| 978 |
+
__privateAdd(this, _buffer, new ArrayBuffer(2 ** 16));
|
| 979 |
+
__privateAdd(this, _bytes, new Uint8Array(__privateGet(this, _buffer)));
|
| 980 |
+
__privateAdd(this, _maxPos, 0);
|
| 981 |
+
__privateSet(this, _target, target);
|
| 982 |
+
}
|
| 983 |
+
write(data) {
|
| 984 |
+
__privateMethod(this, _ensureSize, ensureSize_fn).call(this, this.pos + data.byteLength);
|
| 985 |
+
__privateGet(this, _bytes).set(data, this.pos);
|
| 986 |
+
this.pos += data.byteLength;
|
| 987 |
+
__privateSet(this, _maxPos, Math.max(__privateGet(this, _maxPos), this.pos));
|
| 988 |
+
}
|
| 989 |
+
finalize() {
|
| 990 |
+
__privateMethod(this, _ensureSize, ensureSize_fn).call(this, this.pos);
|
| 991 |
+
__privateGet(this, _target).buffer = __privateGet(this, _buffer).slice(0, Math.max(__privateGet(this, _maxPos), this.pos));
|
| 992 |
+
}
|
| 993 |
+
};
|
| 994 |
+
_target = new WeakMap();
|
| 995 |
+
_buffer = new WeakMap();
|
| 996 |
+
_bytes = new WeakMap();
|
| 997 |
+
_maxPos = new WeakMap();
|
| 998 |
+
_ensureSize = new WeakSet();
|
| 999 |
+
ensureSize_fn = function(size) {
|
| 1000 |
+
let newLength = __privateGet(this, _buffer).byteLength;
|
| 1001 |
+
while (newLength < size)
|
| 1002 |
+
newLength *= 2;
|
| 1003 |
+
if (newLength === __privateGet(this, _buffer).byteLength)
|
| 1004 |
+
return;
|
| 1005 |
+
let newBuffer = new ArrayBuffer(newLength);
|
| 1006 |
+
let newBytes = new Uint8Array(newBuffer);
|
| 1007 |
+
newBytes.set(__privateGet(this, _bytes), 0);
|
| 1008 |
+
__privateSet(this, _buffer, newBuffer);
|
| 1009 |
+
__privateSet(this, _bytes, newBytes);
|
| 1010 |
+
};
|
| 1011 |
+
var DEFAULT_CHUNK_SIZE = 2 ** 24;
|
| 1012 |
+
var MAX_CHUNKS_AT_ONCE = 2;
|
| 1013 |
+
var _target2, _sections, _chunked, _chunkSize, _chunks, _writeDataIntoChunks, writeDataIntoChunks_fn, _insertSectionIntoChunk, insertSectionIntoChunk_fn, _createChunk, createChunk_fn, _flushChunks, flushChunks_fn;
|
| 1014 |
+
var StreamTargetWriter = class extends Writer {
|
| 1015 |
+
constructor(target) {
|
| 1016 |
+
super();
|
| 1017 |
+
__privateAdd(this, _writeDataIntoChunks);
|
| 1018 |
+
__privateAdd(this, _insertSectionIntoChunk);
|
| 1019 |
+
__privateAdd(this, _createChunk);
|
| 1020 |
+
__privateAdd(this, _flushChunks);
|
| 1021 |
+
__privateAdd(this, _target2, void 0);
|
| 1022 |
+
__privateAdd(this, _sections, []);
|
| 1023 |
+
__privateAdd(this, _chunked, void 0);
|
| 1024 |
+
__privateAdd(this, _chunkSize, void 0);
|
| 1025 |
+
/**
|
| 1026 |
+
* The data is divided up into fixed-size chunks, whose contents are first filled in RAM and then flushed out.
|
| 1027 |
+
* A chunk is flushed if all of its contents have been written.
|
| 1028 |
+
*/
|
| 1029 |
+
__privateAdd(this, _chunks, []);
|
| 1030 |
+
__privateSet(this, _target2, target);
|
| 1031 |
+
__privateSet(this, _chunked, target.options?.chunked ?? false);
|
| 1032 |
+
__privateSet(this, _chunkSize, target.options?.chunkSize ?? DEFAULT_CHUNK_SIZE);
|
| 1033 |
+
}
|
| 1034 |
+
write(data) {
|
| 1035 |
+
__privateGet(this, _sections).push({
|
| 1036 |
+
data: data.slice(),
|
| 1037 |
+
start: this.pos
|
| 1038 |
+
});
|
| 1039 |
+
this.pos += data.byteLength;
|
| 1040 |
+
}
|
| 1041 |
+
flush() {
|
| 1042 |
+
if (__privateGet(this, _sections).length === 0)
|
| 1043 |
+
return;
|
| 1044 |
+
let chunks = [];
|
| 1045 |
+
let sorted = [...__privateGet(this, _sections)].sort((a, b) => a.start - b.start);
|
| 1046 |
+
chunks.push({
|
| 1047 |
+
start: sorted[0].start,
|
| 1048 |
+
size: sorted[0].data.byteLength
|
| 1049 |
+
});
|
| 1050 |
+
for (let i = 1; i < sorted.length; i++) {
|
| 1051 |
+
let lastChunk = chunks[chunks.length - 1];
|
| 1052 |
+
let section = sorted[i];
|
| 1053 |
+
if (section.start <= lastChunk.start + lastChunk.size) {
|
| 1054 |
+
lastChunk.size = Math.max(lastChunk.size, section.start + section.data.byteLength - lastChunk.start);
|
| 1055 |
+
} else {
|
| 1056 |
+
chunks.push({
|
| 1057 |
+
start: section.start,
|
| 1058 |
+
size: section.data.byteLength
|
| 1059 |
+
});
|
| 1060 |
+
}
|
| 1061 |
+
}
|
| 1062 |
+
for (let chunk of chunks) {
|
| 1063 |
+
chunk.data = new Uint8Array(chunk.size);
|
| 1064 |
+
for (let section of __privateGet(this, _sections)) {
|
| 1065 |
+
if (chunk.start <= section.start && section.start < chunk.start + chunk.size) {
|
| 1066 |
+
chunk.data.set(section.data, section.start - chunk.start);
|
| 1067 |
+
}
|
| 1068 |
+
}
|
| 1069 |
+
if (__privateGet(this, _chunked)) {
|
| 1070 |
+
__privateMethod(this, _writeDataIntoChunks, writeDataIntoChunks_fn).call(this, chunk.data, chunk.start);
|
| 1071 |
+
__privateMethod(this, _flushChunks, flushChunks_fn).call(this);
|
| 1072 |
+
} else {
|
| 1073 |
+
__privateGet(this, _target2).options.onData?.(chunk.data, chunk.start);
|
| 1074 |
+
}
|
| 1075 |
+
}
|
| 1076 |
+
__privateGet(this, _sections).length = 0;
|
| 1077 |
+
}
|
| 1078 |
+
finalize() {
|
| 1079 |
+
if (__privateGet(this, _chunked)) {
|
| 1080 |
+
__privateMethod(this, _flushChunks, flushChunks_fn).call(this, true);
|
| 1081 |
+
}
|
| 1082 |
+
}
|
| 1083 |
+
};
|
| 1084 |
+
_target2 = new WeakMap();
|
| 1085 |
+
_sections = new WeakMap();
|
| 1086 |
+
_chunked = new WeakMap();
|
| 1087 |
+
_chunkSize = new WeakMap();
|
| 1088 |
+
_chunks = new WeakMap();
|
| 1089 |
+
_writeDataIntoChunks = new WeakSet();
|
| 1090 |
+
writeDataIntoChunks_fn = function(data, position) {
|
| 1091 |
+
let chunkIndex = __privateGet(this, _chunks).findIndex((x) => x.start <= position && position < x.start + __privateGet(this, _chunkSize));
|
| 1092 |
+
if (chunkIndex === -1)
|
| 1093 |
+
chunkIndex = __privateMethod(this, _createChunk, createChunk_fn).call(this, position);
|
| 1094 |
+
let chunk = __privateGet(this, _chunks)[chunkIndex];
|
| 1095 |
+
let relativePosition = position - chunk.start;
|
| 1096 |
+
let toWrite = data.subarray(0, Math.min(__privateGet(this, _chunkSize) - relativePosition, data.byteLength));
|
| 1097 |
+
chunk.data.set(toWrite, relativePosition);
|
| 1098 |
+
let section = {
|
| 1099 |
+
start: relativePosition,
|
| 1100 |
+
end: relativePosition + toWrite.byteLength
|
| 1101 |
+
};
|
| 1102 |
+
__privateMethod(this, _insertSectionIntoChunk, insertSectionIntoChunk_fn).call(this, chunk, section);
|
| 1103 |
+
if (chunk.written[0].start === 0 && chunk.written[0].end === __privateGet(this, _chunkSize)) {
|
| 1104 |
+
chunk.shouldFlush = true;
|
| 1105 |
+
}
|
| 1106 |
+
if (__privateGet(this, _chunks).length > MAX_CHUNKS_AT_ONCE) {
|
| 1107 |
+
for (let i = 0; i < __privateGet(this, _chunks).length - 1; i++) {
|
| 1108 |
+
__privateGet(this, _chunks)[i].shouldFlush = true;
|
| 1109 |
+
}
|
| 1110 |
+
__privateMethod(this, _flushChunks, flushChunks_fn).call(this);
|
| 1111 |
+
}
|
| 1112 |
+
if (toWrite.byteLength < data.byteLength) {
|
| 1113 |
+
__privateMethod(this, _writeDataIntoChunks, writeDataIntoChunks_fn).call(this, data.subarray(toWrite.byteLength), position + toWrite.byteLength);
|
| 1114 |
+
}
|
| 1115 |
+
};
|
| 1116 |
+
_insertSectionIntoChunk = new WeakSet();
|
| 1117 |
+
insertSectionIntoChunk_fn = function(chunk, section) {
|
| 1118 |
+
let low = 0;
|
| 1119 |
+
let high = chunk.written.length - 1;
|
| 1120 |
+
let index = -1;
|
| 1121 |
+
while (low <= high) {
|
| 1122 |
+
let mid = Math.floor(low + (high - low + 1) / 2);
|
| 1123 |
+
if (chunk.written[mid].start <= section.start) {
|
| 1124 |
+
low = mid + 1;
|
| 1125 |
+
index = mid;
|
| 1126 |
+
} else {
|
| 1127 |
+
high = mid - 1;
|
| 1128 |
+
}
|
| 1129 |
+
}
|
| 1130 |
+
chunk.written.splice(index + 1, 0, section);
|
| 1131 |
+
if (index === -1 || chunk.written[index].end < section.start)
|
| 1132 |
+
index++;
|
| 1133 |
+
while (index < chunk.written.length - 1 && chunk.written[index].end >= chunk.written[index + 1].start) {
|
| 1134 |
+
chunk.written[index].end = Math.max(chunk.written[index].end, chunk.written[index + 1].end);
|
| 1135 |
+
chunk.written.splice(index + 1, 1);
|
| 1136 |
+
}
|
| 1137 |
+
};
|
| 1138 |
+
_createChunk = new WeakSet();
|
| 1139 |
+
createChunk_fn = function(includesPosition) {
|
| 1140 |
+
let start = Math.floor(includesPosition / __privateGet(this, _chunkSize)) * __privateGet(this, _chunkSize);
|
| 1141 |
+
let chunk = {
|
| 1142 |
+
start,
|
| 1143 |
+
data: new Uint8Array(__privateGet(this, _chunkSize)),
|
| 1144 |
+
written: [],
|
| 1145 |
+
shouldFlush: false
|
| 1146 |
+
};
|
| 1147 |
+
__privateGet(this, _chunks).push(chunk);
|
| 1148 |
+
__privateGet(this, _chunks).sort((a, b) => a.start - b.start);
|
| 1149 |
+
return __privateGet(this, _chunks).indexOf(chunk);
|
| 1150 |
+
};
|
| 1151 |
+
_flushChunks = new WeakSet();
|
| 1152 |
+
flushChunks_fn = function(force = false) {
|
| 1153 |
+
for (let i = 0; i < __privateGet(this, _chunks).length; i++) {
|
| 1154 |
+
let chunk = __privateGet(this, _chunks)[i];
|
| 1155 |
+
if (!chunk.shouldFlush && !force)
|
| 1156 |
+
continue;
|
| 1157 |
+
for (let section of chunk.written) {
|
| 1158 |
+
__privateGet(this, _target2).options.onData?.(
|
| 1159 |
+
chunk.data.subarray(section.start, section.end),
|
| 1160 |
+
chunk.start + section.start
|
| 1161 |
+
);
|
| 1162 |
+
}
|
| 1163 |
+
__privateGet(this, _chunks).splice(i--, 1);
|
| 1164 |
+
}
|
| 1165 |
+
};
|
| 1166 |
+
var FileSystemWritableFileStreamTargetWriter = class extends StreamTargetWriter {
|
| 1167 |
+
constructor(target) {
|
| 1168 |
+
super(new StreamTarget({
|
| 1169 |
+
onData: (data, position) => target.stream.write({
|
| 1170 |
+
type: "write",
|
| 1171 |
+
data,
|
| 1172 |
+
position
|
| 1173 |
+
}),
|
| 1174 |
+
chunked: true,
|
| 1175 |
+
chunkSize: target.options?.chunkSize
|
| 1176 |
+
}));
|
| 1177 |
+
}
|
| 1178 |
+
};
|
| 1179 |
+
|
| 1180 |
+
// src/muxer.ts
|
| 1181 |
+
var GLOBAL_TIMESCALE = 1e3;
|
| 1182 |
+
var SUPPORTED_VIDEO_CODECS = ["avc", "hevc", "vp9", "av1"];
|
| 1183 |
+
var SUPPORTED_AUDIO_CODECS = ["aac", "opus"];
|
| 1184 |
+
var TIMESTAMP_OFFSET = 2082844800;
|
| 1185 |
+
var FIRST_TIMESTAMP_BEHAVIORS = ["strict", "offset", "cross-track-offset"];
|
| 1186 |
+
var _options, _writer, _ftypSize, _mdat, _videoTrack, _audioTrack, _creationTime, _finalizedChunks, _nextFragmentNumber, _videoSampleQueue, _audioSampleQueue, _finalized, _validateOptions, validateOptions_fn, _writeHeader, writeHeader_fn, _computeMoovSizeUpperBound, computeMoovSizeUpperBound_fn, _prepareTracks, prepareTracks_fn, _generateMpeg4AudioSpecificConfig, generateMpeg4AudioSpecificConfig_fn, _createSampleForTrack, createSampleForTrack_fn, _addSampleToTrack, addSampleToTrack_fn, _validateTimestamp, validateTimestamp_fn, _finalizeCurrentChunk, finalizeCurrentChunk_fn, _finalizeFragment, finalizeFragment_fn, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn, _ensureNotFinalized, ensureNotFinalized_fn;
|
| 1187 |
+
var Muxer = class {
|
| 1188 |
+
constructor(options) {
|
| 1189 |
+
__privateAdd(this, _validateOptions);
|
| 1190 |
+
__privateAdd(this, _writeHeader);
|
| 1191 |
+
__privateAdd(this, _computeMoovSizeUpperBound);
|
| 1192 |
+
__privateAdd(this, _prepareTracks);
|
| 1193 |
+
// https://wiki.multimedia.cx/index.php/MPEG-4_Audio
|
| 1194 |
+
__privateAdd(this, _generateMpeg4AudioSpecificConfig);
|
| 1195 |
+
__privateAdd(this, _createSampleForTrack);
|
| 1196 |
+
__privateAdd(this, _addSampleToTrack);
|
| 1197 |
+
__privateAdd(this, _validateTimestamp);
|
| 1198 |
+
__privateAdd(this, _finalizeCurrentChunk);
|
| 1199 |
+
__privateAdd(this, _finalizeFragment);
|
| 1200 |
+
__privateAdd(this, _maybeFlushStreamingTargetWriter);
|
| 1201 |
+
__privateAdd(this, _ensureNotFinalized);
|
| 1202 |
+
__privateAdd(this, _options, void 0);
|
| 1203 |
+
__privateAdd(this, _writer, void 0);
|
| 1204 |
+
__privateAdd(this, _ftypSize, void 0);
|
| 1205 |
+
__privateAdd(this, _mdat, void 0);
|
| 1206 |
+
__privateAdd(this, _videoTrack, null);
|
| 1207 |
+
__privateAdd(this, _audioTrack, null);
|
| 1208 |
+
__privateAdd(this, _creationTime, Math.floor(Date.now() / 1e3) + TIMESTAMP_OFFSET);
|
| 1209 |
+
__privateAdd(this, _finalizedChunks, []);
|
| 1210 |
+
// Fields for fragmented MP4:
|
| 1211 |
+
__privateAdd(this, _nextFragmentNumber, 1);
|
| 1212 |
+
__privateAdd(this, _videoSampleQueue, []);
|
| 1213 |
+
__privateAdd(this, _audioSampleQueue, []);
|
| 1214 |
+
__privateAdd(this, _finalized, false);
|
| 1215 |
+
__privateMethod(this, _validateOptions, validateOptions_fn).call(this, options);
|
| 1216 |
+
options.video = deepClone(options.video);
|
| 1217 |
+
options.audio = deepClone(options.audio);
|
| 1218 |
+
options.fastStart = deepClone(options.fastStart);
|
| 1219 |
+
this.target = options.target;
|
| 1220 |
+
__privateSet(this, _options, {
|
| 1221 |
+
firstTimestampBehavior: "strict",
|
| 1222 |
+
...options
|
| 1223 |
+
});
|
| 1224 |
+
if (options.target instanceof ArrayBufferTarget) {
|
| 1225 |
+
__privateSet(this, _writer, new ArrayBufferTargetWriter(options.target));
|
| 1226 |
+
} else if (options.target instanceof StreamTarget) {
|
| 1227 |
+
__privateSet(this, _writer, new StreamTargetWriter(options.target));
|
| 1228 |
+
} else if (options.target instanceof FileSystemWritableFileStreamTarget) {
|
| 1229 |
+
__privateSet(this, _writer, new FileSystemWritableFileStreamTargetWriter(options.target));
|
| 1230 |
+
} else {
|
| 1231 |
+
throw new Error(`Invalid target: ${options.target}`);
|
| 1232 |
+
}
|
| 1233 |
+
__privateMethod(this, _prepareTracks, prepareTracks_fn).call(this);
|
| 1234 |
+
__privateMethod(this, _writeHeader, writeHeader_fn).call(this);
|
| 1235 |
+
}
|
| 1236 |
+
addVideoChunk(sample, meta, timestamp, compositionTimeOffset) {
|
| 1237 |
+
if (!(sample instanceof EncodedVideoChunk)) {
|
| 1238 |
+
throw new TypeError("addVideoChunk's first argument (sample) must be of type EncodedVideoChunk.");
|
| 1239 |
+
}
|
| 1240 |
+
if (meta && typeof meta !== "object") {
|
| 1241 |
+
throw new TypeError("addVideoChunk's second argument (meta), when provided, must be an object.");
|
| 1242 |
+
}
|
| 1243 |
+
if (timestamp !== void 0 && (!Number.isFinite(timestamp) || timestamp < 0)) {
|
| 1244 |
+
throw new TypeError(
|
| 1245 |
+
"addVideoChunk's third argument (timestamp), when provided, must be a non-negative real number."
|
| 1246 |
+
);
|
| 1247 |
+
}
|
| 1248 |
+
if (compositionTimeOffset !== void 0 && !Number.isFinite(compositionTimeOffset)) {
|
| 1249 |
+
throw new TypeError(
|
| 1250 |
+
"addVideoChunk's fourth argument (compositionTimeOffset), when provided, must be a real number."
|
| 1251 |
+
);
|
| 1252 |
+
}
|
| 1253 |
+
let data = new Uint8Array(sample.byteLength);
|
| 1254 |
+
sample.copyTo(data);
|
| 1255 |
+
this.addVideoChunkRaw(
|
| 1256 |
+
data,
|
| 1257 |
+
sample.type,
|
| 1258 |
+
timestamp ?? sample.timestamp,
|
| 1259 |
+
sample.duration,
|
| 1260 |
+
meta,
|
| 1261 |
+
compositionTimeOffset
|
| 1262 |
+
);
|
| 1263 |
+
}
|
| 1264 |
+
addVideoChunkRaw(data, type, timestamp, duration, meta, compositionTimeOffset) {
|
| 1265 |
+
if (!(data instanceof Uint8Array)) {
|
| 1266 |
+
throw new TypeError("addVideoChunkRaw's first argument (data) must be an instance of Uint8Array.");
|
| 1267 |
+
}
|
| 1268 |
+
if (type !== "key" && type !== "delta") {
|
| 1269 |
+
throw new TypeError("addVideoChunkRaw's second argument (type) must be either 'key' or 'delta'.");
|
| 1270 |
+
}
|
| 1271 |
+
if (!Number.isFinite(timestamp) || timestamp < 0) {
|
| 1272 |
+
throw new TypeError("addVideoChunkRaw's third argument (timestamp) must be a non-negative real number.");
|
| 1273 |
+
}
|
| 1274 |
+
if (!Number.isFinite(duration) || duration < 0) {
|
| 1275 |
+
throw new TypeError("addVideoChunkRaw's fourth argument (duration) must be a non-negative real number.");
|
| 1276 |
+
}
|
| 1277 |
+
if (meta && typeof meta !== "object") {
|
| 1278 |
+
throw new TypeError("addVideoChunkRaw's fifth argument (meta), when provided, must be an object.");
|
| 1279 |
+
}
|
| 1280 |
+
if (compositionTimeOffset !== void 0 && !Number.isFinite(compositionTimeOffset)) {
|
| 1281 |
+
throw new TypeError(
|
| 1282 |
+
"addVideoChunkRaw's sixth argument (compositionTimeOffset), when provided, must be a real number."
|
| 1283 |
+
);
|
| 1284 |
+
}
|
| 1285 |
+
__privateMethod(this, _ensureNotFinalized, ensureNotFinalized_fn).call(this);
|
| 1286 |
+
if (!__privateGet(this, _options).video)
|
| 1287 |
+
throw new Error("No video track declared.");
|
| 1288 |
+
if (typeof __privateGet(this, _options).fastStart === "object" && __privateGet(this, _videoTrack).samples.length === __privateGet(this, _options).fastStart.expectedVideoChunks) {
|
| 1289 |
+
throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${__privateGet(this, _options).fastStart.expectedVideoChunks}).`);
|
| 1290 |
+
}
|
| 1291 |
+
let videoSample = __privateMethod(this, _createSampleForTrack, createSampleForTrack_fn).call(this, __privateGet(this, _videoTrack), data, type, timestamp, duration, meta, compositionTimeOffset);
|
| 1292 |
+
if (__privateGet(this, _options).fastStart === "fragmented" && __privateGet(this, _audioTrack)) {
|
| 1293 |
+
while (__privateGet(this, _audioSampleQueue).length > 0 && __privateGet(this, _audioSampleQueue)[0].decodeTimestamp <= videoSample.decodeTimestamp) {
|
| 1294 |
+
let audioSample = __privateGet(this, _audioSampleQueue).shift();
|
| 1295 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample);
|
| 1296 |
+
}
|
| 1297 |
+
if (videoSample.decodeTimestamp <= __privateGet(this, _audioTrack).lastDecodeTimestamp) {
|
| 1298 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample);
|
| 1299 |
+
} else {
|
| 1300 |
+
__privateGet(this, _videoSampleQueue).push(videoSample);
|
| 1301 |
+
}
|
| 1302 |
+
} else {
|
| 1303 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample);
|
| 1304 |
+
}
|
| 1305 |
+
}
|
| 1306 |
+
addAudioChunk(sample, meta, timestamp) {
|
| 1307 |
+
if (!(sample instanceof EncodedAudioChunk)) {
|
| 1308 |
+
throw new TypeError("addAudioChunk's first argument (sample) must be of type EncodedAudioChunk.");
|
| 1309 |
+
}
|
| 1310 |
+
if (meta && typeof meta !== "object") {
|
| 1311 |
+
throw new TypeError("addAudioChunk's second argument (meta), when provided, must be an object.");
|
| 1312 |
+
}
|
| 1313 |
+
if (timestamp !== void 0 && (!Number.isFinite(timestamp) || timestamp < 0)) {
|
| 1314 |
+
throw new TypeError(
|
| 1315 |
+
"addAudioChunk's third argument (timestamp), when provided, must be a non-negative real number."
|
| 1316 |
+
);
|
| 1317 |
+
}
|
| 1318 |
+
let data = new Uint8Array(sample.byteLength);
|
| 1319 |
+
sample.copyTo(data);
|
| 1320 |
+
this.addAudioChunkRaw(data, sample.type, timestamp ?? sample.timestamp, sample.duration, meta);
|
| 1321 |
+
}
|
| 1322 |
+
addAudioChunkRaw(data, type, timestamp, duration, meta) {
|
| 1323 |
+
if (!(data instanceof Uint8Array)) {
|
| 1324 |
+
throw new TypeError("addAudioChunkRaw's first argument (data) must be an instance of Uint8Array.");
|
| 1325 |
+
}
|
| 1326 |
+
if (type !== "key" && type !== "delta") {
|
| 1327 |
+
throw new TypeError("addAudioChunkRaw's second argument (type) must be either 'key' or 'delta'.");
|
| 1328 |
+
}
|
| 1329 |
+
if (!Number.isFinite(timestamp) || timestamp < 0) {
|
| 1330 |
+
throw new TypeError("addAudioChunkRaw's third argument (timestamp) must be a non-negative real number.");
|
| 1331 |
+
}
|
| 1332 |
+
if (!Number.isFinite(duration) || duration < 0) {
|
| 1333 |
+
throw new TypeError("addAudioChunkRaw's fourth argument (duration) must be a non-negative real number.");
|
| 1334 |
+
}
|
| 1335 |
+
if (meta && typeof meta !== "object") {
|
| 1336 |
+
throw new TypeError("addAudioChunkRaw's fifth argument (meta), when provided, must be an object.");
|
| 1337 |
+
}
|
| 1338 |
+
__privateMethod(this, _ensureNotFinalized, ensureNotFinalized_fn).call(this);
|
| 1339 |
+
if (!__privateGet(this, _options).audio)
|
| 1340 |
+
throw new Error("No audio track declared.");
|
| 1341 |
+
if (typeof __privateGet(this, _options).fastStart === "object" && __privateGet(this, _audioTrack).samples.length === __privateGet(this, _options).fastStart.expectedAudioChunks) {
|
| 1342 |
+
throw new Error(`Cannot add more audio chunks than specified in 'fastStart' (${__privateGet(this, _options).fastStart.expectedAudioChunks}).`);
|
| 1343 |
+
}
|
| 1344 |
+
let audioSample = __privateMethod(this, _createSampleForTrack, createSampleForTrack_fn).call(this, __privateGet(this, _audioTrack), data, type, timestamp, duration, meta);
|
| 1345 |
+
if (__privateGet(this, _options).fastStart === "fragmented" && __privateGet(this, _videoTrack)) {
|
| 1346 |
+
while (__privateGet(this, _videoSampleQueue).length > 0 && __privateGet(this, _videoSampleQueue)[0].decodeTimestamp <= audioSample.decodeTimestamp) {
|
| 1347 |
+
let videoSample = __privateGet(this, _videoSampleQueue).shift();
|
| 1348 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample);
|
| 1349 |
+
}
|
| 1350 |
+
if (audioSample.decodeTimestamp <= __privateGet(this, _videoTrack).lastDecodeTimestamp) {
|
| 1351 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample);
|
| 1352 |
+
} else {
|
| 1353 |
+
__privateGet(this, _audioSampleQueue).push(audioSample);
|
| 1354 |
+
}
|
| 1355 |
+
} else {
|
| 1356 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample);
|
| 1357 |
+
}
|
| 1358 |
+
}
|
| 1359 |
+
/** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */
|
| 1360 |
+
finalize() {
|
| 1361 |
+
if (__privateGet(this, _finalized)) {
|
| 1362 |
+
throw new Error("Cannot finalize a muxer more than once.");
|
| 1363 |
+
}
|
| 1364 |
+
if (__privateGet(this, _options).fastStart === "fragmented") {
|
| 1365 |
+
for (let videoSample of __privateGet(this, _videoSampleQueue))
|
| 1366 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample);
|
| 1367 |
+
for (let audioSample of __privateGet(this, _audioSampleQueue))
|
| 1368 |
+
__privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample);
|
| 1369 |
+
__privateMethod(this, _finalizeFragment, finalizeFragment_fn).call(this, false);
|
| 1370 |
+
} else {
|
| 1371 |
+
if (__privateGet(this, _videoTrack))
|
| 1372 |
+
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, __privateGet(this, _videoTrack));
|
| 1373 |
+
if (__privateGet(this, _audioTrack))
|
| 1374 |
+
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, __privateGet(this, _audioTrack));
|
| 1375 |
+
}
|
| 1376 |
+
let tracks = [__privateGet(this, _videoTrack), __privateGet(this, _audioTrack)].filter(Boolean);
|
| 1377 |
+
if (__privateGet(this, _options).fastStart === "in-memory") {
|
| 1378 |
+
let mdatSize;
|
| 1379 |
+
for (let i = 0; i < 2; i++) {
|
| 1380 |
+
let movieBox2 = moov(tracks, __privateGet(this, _creationTime));
|
| 1381 |
+
let movieBoxSize = __privateGet(this, _writer).measureBox(movieBox2);
|
| 1382 |
+
mdatSize = __privateGet(this, _writer).measureBox(__privateGet(this, _mdat));
|
| 1383 |
+
let currentChunkPos = __privateGet(this, _writer).pos + movieBoxSize + mdatSize;
|
| 1384 |
+
for (let chunk of __privateGet(this, _finalizedChunks)) {
|
| 1385 |
+
chunk.offset = currentChunkPos;
|
| 1386 |
+
for (let { data } of chunk.samples) {
|
| 1387 |
+
currentChunkPos += data.byteLength;
|
| 1388 |
+
mdatSize += data.byteLength;
|
| 1389 |
+
}
|
| 1390 |
+
}
|
| 1391 |
+
if (currentChunkPos < 2 ** 32)
|
| 1392 |
+
break;
|
| 1393 |
+
if (mdatSize >= 2 ** 32)
|
| 1394 |
+
__privateGet(this, _mdat).largeSize = true;
|
| 1395 |
+
}
|
| 1396 |
+
let movieBox = moov(tracks, __privateGet(this, _creationTime));
|
| 1397 |
+
__privateGet(this, _writer).writeBox(movieBox);
|
| 1398 |
+
__privateGet(this, _mdat).size = mdatSize;
|
| 1399 |
+
__privateGet(this, _writer).writeBox(__privateGet(this, _mdat));
|
| 1400 |
+
for (let chunk of __privateGet(this, _finalizedChunks)) {
|
| 1401 |
+
for (let sample of chunk.samples) {
|
| 1402 |
+
__privateGet(this, _writer).write(sample.data);
|
| 1403 |
+
sample.data = null;
|
| 1404 |
+
}
|
| 1405 |
+
}
|
| 1406 |
+
} else if (__privateGet(this, _options).fastStart === "fragmented") {
|
| 1407 |
+
let startPos = __privateGet(this, _writer).pos;
|
| 1408 |
+
let mfraBox = mfra(tracks);
|
| 1409 |
+
__privateGet(this, _writer).writeBox(mfraBox);
|
| 1410 |
+
let mfraBoxSize = __privateGet(this, _writer).pos - startPos;
|
| 1411 |
+
__privateGet(this, _writer).seek(__privateGet(this, _writer).pos - 4);
|
| 1412 |
+
__privateGet(this, _writer).writeU32(mfraBoxSize);
|
| 1413 |
+
} else {
|
| 1414 |
+
let mdatPos = __privateGet(this, _writer).offsets.get(__privateGet(this, _mdat));
|
| 1415 |
+
let mdatSize = __privateGet(this, _writer).pos - mdatPos;
|
| 1416 |
+
__privateGet(this, _mdat).size = mdatSize;
|
| 1417 |
+
__privateGet(this, _mdat).largeSize = mdatSize >= 2 ** 32;
|
| 1418 |
+
__privateGet(this, _writer).patchBox(__privateGet(this, _mdat));
|
| 1419 |
+
let movieBox = moov(tracks, __privateGet(this, _creationTime));
|
| 1420 |
+
if (typeof __privateGet(this, _options).fastStart === "object") {
|
| 1421 |
+
__privateGet(this, _writer).seek(__privateGet(this, _ftypSize));
|
| 1422 |
+
__privateGet(this, _writer).writeBox(movieBox);
|
| 1423 |
+
let remainingBytes = mdatPos - __privateGet(this, _writer).pos;
|
| 1424 |
+
__privateGet(this, _writer).writeBox(free(remainingBytes));
|
| 1425 |
+
} else {
|
| 1426 |
+
__privateGet(this, _writer).writeBox(movieBox);
|
| 1427 |
+
}
|
| 1428 |
+
}
|
| 1429 |
+
__privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this);
|
| 1430 |
+
__privateGet(this, _writer).finalize();
|
| 1431 |
+
__privateSet(this, _finalized, true);
|
| 1432 |
+
}
|
| 1433 |
+
};
|
| 1434 |
+
_options = new WeakMap();
|
| 1435 |
+
_writer = new WeakMap();
|
| 1436 |
+
_ftypSize = new WeakMap();
|
| 1437 |
+
_mdat = new WeakMap();
|
| 1438 |
+
_videoTrack = new WeakMap();
|
| 1439 |
+
_audioTrack = new WeakMap();
|
| 1440 |
+
_creationTime = new WeakMap();
|
| 1441 |
+
_finalizedChunks = new WeakMap();
|
| 1442 |
+
_nextFragmentNumber = new WeakMap();
|
| 1443 |
+
_videoSampleQueue = new WeakMap();
|
| 1444 |
+
_audioSampleQueue = new WeakMap();
|
| 1445 |
+
_finalized = new WeakMap();
|
| 1446 |
+
_validateOptions = new WeakSet();
|
| 1447 |
+
validateOptions_fn = function(options) {
|
| 1448 |
+
if (typeof options !== "object") {
|
| 1449 |
+
throw new TypeError("The muxer requires an options object to be passed to its constructor.");
|
| 1450 |
+
}
|
| 1451 |
+
if (!(options.target instanceof Target)) {
|
| 1452 |
+
throw new TypeError("The target must be provided and an instance of Target.");
|
| 1453 |
+
}
|
| 1454 |
+
if (options.video) {
|
| 1455 |
+
if (!SUPPORTED_VIDEO_CODECS.includes(options.video.codec)) {
|
| 1456 |
+
throw new TypeError(`Unsupported video codec: ${options.video.codec}`);
|
| 1457 |
+
}
|
| 1458 |
+
if (!Number.isInteger(options.video.width) || options.video.width <= 0) {
|
| 1459 |
+
throw new TypeError(`Invalid video width: ${options.video.width}. Must be a positive integer.`);
|
| 1460 |
+
}
|
| 1461 |
+
if (!Number.isInteger(options.video.height) || options.video.height <= 0) {
|
| 1462 |
+
throw new TypeError(`Invalid video height: ${options.video.height}. Must be a positive integer.`);
|
| 1463 |
+
}
|
| 1464 |
+
const videoRotation = options.video.rotation;
|
| 1465 |
+
if (typeof videoRotation === "number" && ![0, 90, 180, 270].includes(videoRotation)) {
|
| 1466 |
+
throw new TypeError(`Invalid video rotation: ${videoRotation}. Has to be 0, 90, 180 or 270.`);
|
| 1467 |
+
} else if (Array.isArray(videoRotation) && (videoRotation.length !== 9 || videoRotation.some((value) => typeof value !== "number"))) {
|
| 1468 |
+
throw new TypeError(`Invalid video transformation matrix: ${videoRotation.join()}`);
|
| 1469 |
+
}
|
| 1470 |
+
if (options.video.frameRate !== void 0 && (!Number.isInteger(options.video.frameRate) || options.video.frameRate <= 0)) {
|
| 1471 |
+
throw new TypeError(
|
| 1472 |
+
`Invalid video frame rate: ${options.video.frameRate}. Must be a positive integer.`
|
| 1473 |
+
);
|
| 1474 |
+
}
|
| 1475 |
+
}
|
| 1476 |
+
if (options.audio) {
|
| 1477 |
+
if (!SUPPORTED_AUDIO_CODECS.includes(options.audio.codec)) {
|
| 1478 |
+
throw new TypeError(`Unsupported audio codec: ${options.audio.codec}`);
|
| 1479 |
+
}
|
| 1480 |
+
if (!Number.isInteger(options.audio.numberOfChannels) || options.audio.numberOfChannels <= 0) {
|
| 1481 |
+
throw new TypeError(
|
| 1482 |
+
`Invalid number of audio channels: ${options.audio.numberOfChannels}. Must be a positive integer.`
|
| 1483 |
+
);
|
| 1484 |
+
}
|
| 1485 |
+
if (!Number.isInteger(options.audio.sampleRate) || options.audio.sampleRate <= 0) {
|
| 1486 |
+
throw new TypeError(
|
| 1487 |
+
`Invalid audio sample rate: ${options.audio.sampleRate}. Must be a positive integer.`
|
| 1488 |
+
);
|
| 1489 |
+
}
|
| 1490 |
+
}
|
| 1491 |
+
if (options.firstTimestampBehavior && !FIRST_TIMESTAMP_BEHAVIORS.includes(options.firstTimestampBehavior)) {
|
| 1492 |
+
throw new TypeError(`Invalid first timestamp behavior: ${options.firstTimestampBehavior}`);
|
| 1493 |
+
}
|
| 1494 |
+
if (typeof options.fastStart === "object") {
|
| 1495 |
+
if (options.video) {
|
| 1496 |
+
if (options.fastStart.expectedVideoChunks === void 0) {
|
| 1497 |
+
throw new TypeError(`'fastStart' is an object but is missing property 'expectedVideoChunks'.`);
|
| 1498 |
+
} else if (!Number.isInteger(options.fastStart.expectedVideoChunks) || options.fastStart.expectedVideoChunks < 0) {
|
| 1499 |
+
throw new TypeError(`'expectedVideoChunks' must be a non-negative integer.`);
|
| 1500 |
+
}
|
| 1501 |
+
}
|
| 1502 |
+
if (options.audio) {
|
| 1503 |
+
if (options.fastStart.expectedAudioChunks === void 0) {
|
| 1504 |
+
throw new TypeError(`'fastStart' is an object but is missing property 'expectedAudioChunks'.`);
|
| 1505 |
+
} else if (!Number.isInteger(options.fastStart.expectedAudioChunks) || options.fastStart.expectedAudioChunks < 0) {
|
| 1506 |
+
throw new TypeError(`'expectedAudioChunks' must be a non-negative integer.`);
|
| 1507 |
+
}
|
| 1508 |
+
}
|
| 1509 |
+
} else if (![false, "in-memory", "fragmented"].includes(options.fastStart)) {
|
| 1510 |
+
throw new TypeError(`'fastStart' option must be false, 'in-memory', 'fragmented' or an object.`);
|
| 1511 |
+
}
|
| 1512 |
+
if (options.minFragmentDuration !== void 0 && (!Number.isFinite(options.minFragmentDuration) || options.minFragmentDuration < 0)) {
|
| 1513 |
+
throw new TypeError(`'minFragmentDuration' must be a non-negative number.`);
|
| 1514 |
+
}
|
| 1515 |
+
};
|
| 1516 |
+
_writeHeader = new WeakSet();
|
| 1517 |
+
writeHeader_fn = function() {
|
| 1518 |
+
__privateGet(this, _writer).writeBox(ftyp({
|
| 1519 |
+
holdsAvc: __privateGet(this, _options).video?.codec === "avc",
|
| 1520 |
+
fragmented: __privateGet(this, _options).fastStart === "fragmented"
|
| 1521 |
+
}));
|
| 1522 |
+
__privateSet(this, _ftypSize, __privateGet(this, _writer).pos);
|
| 1523 |
+
if (__privateGet(this, _options).fastStart === "in-memory") {
|
| 1524 |
+
__privateSet(this, _mdat, mdat(false));
|
| 1525 |
+
} else if (__privateGet(this, _options).fastStart === "fragmented") {
|
| 1526 |
+
} else {
|
| 1527 |
+
if (typeof __privateGet(this, _options).fastStart === "object") {
|
| 1528 |
+
let moovSizeUpperBound = __privateMethod(this, _computeMoovSizeUpperBound, computeMoovSizeUpperBound_fn).call(this);
|
| 1529 |
+
__privateGet(this, _writer).seek(__privateGet(this, _writer).pos + moovSizeUpperBound);
|
| 1530 |
+
}
|
| 1531 |
+
__privateSet(this, _mdat, mdat(true));
|
| 1532 |
+
__privateGet(this, _writer).writeBox(__privateGet(this, _mdat));
|
| 1533 |
+
}
|
| 1534 |
+
__privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this);
|
| 1535 |
+
};
|
| 1536 |
+
_computeMoovSizeUpperBound = new WeakSet();
|
| 1537 |
+
computeMoovSizeUpperBound_fn = function() {
|
| 1538 |
+
if (typeof __privateGet(this, _options).fastStart !== "object")
|
| 1539 |
+
return;
|
| 1540 |
+
let upperBound = 0;
|
| 1541 |
+
let sampleCounts = [
|
| 1542 |
+
__privateGet(this, _options).fastStart.expectedVideoChunks,
|
| 1543 |
+
__privateGet(this, _options).fastStart.expectedAudioChunks
|
| 1544 |
+
];
|
| 1545 |
+
for (let n of sampleCounts) {
|
| 1546 |
+
if (!n)
|
| 1547 |
+
continue;
|
| 1548 |
+
upperBound += (4 + 4) * Math.ceil(2 / 3 * n);
|
| 1549 |
+
upperBound += 4 * n;
|
| 1550 |
+
upperBound += (4 + 4 + 4) * Math.ceil(2 / 3 * n);
|
| 1551 |
+
upperBound += 4 * n;
|
| 1552 |
+
upperBound += 8 * n;
|
| 1553 |
+
}
|
| 1554 |
+
upperBound += 4096;
|
| 1555 |
+
return upperBound;
|
| 1556 |
+
};
|
| 1557 |
+
_prepareTracks = new WeakSet();
|
| 1558 |
+
prepareTracks_fn = function() {
|
| 1559 |
+
if (__privateGet(this, _options).video) {
|
| 1560 |
+
__privateSet(this, _videoTrack, {
|
| 1561 |
+
id: 1,
|
| 1562 |
+
info: {
|
| 1563 |
+
type: "video",
|
| 1564 |
+
codec: __privateGet(this, _options).video.codec,
|
| 1565 |
+
width: __privateGet(this, _options).video.width,
|
| 1566 |
+
height: __privateGet(this, _options).video.height,
|
| 1567 |
+
rotation: __privateGet(this, _options).video.rotation ?? 0,
|
| 1568 |
+
decoderConfig: null
|
| 1569 |
+
},
|
| 1570 |
+
// The fallback contains many common frame rates as factors
|
| 1571 |
+
timescale: __privateGet(this, _options).video.frameRate ?? 57600,
|
| 1572 |
+
samples: [],
|
| 1573 |
+
finalizedChunks: [],
|
| 1574 |
+
currentChunk: null,
|
| 1575 |
+
firstDecodeTimestamp: void 0,
|
| 1576 |
+
lastDecodeTimestamp: -1,
|
| 1577 |
+
timeToSampleTable: [],
|
| 1578 |
+
compositionTimeOffsetTable: [],
|
| 1579 |
+
lastTimescaleUnits: null,
|
| 1580 |
+
lastSample: null,
|
| 1581 |
+
compactlyCodedChunkTable: []
|
| 1582 |
+
});
|
| 1583 |
+
}
|
| 1584 |
+
if (__privateGet(this, _options).audio) {
|
| 1585 |
+
__privateSet(this, _audioTrack, {
|
| 1586 |
+
id: __privateGet(this, _options).video ? 2 : 1,
|
| 1587 |
+
info: {
|
| 1588 |
+
type: "audio",
|
| 1589 |
+
codec: __privateGet(this, _options).audio.codec,
|
| 1590 |
+
numberOfChannels: __privateGet(this, _options).audio.numberOfChannels,
|
| 1591 |
+
sampleRate: __privateGet(this, _options).audio.sampleRate,
|
| 1592 |
+
decoderConfig: null
|
| 1593 |
+
},
|
| 1594 |
+
timescale: __privateGet(this, _options).audio.sampleRate,
|
| 1595 |
+
samples: [],
|
| 1596 |
+
finalizedChunks: [],
|
| 1597 |
+
currentChunk: null,
|
| 1598 |
+
firstDecodeTimestamp: void 0,
|
| 1599 |
+
lastDecodeTimestamp: -1,
|
| 1600 |
+
timeToSampleTable: [],
|
| 1601 |
+
compositionTimeOffsetTable: [],
|
| 1602 |
+
lastTimescaleUnits: null,
|
| 1603 |
+
lastSample: null,
|
| 1604 |
+
compactlyCodedChunkTable: []
|
| 1605 |
+
});
|
| 1606 |
+
if (__privateGet(this, _options).audio.codec === "aac") {
|
| 1607 |
+
let guessedCodecPrivate = __privateMethod(this, _generateMpeg4AudioSpecificConfig, generateMpeg4AudioSpecificConfig_fn).call(
|
| 1608 |
+
this,
|
| 1609 |
+
2,
|
| 1610 |
+
// Object type for AAC-LC, since it's the most common
|
| 1611 |
+
__privateGet(this, _options).audio.sampleRate,
|
| 1612 |
+
__privateGet(this, _options).audio.numberOfChannels
|
| 1613 |
+
);
|
| 1614 |
+
__privateGet(this, _audioTrack).info.decoderConfig = {
|
| 1615 |
+
codec: __privateGet(this, _options).audio.codec,
|
| 1616 |
+
description: guessedCodecPrivate,
|
| 1617 |
+
numberOfChannels: __privateGet(this, _options).audio.numberOfChannels,
|
| 1618 |
+
sampleRate: __privateGet(this, _options).audio.sampleRate
|
| 1619 |
+
};
|
| 1620 |
+
}
|
| 1621 |
+
}
|
| 1622 |
+
};
|
| 1623 |
+
_generateMpeg4AudioSpecificConfig = new WeakSet();
|
| 1624 |
+
generateMpeg4AudioSpecificConfig_fn = function(objectType, sampleRate, numberOfChannels) {
|
| 1625 |
+
let frequencyIndices = [96e3, 88200, 64e3, 48e3, 44100, 32e3, 24e3, 22050, 16e3, 12e3, 11025, 8e3, 7350];
|
| 1626 |
+
let frequencyIndex = frequencyIndices.indexOf(sampleRate);
|
| 1627 |
+
let channelConfig = numberOfChannels;
|
| 1628 |
+
let configBits = "";
|
| 1629 |
+
configBits += objectType.toString(2).padStart(5, "0");
|
| 1630 |
+
configBits += frequencyIndex.toString(2).padStart(4, "0");
|
| 1631 |
+
if (frequencyIndex === 15)
|
| 1632 |
+
configBits += sampleRate.toString(2).padStart(24, "0");
|
| 1633 |
+
configBits += channelConfig.toString(2).padStart(4, "0");
|
| 1634 |
+
let paddingLength = Math.ceil(configBits.length / 8) * 8;
|
| 1635 |
+
configBits = configBits.padEnd(paddingLength, "0");
|
| 1636 |
+
let configBytes = new Uint8Array(configBits.length / 8);
|
| 1637 |
+
for (let i = 0; i < configBits.length; i += 8) {
|
| 1638 |
+
configBytes[i / 8] = parseInt(configBits.slice(i, i + 8), 2);
|
| 1639 |
+
}
|
| 1640 |
+
return configBytes;
|
| 1641 |
+
};
|
| 1642 |
+
_createSampleForTrack = new WeakSet();
|
| 1643 |
+
createSampleForTrack_fn = function(track, data, type, timestamp, duration, meta, compositionTimeOffset) {
|
| 1644 |
+
let presentationTimestampInSeconds = timestamp / 1e6;
|
| 1645 |
+
let decodeTimestampInSeconds = (timestamp - (compositionTimeOffset ?? 0)) / 1e6;
|
| 1646 |
+
let durationInSeconds = duration / 1e6;
|
| 1647 |
+
let adjusted = __privateMethod(this, _validateTimestamp, validateTimestamp_fn).call(this, presentationTimestampInSeconds, decodeTimestampInSeconds, track);
|
| 1648 |
+
presentationTimestampInSeconds = adjusted.presentationTimestamp;
|
| 1649 |
+
decodeTimestampInSeconds = adjusted.decodeTimestamp;
|
| 1650 |
+
if (meta?.decoderConfig) {
|
| 1651 |
+
if (track.info.decoderConfig === null) {
|
| 1652 |
+
track.info.decoderConfig = meta.decoderConfig;
|
| 1653 |
+
} else {
|
| 1654 |
+
Object.assign(track.info.decoderConfig, meta.decoderConfig);
|
| 1655 |
+
}
|
| 1656 |
+
}
|
| 1657 |
+
let sample = {
|
| 1658 |
+
presentationTimestamp: presentationTimestampInSeconds,
|
| 1659 |
+
decodeTimestamp: decodeTimestampInSeconds,
|
| 1660 |
+
duration: durationInSeconds,
|
| 1661 |
+
data,
|
| 1662 |
+
size: data.byteLength,
|
| 1663 |
+
type,
|
| 1664 |
+
// Will be refined once the next sample comes in
|
| 1665 |
+
timescaleUnitsToNextSample: intoTimescale(durationInSeconds, track.timescale)
|
| 1666 |
+
};
|
| 1667 |
+
return sample;
|
| 1668 |
+
};
|
| 1669 |
+
_addSampleToTrack = new WeakSet();
|
| 1670 |
+
addSampleToTrack_fn = function(track, sample) {
|
| 1671 |
+
if (__privateGet(this, _options).fastStart !== "fragmented") {
|
| 1672 |
+
track.samples.push(sample);
|
| 1673 |
+
}
|
| 1674 |
+
const sampleCompositionTimeOffset = intoTimescale(sample.presentationTimestamp - sample.decodeTimestamp, track.timescale);
|
| 1675 |
+
if (track.lastTimescaleUnits !== null) {
|
| 1676 |
+
let timescaleUnits = intoTimescale(sample.decodeTimestamp, track.timescale, false);
|
| 1677 |
+
let delta = Math.round(timescaleUnits - track.lastTimescaleUnits);
|
| 1678 |
+
track.lastTimescaleUnits += delta;
|
| 1679 |
+
track.lastSample.timescaleUnitsToNextSample = delta;
|
| 1680 |
+
if (__privateGet(this, _options).fastStart !== "fragmented") {
|
| 1681 |
+
let lastTableEntry = last(track.timeToSampleTable);
|
| 1682 |
+
if (lastTableEntry.sampleCount === 1) {
|
| 1683 |
+
lastTableEntry.sampleDelta = delta;
|
| 1684 |
+
lastTableEntry.sampleCount++;
|
| 1685 |
+
} else if (lastTableEntry.sampleDelta === delta) {
|
| 1686 |
+
lastTableEntry.sampleCount++;
|
| 1687 |
+
} else {
|
| 1688 |
+
lastTableEntry.sampleCount--;
|
| 1689 |
+
track.timeToSampleTable.push({
|
| 1690 |
+
sampleCount: 2,
|
| 1691 |
+
sampleDelta: delta
|
| 1692 |
+
});
|
| 1693 |
+
}
|
| 1694 |
+
const lastCompositionTimeOffsetTableEntry = last(track.compositionTimeOffsetTable);
|
| 1695 |
+
if (lastCompositionTimeOffsetTableEntry.sampleCompositionTimeOffset === sampleCompositionTimeOffset) {
|
| 1696 |
+
lastCompositionTimeOffsetTableEntry.sampleCount++;
|
| 1697 |
+
} else {
|
| 1698 |
+
track.compositionTimeOffsetTable.push({
|
| 1699 |
+
sampleCount: 1,
|
| 1700 |
+
sampleCompositionTimeOffset
|
| 1701 |
+
});
|
| 1702 |
+
}
|
| 1703 |
+
}
|
| 1704 |
+
} else {
|
| 1705 |
+
track.lastTimescaleUnits = 0;
|
| 1706 |
+
if (__privateGet(this, _options).fastStart !== "fragmented") {
|
| 1707 |
+
track.timeToSampleTable.push({
|
| 1708 |
+
sampleCount: 1,
|
| 1709 |
+
sampleDelta: intoTimescale(sample.duration, track.timescale)
|
| 1710 |
+
});
|
| 1711 |
+
track.compositionTimeOffsetTable.push({
|
| 1712 |
+
sampleCount: 1,
|
| 1713 |
+
sampleCompositionTimeOffset
|
| 1714 |
+
});
|
| 1715 |
+
}
|
| 1716 |
+
}
|
| 1717 |
+
track.lastSample = sample;
|
| 1718 |
+
let beginNewChunk = false;
|
| 1719 |
+
if (!track.currentChunk) {
|
| 1720 |
+
beginNewChunk = true;
|
| 1721 |
+
} else {
|
| 1722 |
+
let currentChunkDuration = sample.presentationTimestamp - track.currentChunk.startTimestamp;
|
| 1723 |
+
if (__privateGet(this, _options).fastStart === "fragmented") {
|
| 1724 |
+
let mostImportantTrack = __privateGet(this, _videoTrack) ?? __privateGet(this, _audioTrack);
|
| 1725 |
+
const chunkDuration = __privateGet(this, _options).minFragmentDuration ?? 1;
|
| 1726 |
+
if (track === mostImportantTrack && sample.type === "key" && currentChunkDuration >= chunkDuration) {
|
| 1727 |
+
beginNewChunk = true;
|
| 1728 |
+
__privateMethod(this, _finalizeFragment, finalizeFragment_fn).call(this);
|
| 1729 |
+
}
|
| 1730 |
+
} else {
|
| 1731 |
+
beginNewChunk = currentChunkDuration >= 0.5;
|
| 1732 |
+
}
|
| 1733 |
+
}
|
| 1734 |
+
if (beginNewChunk) {
|
| 1735 |
+
if (track.currentChunk) {
|
| 1736 |
+
__privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, track);
|
| 1737 |
+
}
|
| 1738 |
+
track.currentChunk = {
|
| 1739 |
+
startTimestamp: sample.presentationTimestamp,
|
| 1740 |
+
samples: []
|
| 1741 |
+
};
|
| 1742 |
+
}
|
| 1743 |
+
track.currentChunk.samples.push(sample);
|
| 1744 |
+
};
|
| 1745 |
+
_validateTimestamp = new WeakSet();
|
| 1746 |
+
validateTimestamp_fn = function(presentationTimestamp, decodeTimestamp, track) {
|
| 1747 |
+
const strictTimestampBehavior = __privateGet(this, _options).firstTimestampBehavior === "strict";
|
| 1748 |
+
const noLastDecodeTimestamp = track.lastDecodeTimestamp === -1;
|
| 1749 |
+
const timestampNonZero = decodeTimestamp !== 0;
|
| 1750 |
+
if (strictTimestampBehavior && noLastDecodeTimestamp && timestampNonZero) {
|
| 1751 |
+
throw new Error(
|
| 1752 |
+
`The first chunk for your media track must have a timestamp of 0 (received DTS=${decodeTimestamp}).Non-zero first timestamps are often caused by directly piping frames or audio data from a MediaStreamTrack into the encoder. Their timestamps are typically relative to the age of thedocument, which is probably what you want.
|
| 1753 |
+
|
| 1754 |
+
If you want to offset all timestamps of a track such that the first one is zero, set firstTimestampBehavior: 'offset' in the options.
|
| 1755 |
+
`
|
| 1756 |
+
);
|
| 1757 |
+
} else if (__privateGet(this, _options).firstTimestampBehavior === "offset" || __privateGet(this, _options).firstTimestampBehavior === "cross-track-offset") {
|
| 1758 |
+
if (track.firstDecodeTimestamp === void 0) {
|
| 1759 |
+
track.firstDecodeTimestamp = decodeTimestamp;
|
| 1760 |
+
}
|
| 1761 |
+
let baseDecodeTimestamp;
|
| 1762 |
+
if (__privateGet(this, _options).firstTimestampBehavior === "offset") {
|
| 1763 |
+
baseDecodeTimestamp = track.firstDecodeTimestamp;
|
| 1764 |
+
} else {
|
| 1765 |
+
baseDecodeTimestamp = Math.min(
|
| 1766 |
+
__privateGet(this, _videoTrack)?.firstDecodeTimestamp ?? Infinity,
|
| 1767 |
+
__privateGet(this, _audioTrack)?.firstDecodeTimestamp ?? Infinity
|
| 1768 |
+
);
|
| 1769 |
+
}
|
| 1770 |
+
decodeTimestamp -= baseDecodeTimestamp;
|
| 1771 |
+
presentationTimestamp -= baseDecodeTimestamp;
|
| 1772 |
+
}
|
| 1773 |
+
if (decodeTimestamp < track.lastDecodeTimestamp) {
|
| 1774 |
+
throw new Error(
|
| 1775 |
+
`Timestamps must be monotonically increasing (DTS went from ${track.lastDecodeTimestamp * 1e6} to ${decodeTimestamp * 1e6}).`
|
| 1776 |
+
);
|
| 1777 |
+
}
|
| 1778 |
+
track.lastDecodeTimestamp = decodeTimestamp;
|
| 1779 |
+
return { presentationTimestamp, decodeTimestamp };
|
| 1780 |
+
};
|
| 1781 |
+
_finalizeCurrentChunk = new WeakSet();
|
| 1782 |
+
finalizeCurrentChunk_fn = function(track) {
|
| 1783 |
+
if (__privateGet(this, _options).fastStart === "fragmented") {
|
| 1784 |
+
throw new Error("Can't finalize individual chunks if 'fastStart' is set to 'fragmented'.");
|
| 1785 |
+
}
|
| 1786 |
+
if (!track.currentChunk)
|
| 1787 |
+
return;
|
| 1788 |
+
track.finalizedChunks.push(track.currentChunk);
|
| 1789 |
+
__privateGet(this, _finalizedChunks).push(track.currentChunk);
|
| 1790 |
+
if (track.compactlyCodedChunkTable.length === 0 || last(track.compactlyCodedChunkTable).samplesPerChunk !== track.currentChunk.samples.length) {
|
| 1791 |
+
track.compactlyCodedChunkTable.push({
|
| 1792 |
+
firstChunk: track.finalizedChunks.length,
|
| 1793 |
+
// 1-indexed
|
| 1794 |
+
samplesPerChunk: track.currentChunk.samples.length
|
| 1795 |
+
});
|
| 1796 |
+
}
|
| 1797 |
+
if (__privateGet(this, _options).fastStart === "in-memory") {
|
| 1798 |
+
track.currentChunk.offset = 0;
|
| 1799 |
+
return;
|
| 1800 |
+
}
|
| 1801 |
+
track.currentChunk.offset = __privateGet(this, _writer).pos;
|
| 1802 |
+
for (let sample of track.currentChunk.samples) {
|
| 1803 |
+
__privateGet(this, _writer).write(sample.data);
|
| 1804 |
+
sample.data = null;
|
| 1805 |
+
}
|
| 1806 |
+
__privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this);
|
| 1807 |
+
};
|
| 1808 |
+
_finalizeFragment = new WeakSet();
|
| 1809 |
+
finalizeFragment_fn = function(flushStreamingWriter = true) {
|
| 1810 |
+
if (__privateGet(this, _options).fastStart !== "fragmented") {
|
| 1811 |
+
throw new Error("Can't finalize a fragment unless 'fastStart' is set to 'fragmented'.");
|
| 1812 |
+
}
|
| 1813 |
+
let tracks = [__privateGet(this, _videoTrack), __privateGet(this, _audioTrack)].filter((track) => track && track.currentChunk);
|
| 1814 |
+
if (tracks.length === 0)
|
| 1815 |
+
return;
|
| 1816 |
+
let fragmentNumber = __privateWrapper(this, _nextFragmentNumber)._++;
|
| 1817 |
+
if (fragmentNumber === 1) {
|
| 1818 |
+
let movieBox = moov(tracks, __privateGet(this, _creationTime), true);
|
| 1819 |
+
__privateGet(this, _writer).writeBox(movieBox);
|
| 1820 |
+
}
|
| 1821 |
+
let moofOffset = __privateGet(this, _writer).pos;
|
| 1822 |
+
let moofBox = moof(fragmentNumber, tracks);
|
| 1823 |
+
__privateGet(this, _writer).writeBox(moofBox);
|
| 1824 |
+
{
|
| 1825 |
+
let mdatBox = mdat(false);
|
| 1826 |
+
let totalTrackSampleSize = 0;
|
| 1827 |
+
for (let track of tracks) {
|
| 1828 |
+
for (let sample of track.currentChunk.samples) {
|
| 1829 |
+
totalTrackSampleSize += sample.size;
|
| 1830 |
+
}
|
| 1831 |
+
}
|
| 1832 |
+
let mdatSize = __privateGet(this, _writer).measureBox(mdatBox) + totalTrackSampleSize;
|
| 1833 |
+
if (mdatSize >= 2 ** 32) {
|
| 1834 |
+
mdatBox.largeSize = true;
|
| 1835 |
+
mdatSize = __privateGet(this, _writer).measureBox(mdatBox) + totalTrackSampleSize;
|
| 1836 |
+
}
|
| 1837 |
+
mdatBox.size = mdatSize;
|
| 1838 |
+
__privateGet(this, _writer).writeBox(mdatBox);
|
| 1839 |
+
}
|
| 1840 |
+
for (let track of tracks) {
|
| 1841 |
+
track.currentChunk.offset = __privateGet(this, _writer).pos;
|
| 1842 |
+
track.currentChunk.moofOffset = moofOffset;
|
| 1843 |
+
for (let sample of track.currentChunk.samples) {
|
| 1844 |
+
__privateGet(this, _writer).write(sample.data);
|
| 1845 |
+
sample.data = null;
|
| 1846 |
+
}
|
| 1847 |
+
}
|
| 1848 |
+
let endPos = __privateGet(this, _writer).pos;
|
| 1849 |
+
__privateGet(this, _writer).seek(__privateGet(this, _writer).offsets.get(moofBox));
|
| 1850 |
+
let newMoofBox = moof(fragmentNumber, tracks);
|
| 1851 |
+
__privateGet(this, _writer).writeBox(newMoofBox);
|
| 1852 |
+
__privateGet(this, _writer).seek(endPos);
|
| 1853 |
+
for (let track of tracks) {
|
| 1854 |
+
track.finalizedChunks.push(track.currentChunk);
|
| 1855 |
+
__privateGet(this, _finalizedChunks).push(track.currentChunk);
|
| 1856 |
+
track.currentChunk = null;
|
| 1857 |
+
}
|
| 1858 |
+
if (flushStreamingWriter) {
|
| 1859 |
+
__privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this);
|
| 1860 |
+
}
|
| 1861 |
+
};
|
| 1862 |
+
_maybeFlushStreamingTargetWriter = new WeakSet();
|
| 1863 |
+
maybeFlushStreamingTargetWriter_fn = function() {
|
| 1864 |
+
if (__privateGet(this, _writer) instanceof StreamTargetWriter) {
|
| 1865 |
+
__privateGet(this, _writer).flush();
|
| 1866 |
+
}
|
| 1867 |
+
};
|
| 1868 |
+
_ensureNotFinalized = new WeakSet();
|
| 1869 |
+
ensureNotFinalized_fn = function() {
|
| 1870 |
+
if (__privateGet(this, _finalized)) {
|
| 1871 |
+
throw new Error("Cannot add new video or audio chunks after the file has been finalized.");
|
| 1872 |
+
}
|
| 1873 |
+
};
|
| 1874 |
+
export {
|
| 1875 |
+
ArrayBufferTarget,
|
| 1876 |
+
FileSystemWritableFileStreamTarget,
|
| 1877 |
+
Muxer,
|
| 1878 |
+
StreamTarget
|
| 1879 |
+
};
|