From af19c3c9686d1382341f6fa77672b72bf52aea71 Mon Sep 17 00:00:00 2001 From: orfelorfel23 Date: Sat, 8 Aug 2026 22:13:58 +0200 Subject: [PATCH] feat: Add high-speed client-side GPU video export (WebCodecs) and server batching --- package-lock.json | 38 +- package.json | 4 +- public/app.js | 698 ++++++++++++---- public/index.html | 26 +- public/mp4-muxer.js | 1904 ++++++++++++++++++++++++++++++++++++++++++ public/style.css | 44 + public/webm-muxer.js | 1475 ++++++++++++++++++++++++++++++++ server.mjs | 4 +- 8 files changed, 4024 insertions(+), 169 deletions(-) create mode 100644 public/mp4-muxer.js create mode 100644 public/webm-muxer.js diff --git a/package-lock.json b/package-lock.json index 0773fb4..2ee39bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,23 @@ "dependencies": { "cors": "^2.8.5", "express": "^4.21.2", - "multer": "^1.4.5-lts.1" + "mp4-muxer": "^5.2.2", + "multer": "^1.4.5-lts.1", + "webm-muxer": "^5.1.4" } }, + "node_modules/@types/dom-webcodecs": { + "version": "0.1.18", + "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.18.tgz", + "integrity": "sha512-vAvE8C9DGWR+tkb19xyjk1TSUlJ7RUzzp4a9Anu7mwBT+fpyePWK1UxmH14tMO5zHmrnrRIMg5NutnnDztLxgg==", + "license": "MIT" + }, + "node_modules/@types/wicg-file-system-access": { + "version": "2020.9.8", + "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.8.tgz", + "integrity": "sha512-ggMz8nOygG7d/stpH40WVaNvBwuyYLnrg5Mbyf6bmsj/8+gb6Ei4ZZ9/4PNpcPNTT8th9Q8sM8wYmWGjMWLX/A==", + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -601,6 +615,17 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mp4-muxer": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/mp4-muxer/-/mp4-muxer-5.2.2.tgz", + "integrity": "sha512-dhozjTywI0h2qFzeShagt8YYw811fh1XlwiDCE2f6Aeqf6xG2CyuShoSa5E0AZDO8pPF0JOZ3wOmWBNWIGdSpQ==", + "deprecated": "This library is superseded by Mediabunny. Please migrate to it.", + "license": "MIT", + "dependencies": { + "@types/dom-webcodecs": "^0.1.6", + "@types/wicg-file-system-access": "^2020.9.5" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1005,6 +1030,17 @@ "node": ">= 0.8" } }, + "node_modules/webm-muxer": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webm-muxer/-/webm-muxer-5.1.4.tgz", + "integrity": "sha512-ditzgFVFbfqPaugkIr4mGhAdob5K9HY6Rzlh7TRsA368yA1sp/m5O7nQCcMLdgFDeNGtFPg8B+MeXLtpzKWX6Q==", + "deprecated": "This library is superseded by Mediabunny. Please migrate to it.", + "license": "MIT", + "dependencies": { + "@types/dom-webcodecs": "^0.1.4", + "@types/wicg-file-system-access": "^2020.9.5" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index bf055ee..3070d9c 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "dependencies": { "cors": "^2.8.5", "express": "^4.21.2", - "multer": "^1.4.5-lts.1" + "mp4-muxer": "^5.2.2", + "multer": "^1.4.5-lts.1", + "webm-muxer": "^5.1.4" } } diff --git a/public/app.js b/public/app.js index 5398073..a010d93 100644 --- a/public/app.js +++ b/public/app.js @@ -1677,6 +1677,532 @@ return new Blob([view], { type: 'audio/wav' }); } + function updateExportEngineBadge() { + const formatSelect = document.getElementById('exportFormat'); + if (!formatSelect) return; + const format = formatSelect.value; + const badge = document.getElementById('exportEngineBadge'); + const title = document.getElementById('exportEngineTitle'); + const desc = document.getElementById('exportEngineDesc'); + const icon = badge ? badge.querySelector('.badge-icon') : null; + + if (!badge || !title || !desc) return; + + if (format === 'webm_alpha' || format === 'mp4') { + badge.classList.remove('badge-server'); + if (icon) icon.textContent = '⚡'; + title.textContent = 'Client-GPU Export (Blitzschnell im Browser)'; + if (format === 'webm_alpha') { + desc.textContent = 'Rendert direkt im Browser bei 60–120+ FPS mit transparentem Alphakanal für DaVinci Resolve (0 MB Server-Upload).'; + } else { + desc.textContent = 'Rendert direkt im Browser bei 60–120+ FPS als H.264 MP4 für YouTube / Social Media (0 MB Server-Upload).'; + } + } else { + badge.classList.add('badge-server'); + if (icon) icon.textContent = '🖥️'; + title.textContent = 'Studio Master Server Export (FFmpeg)'; + if (format === 'prores') { + desc.textContent = 'Rendert Apple ProRes 4444 mit 10-Bit Alpha über FFmpeg auf dem Server (für High-End Broadcast Mastering).'; + } else { + desc.textContent = 'Rendert QuickTime RLE verlustfrei über FFmpeg auf dem Server.'; + } + } + } + + /** + * Hardware-Accelerated In-Browser Video Export Engine (WebCodecs + Muxer) + * Supports: WebM VP9 Alpha & MP4 H.264 + */ + async function exportVideoClientSide(format, targetWidth, targetHeight, fps, title) { + const isWebM = (format === 'webm_alpha'); + const ext = isWebM ? 'webm' : 'mp4'; + const suffix = isWebM ? 'Alpha_DaVinci' : 'Visualizer'; + const outputFilename = `${title}_${suffix}.${ext}`; + + const totalFrames = Math.ceil(state.duration * fps); + const sampleRate = state.audioBuffer.sampleRate; + const numChannels = Math.min(2, state.audioBuffer.numberOfChannels); + const channelData = state.audioBuffer.getChannelData(0); + const sampleWindow = 2048; + const samplesBuffer = new Float32Array(sampleWindow); + + // Create Offscreen Canvas for Frame Rendering + const renderCanvas = document.createElement('canvas'); + renderCanvas.width = targetWidth; + renderCanvas.height = targetHeight; + const renderCtx = renderCanvas.getContext('2d', { alpha: true, willReadFrequently: true }); + + // Determine Bitrate based on Resolution + let videoBitrate = 25_000_000; // 25 Mbps default for 1080p + if (targetWidth >= 3840) videoBitrate = 65_000_000; // 65 Mbps for 4K + else if (targetWidth <= 1280) videoBitrate = 14_000_000; + + let muxer = null; + let videoEncoder = null; + let audioEncoder = null; + let hasAudioTrack = false; + + // Verify WebCodecs support + const hasWebCodecs = (typeof VideoEncoder !== 'undefined' && typeof VideoFrame !== 'undefined'); + if (!hasWebCodecs) { + throw new Error('Dein Browser unterstützt die WebCodecs-Schnittstelle nicht für Offline-Rendering. Bitte verwende Google Chrome, Microsoft Edge oder einen aktuellen Chromium-Browser.'); + } + + if (isWebM) { + if (typeof WebMMuxer === 'undefined') { + throw new Error('WebMMuxer-Bibliothek wurde nicht geladen.'); + } + + // Check audio encoder support (Opus) + let audioConfig = null; + if (typeof AudioEncoder !== 'undefined') { + try { + const testConfig = { + codec: 'opus', + sampleRate: sampleRate, + numberOfChannels: numChannels, + bitrate: 320000 + }; + const supported = await AudioEncoder.isConfigSupported(testConfig); + if (supported && supported.supported) { + audioConfig = testConfig; + } + } catch (e) { + console.warn('Opus AudioEncoder test failed:', e); + } + } + + muxer = new WebMMuxer.Muxer({ + target: new WebMMuxer.ArrayBufferTarget(), + video: { + codec: 'V_VP9', + width: targetWidth, + height: targetHeight, + alpha: true, + frameRate: fps + }, + audio: audioConfig ? { + codec: 'A_OPUS', + numberOfChannels: audioConfig.numberOfChannels, + sampleRate: sampleRate, + bitrate: 320000 + } : undefined + }); + + if (audioConfig) { + audioEncoder = new AudioEncoder({ + output: (chunk, meta) => muxer.addAudioChunk(chunk, meta), + error: (e) => console.error('AudioEncoder Error:', e) + }); + audioEncoder.configure(audioConfig); + hasAudioTrack = true; + } + + // Configure VP9 VideoEncoder with alpha support + videoEncoder = new VideoEncoder({ + output: (chunk, meta) => muxer.addVideoChunk(chunk, meta), + error: (e) => console.error('VideoEncoder Error:', e) + }); + + videoEncoder.configure({ + codec: 'vp09.00.10.08', // VP9 Profile 0, 8-bit + width: targetWidth, + height: targetHeight, + bitrate: videoBitrate, + framerate: fps, + alpha: 'keep' + }); + + } else { + // MP4 Export + if (typeof Mp4Muxer === 'undefined') { + throw new Error('Mp4Muxer-Bibliothek wurde nicht geladen.'); + } + + // Check audio encoder support (AAC) + let audioConfig = null; + if (typeof AudioEncoder !== 'undefined') { + try { + const testConfig = { + codec: 'mp4a.40.2', + sampleRate: sampleRate, + numberOfChannels: numChannels, + bitrate: 320000 + }; + const supported = await AudioEncoder.isConfigSupported(testConfig); + if (supported && supported.supported) { + audioConfig = testConfig; + } + } catch (e) { + console.warn('AAC AudioEncoder test failed:', e); + } + } + + muxer = new Mp4Muxer.Muxer({ + target: new Mp4Muxer.ArrayBufferTarget(), + video: { + codec: 'avc', + width: targetWidth, + height: targetHeight + }, + audio: audioConfig ? { + codec: 'aac', + numberOfChannels: audioConfig.numberOfChannels, + sampleRate: sampleRate + } : undefined, + fastStart: 'in-memory' + }); + + if (audioConfig) { + audioEncoder = new AudioEncoder({ + output: (chunk, meta) => muxer.addAudioChunk(chunk, meta), + error: (e) => console.error('AudioEncoder Error:', e) + }); + audioEncoder.configure(audioConfig); + hasAudioTrack = true; + } + + // Determine best supported AVC profile + let selectedCodec = 'avc1.64002a'; // High Profile Level 4.2 + try { + const testSupported = await VideoEncoder.isConfigSupported({ + codec: selectedCodec, + width: targetWidth, + height: targetHeight, + bitrate: videoBitrate, + framerate: fps + }); + if (!testSupported || !testSupported.supported) { + selectedCodec = 'avc1.4d002a'; // Main Profile Level 4.2 + } + } catch (e) { + selectedCodec = 'avc1.4d002a'; + } + + videoEncoder = new VideoEncoder({ + output: (chunk, meta) => muxer.addVideoChunk(chunk, meta), + error: (e) => console.error('VideoEncoder Error:', e) + }); + + videoEncoder.configure({ + codec: selectedCodec, + width: targetWidth, + height: targetHeight, + bitrate: videoBitrate, + framerate: fps + }); + } + + // 1. Pre-encode Audio track in parallel if AudioEncoder is available + if (audioEncoder && hasAudioTrack) { + exportStatusText.textContent = 'Encodiere Audio-Track in Studio-Qualität...'; + const totalSamples = state.audioBuffer.length; + const channels = numChannels; + const chunkSize = 4096; + + for (let offset = 0; offset < totalSamples; offset += chunkSize) { + if (isExportCancelled) throw new Error('Export durch Benutzer abgebrochen'); + + const currentChunkSize = Math.min(chunkSize, totalSamples - offset); + const chunkData = new Float32Array(currentChunkSize * channels); + for (let c = 0; c < channels; c++) { + const srcData = state.audioBuffer.getChannelData(c); + for (let i = 0; i < currentChunkSize; i++) { + chunkData[c * currentChunkSize + i] = srcData[offset + i]; + } + } + + const timestampUs = Math.round((offset / sampleRate) * 1_000_000); + const audioData = new AudioData({ + format: 'f32-planar', + sampleRate: sampleRate, + numberOfChannels: channels, + numberOfFrames: currentChunkSize, + timestamp: timestampUs, + data: chunkData + }); + + audioEncoder.encode(audioData); + audioData.close(); + } + } + + // 2. Offline Frame-by-Frame Video Render Loop + const renderStartTime = performance.now(); + let lastEtaUpdate = 0; + + for (let f = 0; f < totalFrames; f++) { + if (isExportCancelled) throw new Error('Export durch Benutzer abgebrochen'); + + const currentTime = f / fps; + const centerSample = Math.floor(currentTime * sampleRate); + const startSample = Math.max(0, centerSample - 1024); + + for (let s = 0; s < sampleWindow; s++) { + const idx = startSample + s; + samplesBuffer[s] = (idx < channelData.length) ? channelData[idx] : 0; + } + + const frameFreq = globalFastFFT.compute(samplesBuffer, 1024); + + // Render frame to offscreen canvas + renderFrame(renderCtx, targetWidth, targetHeight, frameFreq, currentTime, state.duration, state.config, true); + + // Create VideoFrame for WebCodecs + const timestampUs = Math.round((f / fps) * 1_000_000); + const durationUs = Math.round((1 / fps) * 1_000_000); + const isKeyFrame = (f % (fps * 2) === 0); + + const videoFrame = new VideoFrame(renderCanvas, { + timestamp: timestampUs, + duration: durationUs, + alpha: isWebM ? 'keep' : 'discard' + }); + + videoEncoder.encode(videoFrame, { keyFrame: isKeyFrame }); + videoFrame.close(); + + // Backpressure management (prevent encoder queue from filling RAM) + if (videoEncoder.encodeQueueSize > 6) { + await new Promise(resolve => { + videoEncoder.ondequeue = () => { + if (videoEncoder.encodeQueueSize <= 2) { + videoEncoder.ondequeue = null; + resolve(); + } + }; + }); + } + + // Live Progress & FPS Display + const pct = Math.round(((f + 1) / totalFrames) * 100); + exportPercent.textContent = `${pct}%`; + exportProgressBarFill.style.width = `${pct}%`; + exportStatusText.textContent = `Rendert Frame ${f + 1} / ${totalFrames} (${pct}%)`; + + const now = performance.now(); + if (now - lastEtaUpdate > 250 || f === totalFrames - 1) { + const elapsedSecs = (now - renderStartTime) / 1000; + const currentFps = (f + 1) / (elapsedSecs || 0.001); + const remainingFrames = totalFrames - f - 1; + const remainingSecs = remainingFrames / (currentFps || 1); + + if (exportFpsLive) exportFpsLive.textContent = `Geschwindigkeit: ${currentFps.toFixed(1)} fps (GPU)`; + exportEta.textContent = `Verbleibend: ~${formatTime(remainingSecs)}`; + lastEtaUpdate = now; + } + + // UI thread breathing room + if (f % 6 === 0) { + await new Promise(r => setTimeout(r, 0)); + } + } + + // 3. Finalize Video & Audio Containers + exportStatusText.textContent = 'Finalisiere Video-Container...'; + if (exportFpsLive) exportFpsLive.textContent = 'Schreibe Metadaten...'; + + await videoEncoder.flush(); + if (audioEncoder) await audioEncoder.flush(); + muxer.finalize(); + + const { buffer } = muxer.target; + const mimeType = isWebM ? 'video/webm' : 'video/mp4'; + const blob = new Blob([buffer], { type: mimeType }); + + const elapsedTotal = ((performance.now() - renderStartTime) / 1000).toFixed(1); + const avgFps = (totalFrames / ((performance.now() - renderStartTime) / 1000)).toFixed(1); + const sizeMb = (blob.size / 1024 / 1024).toFixed(1); + + console.log(`✅ Client Render Complete in ${elapsedTotal}s @ ${avgFps} fps (${sizeMb} MB)`); + + // Download file + const downloadUrl = URL.createObjectURL(blob); + const downloadLink = document.createElement('a'); + downloadLink.style.display = 'none'; + downloadLink.href = downloadUrl; + downloadLink.download = outputFilename; + document.body.appendChild(downloadLink); + downloadLink.click(); + setTimeout(() => { + document.body.removeChild(downloadLink); + URL.revokeObjectURL(downloadUrl); + }, 5000); + + alert(`🎉 Fertig! Dein Video "${outputFilename}" (${sizeMb} MB) wurde mit durchschnittlich ${avgFps} fps gerendert und heruntergeladen.`); + } + + /** + * Batched High-Performance Server-Side FFmpeg Render Engine + * Used for Apple ProRes 4444 (.mov 10-Bit Alpha) & QuickTime RLE + */ + async function exportVideoServerSide(format, targetWidth, targetHeight, fps, title) { + let audioBlob = state.audioFile; + if (!audioBlob) { + audioBlob = audioBufferToWavBlob(state.audioBuffer); + } + + const formData = new FormData(); + formData.append('audio', audioBlob, state.audioFile ? state.audioFile.name : 'audio.wav'); + formData.append('format', format); + formData.append('width', targetWidth); + formData.append('height', targetHeight); + formData.append('fps', fps); + formData.append('title', title); + + exportStatusText.textContent = 'Verbinde mit Server FFmpeg Transcoder...'; + const startRes = await fetch('/api/export/start', { + method: 'POST', + body: formData + }); + + if (!startRes.ok) { + const errJson = await startRes.json().catch(() => ({})); + throw new Error(errJson.error || `Server antwortete mit Status ${startRes.status}`); + } + + const startData = await startRes.json(); + currentExportSessionId = startData.sessionId; + + // Offscreen Canvas for Rendering + const renderCanvas = document.createElement('canvas'); + renderCanvas.width = targetWidth; + renderCanvas.height = targetHeight; + const renderCtx = renderCanvas.getContext('2d', { alpha: true, willReadFrequently: true }); + + const totalFrames = Math.ceil(state.duration * fps); + const sampleRate = state.audioBuffer.sampleRate; + const channelData = state.audioBuffer.getChannelData(0); + const sampleWindow = 2048; + const samplesBuffer = new Float32Array(sampleWindow); + + const renderStartTime = performance.now(); + let lastEtaUpdate = 0; + + // Batching: Concatenate 15 frames into one network payload to drastically cut HTTP latency + const batchFrameCount = 15; + const singleFrameBytes = targetWidth * targetHeight * 4; + const batchBuffer = new Uint8Array(singleFrameBytes * batchFrameCount); + let framesInBatch = 0; + + for (let f = 0; f < totalFrames; f++) { + if (isExportCancelled) throw new Error('Export durch Benutzer abgebrochen'); + + const currentTime = f / fps; + const centerSample = Math.floor(currentTime * sampleRate); + const startSample = Math.max(0, centerSample - 1024); + + for (let s = 0; s < sampleWindow; s++) { + const idx = startSample + s; + samplesBuffer[s] = (idx < channelData.length) ? channelData[idx] : 0; + } + + const frameFreq = globalFastFFT.compute(samplesBuffer, 1024); + + // Render high-precision frame + renderFrame(renderCtx, targetWidth, targetHeight, frameFreq, currentTime, state.duration, state.config, true); + + // Extract raw RGBA + const imgData = renderCtx.getImageData(0, 0, targetWidth, targetHeight); + batchBuffer.set(imgData.data, framesInBatch * singleFrameBytes); + framesInBatch++; + + // Send batch when full or on last frame + if (framesInBatch === batchFrameCount || f === totalFrames - 1) { + const payload = (framesInBatch === batchFrameCount) + ? batchBuffer.buffer + : batchBuffer.slice(0, framesInBatch * singleFrameBytes).buffer; + + const frameRes = await fetch(`/api/export/frame-batch/${currentExportSessionId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/octet-stream' }, + body: payload + }); + + if (!frameRes.ok) { + const frameErr = await frameRes.json().catch(() => ({})); + throw new Error(frameErr.error || 'Fehler beim Senden des Frame-Batches an Server FFmpeg'); + } + + framesInBatch = 0; + } + + // Live Progress & ETA + const pct = Math.round(((f + 1) / totalFrames) * 100); + exportPercent.textContent = `${pct}%`; + exportProgressBarFill.style.width = `${pct}%`; + exportStatusText.textContent = `Rendert Frame ${f + 1} / ${totalFrames} (${pct}%)`; + + const now = performance.now(); + if (now - lastEtaUpdate > 400 || f === totalFrames - 1) { + const elapsedSecs = (now - renderStartTime) / 1000; + const currentFps = (f + 1) / elapsedSecs; + const remainingFrames = totalFrames - f - 1; + const remainingSecs = remainingFrames / (currentFps || 1); + + if (exportFpsLive) exportFpsLive.textContent = `Geschwindigkeit: ${currentFps.toFixed(1)} fps (Server)`; + exportEta.textContent = `Verbleibend: ~${formatTime(remainingSecs)}`; + lastEtaUpdate = now; + } + + if (f % 6 === 0) { + await new Promise(r => setTimeout(r, 0)); + } + } + + // Finish Server Encoding + exportStatusText.textContent = 'Finalisiere Video mit Studio-Codec auf dem Server...'; + if (exportFpsLive) exportFpsLive.textContent = 'Schreibe Audio & Video...'; + + const finishRes = await fetch(`/api/export/finish/${currentExportSessionId}`, { + method: 'POST' + }); + + if (!finishRes.ok) { + const finishErr = await finishRes.json().catch(() => ({})); + throw new Error(finishErr.error || 'Fehler beim Finalisieren des Videos auf dem Server'); + } + + // Poll status until ready + let finishData = null; + while (!finishData && !isExportCancelled) { + await new Promise(r => setTimeout(r, 600)); + + const statusRes = await fetch(`/api/export/status/${currentExportSessionId}`); + if (!statusRes.ok) { + const errData = await statusRes.json().catch(() => ({})); + throw new Error(errData.error || 'Fehler beim Abrufen des Video-Status'); + } + + const statusJson = await statusRes.json(); + if (statusJson.status === 'ready') { + finishData = statusJson; + break; + } else if (statusJson.status === 'error') { + throw new Error(statusJson.error || 'FFmpeg Encoder-Fehler auf dem Server'); + } else { + const mb = (statusJson.sizeBytes / 1024 / 1024).toFixed(1); + exportStatusText.textContent = `Finalisiere Video (${mb} MB geschrieben)...`; + if (exportFpsLive) exportFpsLive.textContent = 'Schreibe Datei auf Festplatte...'; + } + } + + if (isExportCancelled) throw new Error('Export durch Benutzer abgebrochen'); + + // Automatic Download Trigger + const downloadLink = document.createElement('a'); + downloadLink.style.display = 'none'; + downloadLink.href = finishData.downloadUrl; + downloadLink.download = finishData.filename; + document.body.appendChild(downloadLink); + downloadLink.click(); + setTimeout(() => { + document.body.removeChild(downloadLink); + }, 1000); + + alert(`🎉 Fertig! Dein Video "${finishData.filename}" wurde in makelloser Studio-Qualität gerendert.`); + } + async function startExport() { if (!state.audioLoaded || !state.audioBuffer) { alert('Bitte lade zuerst eine Audio-Datei (.wav oder .mp3) im Tab "Media" hoch!'); @@ -1713,173 +2239,21 @@ isExportCancelled = false; exportProgressCard.classList.remove('hidden'); btnStartExport.classList.add('hidden'); - exportStatusText.textContent = 'Bereite Studio-Render vor...'; + exportStatusText.textContent = 'Bereite Render vor...'; exportPercent.textContent = '0%'; exportProgressBarFill.style.width = '0%'; exportEta.textContent = `Gesamtlänge: ${formatTime(state.duration)}`; const exportFpsLive = document.getElementById('exportFpsLive'); - if (exportFpsLive) exportFpsLive.textContent = 'Status: Initialisiere FFmpeg Pipeline...'; + if (exportFpsLive) exportFpsLive.textContent = 'Status: Initialisiere Engine...'; - // Create Offscreen Canvas for Frame Rendering - const renderCanvas = document.createElement('canvas'); - renderCanvas.width = targetWidth; - renderCanvas.height = targetHeight; - const renderCtx = renderCanvas.getContext('2d', { alpha: true, willReadFrequently: true }); + const title = (document.getElementById('trackTitle').value || 'Visualizer').trim(); try { - // 1. Prepare Audio Blob - let audioBlob = state.audioFile; - if (!audioBlob) { - audioBlob = audioBufferToWavBlob(state.audioBuffer); + if (format === 'webm_alpha' || format === 'mp4') { + await exportVideoClientSide(format, targetWidth, targetHeight, fps, title); + } else { + await exportVideoServerSide(format, targetWidth, targetHeight, fps, title); } - - const title = (document.getElementById('trackTitle').value || 'Visualizer').trim(); - - // 2. Start Export Session with Backend FFmpeg Engine - const formData = new FormData(); - formData.append('audio', audioBlob, state.audioFile ? state.audioFile.name : 'audio.wav'); - formData.append('format', format); - formData.append('width', targetWidth); - formData.append('height', targetHeight); - formData.append('fps', fps); - formData.append('title', title); - - exportStatusText.textContent = 'Verbinde mit FFmpeg Transcoder...'; - const startRes = await fetch('/api/export/start', { - method: 'POST', - body: formData - }); - - if (!startRes.ok) { - const errJson = await startRes.json().catch(() => ({})); - throw new Error(errJson.error || `Server antwortete mit Status ${startRes.status}`); - } - - const startData = await startRes.json(); - currentExportSessionId = startData.sessionId; - - // 3. Render and Stream Frames - const totalFrames = Math.ceil(state.duration * fps); - const sampleRate = state.audioBuffer.sampleRate; - const channelData = state.audioBuffer.getChannelData(0); - const sampleWindow = 2048; - const samplesBuffer = new Float32Array(sampleWindow); - - const renderStartTime = performance.now(); - let lastEtaUpdate = 0; - - for (let f = 0; f < totalFrames; f++) { - if (isExportCancelled) { - throw new Error('Export durch Benutzer abgebrochen'); - } - - const currentTime = f / fps; - const centerSample = Math.floor(currentTime * sampleRate); - const startSample = Math.max(0, centerSample - 1024); - - for (let s = 0; s < sampleWindow; s++) { - const idx = startSample + s; - samplesBuffer[s] = (idx < channelData.length) ? channelData[idx] : 0; - } - - const frameFreq = globalFastFFT.compute(samplesBuffer, 1024); - - // Render high-precision frame - renderFrame(renderCtx, targetWidth, targetHeight, frameFreq, currentTime, state.duration, state.config, true); - - // Extract raw RGBA Buffer - const imgData = renderCtx.getImageData(0, 0, targetWidth, targetHeight); - - // Stream Frame to Server - const frameRes = await fetch(`/api/export/frame/${currentExportSessionId}`, { - method: 'POST', - headers: { 'Content-Type': 'application/octet-stream' }, - body: imgData.data.buffer - }); - - if (!frameRes.ok) { - const frameErr = await frameRes.json().catch(() => ({})); - throw new Error(frameErr.error || 'Fehler beim Senden des Render-Frames an FFmpeg'); - } - - // Live Progress & ETA Calculation - const pct = Math.round(((f + 1) / totalFrames) * 100); - exportPercent.textContent = `${pct}%`; - exportProgressBarFill.style.width = `${pct}%`; - exportStatusText.textContent = `Rendert Frame ${f + 1} / ${totalFrames} (${pct}%)`; - - const now = performance.now(); - if (now - lastEtaUpdate > 400 || f === totalFrames - 1) { - const elapsedSecs = (now - renderStartTime) / 1000; - const currentFps = (f + 1) / elapsedSecs; - const remainingFrames = totalFrames - f - 1; - const remainingSecs = remainingFrames / (currentFps || 1); - - if (exportFpsLive) exportFpsLive.textContent = `Geschwindigkeit: ${currentFps.toFixed(1)} fps`; - exportEta.textContent = `Verbleibend: ~${formatTime(remainingSecs)}`; - lastEtaUpdate = now; - } - - // Keep UI thread responsive - if (f % 4 === 0) { - await new Promise(r => setTimeout(r, 0)); - } - } - - // 4. Finish Encoding & Finalize Container - exportStatusText.textContent = 'Finalisiere Video mit Studio-Codec...'; - if (exportFpsLive) exportFpsLive.textContent = 'Schreibe Audio & Video...'; - - const finishRes = await fetch(`/api/export/finish/${currentExportSessionId}`, { - method: 'POST' - }); - - if (!finishRes.ok) { - const finishErr = await finishRes.json().catch(() => ({})); - throw new Error(finishErr.error || 'Fehler beim Finalisieren des Videos'); - } - - // Poll status until ready or error (no timeout, shows live disk size) - let finishData = null; - while (!finishData && !isExportCancelled) { - await new Promise(r => setTimeout(r, 600)); - - const statusRes = await fetch(`/api/export/status/${currentExportSessionId}`); - if (!statusRes.ok) { - const errData = await statusRes.json().catch(() => ({})); - throw new Error(errData.error || 'Fehler beim Abrufen des Video-Status'); - } - - const statusJson = await statusRes.json(); - if (statusJson.status === 'ready') { - finishData = statusJson; - break; - } else if (statusJson.status === 'error') { - throw new Error(statusJson.error || 'FFmpeg Encoder-Fehler'); - } else { - const mb = (statusJson.sizeBytes / 1024 / 1024).toFixed(1); - exportStatusText.textContent = `Finalisiere Video (${mb} MB geschrieben)...`; - if (exportFpsLive) exportFpsLive.textContent = 'Schreibe Datei auf Festplatte...'; - } - } - - if (isExportCancelled) { - throw new Error('Export durch Benutzer abgebrochen'); - } - - // 5. Automatic Download Trigger - const downloadLink = document.createElement('a'); - downloadLink.style.display = 'none'; - downloadLink.href = finishData.downloadUrl; - downloadLink.download = finishData.filename; - document.body.appendChild(downloadLink); - downloadLink.click(); - setTimeout(() => { - document.body.removeChild(downloadLink); - }, 1000); - - alert(`🎉 Fertig! Dein Video "${finishData.filename}" wurde in makelloser Studio-Qualität gerendert.`); - } catch (err) { console.error('Export Engine Error:', err); if (!isExportCancelled) { @@ -2117,6 +2491,11 @@ } }); + const exportFormatSelect = document.getElementById('exportFormat'); + if (exportFormatSelect) { + exportFormatSelect.addEventListener('change', updateExportEngineBadge); + } + btnStartExport.addEventListener('click', startExport); headerExportBtn.addEventListener('click', () => { document.querySelector('[data-tab="tab-export"]').click(); @@ -2136,6 +2515,7 @@ setAspectRatio('16:9'); applyPreset('trap_nation'); setupEvents(); + updateExportEngineBadge(); state.isPlaying = true; iconPlay.classList.add('hidden'); iconPause.classList.remove('hidden'); diff --git a/public/index.html b/public/index.html index 1ef623e..5e97cbb 100644 --- a/public/index.html +++ b/public/index.html @@ -1173,13 +1173,25 @@

Video Export

-
- +
+ ⚡ +
+ Client-GPU Export (Empfohlen) + Rendert direkt im Browser bei 60–120+ FPS mit 0 MB Server-Upload. +
+
+ +
+
@@ -1230,6 +1242,8 @@
+ + diff --git a/public/mp4-muxer.js b/public/mp4-muxer.js new file mode 100644 index 0000000..10cbbd3 --- /dev/null +++ b/public/mp4-muxer.js @@ -0,0 +1,1904 @@ +"use strict"; +var Mp4Muxer = (() => { + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + var __accessCheck = (obj, member, msg) => { + if (!member.has(obj)) + throw TypeError("Cannot " + msg); + }; + var __privateGet = (obj, member, getter) => { + __accessCheck(obj, member, "read from private field"); + return getter ? getter.call(obj) : member.get(obj); + }; + var __privateAdd = (obj, member, value) => { + if (member.has(obj)) + throw TypeError("Cannot add the same private member more than once"); + member instanceof WeakSet ? member.add(obj) : member.set(obj, value); + }; + var __privateSet = (obj, member, value, setter) => { + __accessCheck(obj, member, "write to private field"); + setter ? setter.call(obj, value) : member.set(obj, value); + return value; + }; + var __privateWrapper = (obj, member, setter, getter) => ({ + set _(value) { + __privateSet(obj, member, value, setter); + }, + get _() { + return __privateGet(obj, member, getter); + } + }); + var __privateMethod = (obj, member, method) => { + __accessCheck(obj, member, "access private method"); + return method; + }; + + // src/index.ts + var src_exports = {}; + __export(src_exports, { + ArrayBufferTarget: () => ArrayBufferTarget, + FileSystemWritableFileStreamTarget: () => FileSystemWritableFileStreamTarget, + Muxer: () => Muxer, + StreamTarget: () => StreamTarget + }); + + // src/misc.ts + var bytes = new Uint8Array(8); + var view = new DataView(bytes.buffer); + var u8 = (value) => { + return [(value % 256 + 256) % 256]; + }; + var u16 = (value) => { + view.setUint16(0, value, false); + return [bytes[0], bytes[1]]; + }; + var i16 = (value) => { + view.setInt16(0, value, false); + return [bytes[0], bytes[1]]; + }; + var u24 = (value) => { + view.setUint32(0, value, false); + return [bytes[1], bytes[2], bytes[3]]; + }; + var u32 = (value) => { + view.setUint32(0, value, false); + return [bytes[0], bytes[1], bytes[2], bytes[3]]; + }; + var i32 = (value) => { + view.setInt32(0, value, false); + return [bytes[0], bytes[1], bytes[2], bytes[3]]; + }; + var u64 = (value) => { + view.setUint32(0, Math.floor(value / 2 ** 32), false); + view.setUint32(4, value, false); + return [bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]]; + }; + var fixed_8_8 = (value) => { + view.setInt16(0, 2 ** 8 * value, false); + return [bytes[0], bytes[1]]; + }; + var fixed_16_16 = (value) => { + view.setInt32(0, 2 ** 16 * value, false); + return [bytes[0], bytes[1], bytes[2], bytes[3]]; + }; + var fixed_2_30 = (value) => { + view.setInt32(0, 2 ** 30 * value, false); + return [bytes[0], bytes[1], bytes[2], bytes[3]]; + }; + var ascii = (text, nullTerminated = false) => { + let bytes2 = Array(text.length).fill(null).map((_, i) => text.charCodeAt(i)); + if (nullTerminated) + bytes2.push(0); + return bytes2; + }; + var last = (arr) => { + return arr && arr[arr.length - 1]; + }; + var lastPresentedSample = (samples) => { + let result = void 0; + for (let sample of samples) { + if (!result || sample.presentationTimestamp > result.presentationTimestamp) { + result = sample; + } + } + return result; + }; + var intoTimescale = (timeInSeconds, timescale, round = true) => { + let value = timeInSeconds * timescale; + return round ? Math.round(value) : value; + }; + var rotationMatrix = (rotationInDegrees) => { + let theta = rotationInDegrees * (Math.PI / 180); + let cosTheta = Math.cos(theta); + let sinTheta = Math.sin(theta); + return [ + cosTheta, + sinTheta, + 0, + -sinTheta, + cosTheta, + 0, + 0, + 0, + 1 + ]; + }; + var IDENTITY_MATRIX = rotationMatrix(0); + var matrixToBytes = (matrix) => { + return [ + fixed_16_16(matrix[0]), + fixed_16_16(matrix[1]), + fixed_2_30(matrix[2]), + fixed_16_16(matrix[3]), + fixed_16_16(matrix[4]), + fixed_2_30(matrix[5]), + fixed_16_16(matrix[6]), + fixed_16_16(matrix[7]), + fixed_2_30(matrix[8]) + ]; + }; + var deepClone = (x) => { + if (!x) + return x; + if (typeof x !== "object") + return x; + if (Array.isArray(x)) + return x.map(deepClone); + return Object.fromEntries(Object.entries(x).map(([key, value]) => [key, deepClone(value)])); + }; + var isU32 = (value) => { + return value >= 0 && value < 2 ** 32; + }; + + // src/box.ts + var box = (type, contents, children) => ({ + type, + contents: contents && new Uint8Array(contents.flat(10)), + children + }); + var fullBox = (type, version, flags, contents, children) => box( + type, + [u8(version), u24(flags), contents ?? []], + children + ); + var ftyp = (details) => { + let minorVersion = 512; + if (details.fragmented) + return box("ftyp", [ + ascii("iso5"), + // Major brand + u32(minorVersion), + // Minor version + // Compatible brands + ascii("iso5"), + ascii("iso6"), + ascii("mp41") + ]); + return box("ftyp", [ + ascii("isom"), + // Major brand + u32(minorVersion), + // Minor version + // Compatible brands + ascii("isom"), + details.holdsAvc ? ascii("avc1") : [], + ascii("mp41") + ]); + }; + var mdat = (reserveLargeSize) => ({ type: "mdat", largeSize: reserveLargeSize }); + var free = (size) => ({ type: "free", size }); + var moov = (tracks, creationTime, fragmented = false) => box("moov", null, [ + mvhd(creationTime, tracks), + ...tracks.map((x) => trak(x, creationTime)), + fragmented ? mvex(tracks) : null + ]); + var mvhd = (creationTime, tracks) => { + let duration = intoTimescale(Math.max( + 0, + ...tracks.filter((x) => x.samples.length > 0).map((x) => { + const lastSample = lastPresentedSample(x.samples); + return lastSample.presentationTimestamp + lastSample.duration; + }) + ), GLOBAL_TIMESCALE); + let nextTrackId = Math.max(...tracks.map((x) => x.id)) + 1; + let needsU64 = !isU32(creationTime) || !isU32(duration); + let u32OrU64 = needsU64 ? u64 : u32; + return fullBox("mvhd", +needsU64, 0, [ + u32OrU64(creationTime), + // Creation time + u32OrU64(creationTime), + // Modification time + u32(GLOBAL_TIMESCALE), + // Timescale + u32OrU64(duration), + // Duration + fixed_16_16(1), + // Preferred rate + fixed_8_8(1), + // Preferred volume + Array(10).fill(0), + // Reserved + matrixToBytes(IDENTITY_MATRIX), + // Matrix + Array(24).fill(0), + // Pre-defined + u32(nextTrackId) + // Next track ID + ]); + }; + var trak = (track, creationTime) => box("trak", null, [ + tkhd(track, creationTime), + mdia(track, creationTime) + ]); + var tkhd = (track, creationTime) => { + let lastSample = lastPresentedSample(track.samples); + let durationInGlobalTimescale = intoTimescale( + lastSample ? lastSample.presentationTimestamp + lastSample.duration : 0, + GLOBAL_TIMESCALE + ); + let needsU64 = !isU32(creationTime) || !isU32(durationInGlobalTimescale); + let u32OrU64 = needsU64 ? u64 : u32; + let matrix; + if (track.info.type === "video") { + matrix = typeof track.info.rotation === "number" ? rotationMatrix(track.info.rotation) : track.info.rotation; + } else { + matrix = IDENTITY_MATRIX; + } + return fullBox("tkhd", +needsU64, 3, [ + u32OrU64(creationTime), + // Creation time + u32OrU64(creationTime), + // Modification time + u32(track.id), + // Track ID + u32(0), + // Reserved + u32OrU64(durationInGlobalTimescale), + // Duration + Array(8).fill(0), + // Reserved + u16(0), + // Layer + u16(0), + // Alternate group + fixed_8_8(track.info.type === "audio" ? 1 : 0), + // Volume + u16(0), + // Reserved + matrixToBytes(matrix), + // Matrix + fixed_16_16(track.info.type === "video" ? track.info.width : 0), + // Track width + fixed_16_16(track.info.type === "video" ? track.info.height : 0) + // Track height + ]); + }; + var mdia = (track, creationTime) => box("mdia", null, [ + mdhd(track, creationTime), + hdlr(track.info.type === "video" ? "vide" : "soun"), + minf(track) + ]); + var mdhd = (track, creationTime) => { + let lastSample = lastPresentedSample(track.samples); + let localDuration = intoTimescale( + lastSample ? lastSample.presentationTimestamp + lastSample.duration : 0, + track.timescale + ); + let needsU64 = !isU32(creationTime) || !isU32(localDuration); + let u32OrU64 = needsU64 ? u64 : u32; + return fullBox("mdhd", +needsU64, 0, [ + u32OrU64(creationTime), + // Creation time + u32OrU64(creationTime), + // Modification time + u32(track.timescale), + // Timescale + u32OrU64(localDuration), + // Duration + u16(21956), + // Language ("und", undetermined) + u16(0) + // Quality + ]); + }; + var hdlr = (componentSubtype) => fullBox("hdlr", 0, 0, [ + ascii("mhlr"), + // Component type + ascii(componentSubtype), + // Component subtype + u32(0), + // Component manufacturer + u32(0), + // Component flags + u32(0), + // Component flags mask + ascii("mp4-muxer-hdlr", true) + // Component name + ]); + var minf = (track) => box("minf", null, [ + track.info.type === "video" ? vmhd() : smhd(), + dinf(), + stbl(track) + ]); + var vmhd = () => fullBox("vmhd", 0, 1, [ + u16(0), + // Graphics mode + u16(0), + // Opcolor R + u16(0), + // Opcolor G + u16(0) + // Opcolor B + ]); + var smhd = () => fullBox("smhd", 0, 0, [ + u16(0), + // Balance + u16(0) + // Reserved + ]); + var dinf = () => box("dinf", null, [ + dref() + ]); + var dref = () => fullBox("dref", 0, 0, [ + u32(1) + // Entry count + ], [ + url() + ]); + var url = () => fullBox("url ", 0, 1); + var stbl = (track) => { + const needsCtts = track.compositionTimeOffsetTable.length > 1 || track.compositionTimeOffsetTable.some((x) => x.sampleCompositionTimeOffset !== 0); + return box("stbl", null, [ + stsd(track), + stts(track), + stss(track), + stsc(track), + stsz(track), + stco(track), + needsCtts ? ctts(track) : null + ]); + }; + var stsd = (track) => fullBox("stsd", 0, 0, [ + u32(1) + // Entry count + ], [ + track.info.type === "video" ? videoSampleDescription( + VIDEO_CODEC_TO_BOX_NAME[track.info.codec], + track + ) : soundSampleDescription( + AUDIO_CODEC_TO_BOX_NAME[track.info.codec], + track + ) + ]); + var videoSampleDescription = (compressionType, track) => box(compressionType, [ + Array(6).fill(0), + // Reserved + u16(1), + // Data reference index + u16(0), + // Pre-defined + u16(0), + // Reserved + Array(12).fill(0), + // Pre-defined + u16(track.info.width), + // Width + u16(track.info.height), + // Height + u32(4718592), + // Horizontal resolution + u32(4718592), + // Vertical resolution + u32(0), + // Reserved + u16(1), + // Frame count + Array(32).fill(0), + // Compressor name + u16(24), + // Depth + i16(65535) + // Pre-defined + ], [ + VIDEO_CODEC_TO_CONFIGURATION_BOX[track.info.codec](track), + track.info.decoderConfig.colorSpace ? colr(track) : null + ]); + var COLOR_PRIMARIES_MAP = { + "bt709": 1, + // ITU-R BT.709 + "bt470bg": 5, + // ITU-R BT.470BG + "smpte170m": 6 + // ITU-R BT.601 525 - SMPTE 170M + }; + var TRANSFER_CHARACTERISTICS_MAP = { + "bt709": 1, + // ITU-R BT.709 + "smpte170m": 6, + // SMPTE 170M + "iec61966-2-1": 13 + // IEC 61966-2-1 + }; + var MATRIX_COEFFICIENTS_MAP = { + "rgb": 0, + // Identity + "bt709": 1, + // ITU-R BT.709 + "bt470bg": 5, + // ITU-R BT.470BG + "smpte170m": 6 + // SMPTE 170M + }; + var colr = (track) => box("colr", [ + ascii("nclx"), + // Colour type + u16(COLOR_PRIMARIES_MAP[track.info.decoderConfig.colorSpace.primaries]), + // Colour primaries + u16(TRANSFER_CHARACTERISTICS_MAP[track.info.decoderConfig.colorSpace.transfer]), + // Transfer characteristics + u16(MATRIX_COEFFICIENTS_MAP[track.info.decoderConfig.colorSpace.matrix]), + // Matrix coefficients + u8((track.info.decoderConfig.colorSpace.fullRange ? 1 : 0) << 7) + // Full range flag + ]); + var avcC = (track) => track.info.decoderConfig && box("avcC", [ + // For AVC, description is an AVCDecoderConfigurationRecord, so nothing else to do here + ...new Uint8Array(track.info.decoderConfig.description) + ]); + var hvcC = (track) => track.info.decoderConfig && box("hvcC", [ + // For HEVC, description is a HEVCDecoderConfigurationRecord, so nothing else to do here + ...new Uint8Array(track.info.decoderConfig.description) + ]); + var vpcC = (track) => { + if (!track.info.decoderConfig) { + return null; + } + let decoderConfig = track.info.decoderConfig; + if (!decoderConfig.colorSpace) { + throw new Error(`'colorSpace' is required in the decoder config for VP9.`); + } + let parts = decoderConfig.codec.split("."); + let profile = Number(parts[1]); + let level = Number(parts[2]); + let bitDepth = Number(parts[3]); + let chromaSubsampling = 0; + let thirdByte = (bitDepth << 4) + (chromaSubsampling << 1) + Number(decoderConfig.colorSpace.fullRange); + let colourPrimaries = 2; + let transferCharacteristics = 2; + let matrixCoefficients = 2; + return fullBox("vpcC", 1, 0, [ + u8(profile), + // Profile + u8(level), + // Level + u8(thirdByte), + // Bit depth, chroma subsampling, full range + u8(colourPrimaries), + // Colour primaries + u8(transferCharacteristics), + // Transfer characteristics + u8(matrixCoefficients), + // Matrix coefficients + u16(0) + // Codec initialization data size + ]); + }; + var av1C = () => { + let marker = 1; + let version = 1; + let firstByte = (marker << 7) + version; + return box("av1C", [ + firstByte, + 0, + 0, + 0 + ]); + }; + var soundSampleDescription = (compressionType, track) => box(compressionType, [ + Array(6).fill(0), + // Reserved + u16(1), + // Data reference index + u16(0), + // Version + u16(0), + // Revision level + u32(0), + // Vendor + u16(track.info.numberOfChannels), + // Number of channels + u16(16), + // Sample size (bits) + u16(0), + // Compression ID + u16(0), + // Packet size + fixed_16_16(track.info.sampleRate) + // Sample rate + ], [ + AUDIO_CODEC_TO_CONFIGURATION_BOX[track.info.codec](track) + ]); + var esds = (track) => { + let description = new Uint8Array(track.info.decoderConfig.description); + return fullBox("esds", 0, 0, [ + // https://stackoverflow.com/a/54803118 + u32(58753152), + // TAG(3) = Object Descriptor ([2]) + u8(32 + description.byteLength), + // length of this OD (which includes the next 2 tags) + u16(1), + // ES_ID = 1 + u8(0), + // flags etc = 0 + u32(75530368), + // TAG(4) = ES Descriptor ([2]) embedded in above OD + u8(18 + description.byteLength), + // length of this ESD + u8(64), + // MPEG-4 Audio + u8(21), + // stream type(6bits)=5 audio, flags(2bits)=1 + u24(0), + // 24bit buffer size + u32(130071), + // max bitrate + u32(130071), + // avg bitrate + u32(92307584), + // TAG(5) = ASC ([2],[3]) embedded in above OD + u8(description.byteLength), + // length + ...description, + u32(109084800), + // TAG(6) + u8(1), + // length + u8(2) + // data + ]); + }; + var dOps = (track) => { + let preskip = 3840; + let gain = 0; + const description = track.info.decoderConfig?.description; + if (description) { + if (description.byteLength < 18) { + throw new TypeError("Invalid decoder description provided for Opus; must be at least 18 bytes long."); + } + const view2 = ArrayBuffer.isView(description) ? new DataView(description.buffer, description.byteOffset, description.byteLength) : new DataView(description); + preskip = view2.getUint16(10, true); + gain = view2.getInt16(14, true); + } + return box("dOps", [ + u8(0), + // Version + u8(track.info.numberOfChannels), + // OutputChannelCount + u16(preskip), + u32(track.info.sampleRate), + // InputSampleRate + fixed_8_8(gain), + // OutputGain + u8(0) + // ChannelMappingFamily + ]); + }; + var stts = (track) => { + return fullBox("stts", 0, 0, [ + u32(track.timeToSampleTable.length), + // Number of entries + track.timeToSampleTable.map((x) => [ + // Time-to-sample table + u32(x.sampleCount), + // Sample count + u32(x.sampleDelta) + // Sample duration + ]) + ]); + }; + var stss = (track) => { + if (track.samples.every((x) => x.type === "key")) + return null; + let keySamples = [...track.samples.entries()].filter(([, sample]) => sample.type === "key"); + return fullBox("stss", 0, 0, [ + u32(keySamples.length), + // Number of entries + keySamples.map(([index]) => u32(index + 1)) + // Sync sample table + ]); + }; + var stsc = (track) => { + return fullBox("stsc", 0, 0, [ + u32(track.compactlyCodedChunkTable.length), + // Number of entries + track.compactlyCodedChunkTable.map((x) => [ + // Sample-to-chunk table + u32(x.firstChunk), + // First chunk + u32(x.samplesPerChunk), + // Samples per chunk + u32(1) + // Sample description index + ]) + ]); + }; + var stsz = (track) => fullBox("stsz", 0, 0, [ + u32(0), + // Sample size (0 means non-constant size) + u32(track.samples.length), + // Number of entries + track.samples.map((x) => u32(x.size)) + // Sample size table + ]); + var stco = (track) => { + if (track.finalizedChunks.length > 0 && last(track.finalizedChunks).offset >= 2 ** 32) { + return fullBox("co64", 0, 0, [ + u32(track.finalizedChunks.length), + // Number of entries + track.finalizedChunks.map((x) => u64(x.offset)) + // Chunk offset table + ]); + } + return fullBox("stco", 0, 0, [ + u32(track.finalizedChunks.length), + // Number of entries + track.finalizedChunks.map((x) => u32(x.offset)) + // Chunk offset table + ]); + }; + var ctts = (track) => { + return fullBox("ctts", 0, 0, [ + u32(track.compositionTimeOffsetTable.length), + // Number of entries + track.compositionTimeOffsetTable.map((x) => [ + // Time-to-sample table + u32(x.sampleCount), + // Sample count + u32(x.sampleCompositionTimeOffset) + // Sample offset + ]) + ]); + }; + var mvex = (tracks) => { + return box("mvex", null, tracks.map(trex)); + }; + var trex = (track) => { + return fullBox("trex", 0, 0, [ + u32(track.id), + // Track ID + u32(1), + // Default sample description index + u32(0), + // Default sample duration + u32(0), + // Default sample size + u32(0) + // Default sample flags + ]); + }; + var moof = (sequenceNumber, tracks) => { + return box("moof", null, [ + mfhd(sequenceNumber), + ...tracks.map(traf) + ]); + }; + var mfhd = (sequenceNumber) => { + return fullBox("mfhd", 0, 0, [ + u32(sequenceNumber) + // Sequence number + ]); + }; + var fragmentSampleFlags = (sample) => { + let byte1 = 0; + let byte2 = 0; + let byte3 = 0; + let byte4 = 0; + let sampleIsDifferenceSample = sample.type === "delta"; + byte2 |= +sampleIsDifferenceSample; + if (sampleIsDifferenceSample) { + byte1 |= 1; + } else { + byte1 |= 2; + } + return byte1 << 24 | byte2 << 16 | byte3 << 8 | byte4; + }; + var traf = (track) => { + return box("traf", null, [ + tfhd(track), + tfdt(track), + trun(track) + ]); + }; + var tfhd = (track) => { + let tfFlags = 0; + tfFlags |= 8; + tfFlags |= 16; + tfFlags |= 32; + tfFlags |= 131072; + let referenceSample = track.currentChunk.samples[1] ?? track.currentChunk.samples[0]; + let referenceSampleInfo = { + duration: referenceSample.timescaleUnitsToNextSample, + size: referenceSample.size, + flags: fragmentSampleFlags(referenceSample) + }; + return fullBox("tfhd", 0, tfFlags, [ + u32(track.id), + // Track ID + u32(referenceSampleInfo.duration), + // Default sample duration + u32(referenceSampleInfo.size), + // Default sample size + u32(referenceSampleInfo.flags) + // Default sample flags + ]); + }; + var tfdt = (track) => { + return fullBox("tfdt", 1, 0, [ + u64(intoTimescale(track.currentChunk.startTimestamp, track.timescale)) + // Base Media Decode Time + ]); + }; + var trun = (track) => { + let allSampleDurations = track.currentChunk.samples.map((x) => x.timescaleUnitsToNextSample); + let allSampleSizes = track.currentChunk.samples.map((x) => x.size); + let allSampleFlags = track.currentChunk.samples.map(fragmentSampleFlags); + let allSampleCompositionTimeOffsets = track.currentChunk.samples.map((x) => intoTimescale(x.presentationTimestamp - x.decodeTimestamp, track.timescale)); + let uniqueSampleDurations = new Set(allSampleDurations); + let uniqueSampleSizes = new Set(allSampleSizes); + let uniqueSampleFlags = new Set(allSampleFlags); + let uniqueSampleCompositionTimeOffsets = new Set(allSampleCompositionTimeOffsets); + let firstSampleFlagsPresent = uniqueSampleFlags.size === 2 && allSampleFlags[0] !== allSampleFlags[1]; + let sampleDurationPresent = uniqueSampleDurations.size > 1; + let sampleSizePresent = uniqueSampleSizes.size > 1; + let sampleFlagsPresent = !firstSampleFlagsPresent && uniqueSampleFlags.size > 1; + let sampleCompositionTimeOffsetsPresent = uniqueSampleCompositionTimeOffsets.size > 1 || [...uniqueSampleCompositionTimeOffsets].some((x) => x !== 0); + let flags = 0; + flags |= 1; + flags |= 4 * +firstSampleFlagsPresent; + flags |= 256 * +sampleDurationPresent; + flags |= 512 * +sampleSizePresent; + flags |= 1024 * +sampleFlagsPresent; + flags |= 2048 * +sampleCompositionTimeOffsetsPresent; + return fullBox("trun", 1, flags, [ + u32(track.currentChunk.samples.length), + // Sample count + u32(track.currentChunk.offset - track.currentChunk.moofOffset || 0), + // Data offset + firstSampleFlagsPresent ? u32(allSampleFlags[0]) : [], + track.currentChunk.samples.map((_, i) => [ + sampleDurationPresent ? u32(allSampleDurations[i]) : [], + // Sample duration + sampleSizePresent ? u32(allSampleSizes[i]) : [], + // Sample size + sampleFlagsPresent ? u32(allSampleFlags[i]) : [], + // Sample flags + // Sample composition time offsets + sampleCompositionTimeOffsetsPresent ? i32(allSampleCompositionTimeOffsets[i]) : [] + ]) + ]); + }; + var mfra = (tracks) => { + return box("mfra", null, [ + ...tracks.map(tfra), + mfro() + ]); + }; + var tfra = (track, trackIndex) => { + let version = 1; + return fullBox("tfra", version, 0, [ + u32(track.id), + // Track ID + u32(63), + // This specifies that traf number, trun number and sample number are 32-bit ints + u32(track.finalizedChunks.length), + // Number of entries + track.finalizedChunks.map((chunk) => [ + u64(intoTimescale(chunk.startTimestamp, track.timescale)), + // Time + u64(chunk.moofOffset), + // moof offset + u32(trackIndex + 1), + // traf number + u32(1), + // trun number + u32(1) + // Sample number + ]) + ]); + }; + var mfro = () => { + return fullBox("mfro", 0, 0, [ + // This value needs to be overwritten manually from the outside, where the actual size of the enclosing mfra box + // is known + u32(0) + // Size + ]); + }; + var VIDEO_CODEC_TO_BOX_NAME = { + "avc": "avc1", + "hevc": "hvc1", + "vp9": "vp09", + "av1": "av01" + }; + var VIDEO_CODEC_TO_CONFIGURATION_BOX = { + "avc": avcC, + "hevc": hvcC, + "vp9": vpcC, + "av1": av1C + }; + var AUDIO_CODEC_TO_BOX_NAME = { + "aac": "mp4a", + "opus": "Opus" + }; + var AUDIO_CODEC_TO_CONFIGURATION_BOX = { + "aac": esds, + "opus": dOps + }; + + // src/target.ts + var isTarget = Symbol("isTarget"); + var Target = class { + }; + isTarget; + var ArrayBufferTarget = class extends Target { + constructor() { + super(...arguments); + this.buffer = null; + } + }; + var StreamTarget = class extends Target { + constructor(options) { + super(); + this.options = options; + if (typeof options !== "object") { + throw new TypeError("StreamTarget requires an options object to be passed to its constructor."); + } + if (options.onData) { + if (typeof options.onData !== "function") { + throw new TypeError("options.onData, when provided, must be a function."); + } + if (options.onData.length < 2) { + throw new TypeError( + "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." + ); + } + } + if (options.chunked !== void 0 && typeof options.chunked !== "boolean") { + throw new TypeError("options.chunked, when provided, must be a boolean."); + } + if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize < 1024)) { + throw new TypeError("options.chunkSize, when provided, must be an integer and not smaller than 1024."); + } + } + }; + var FileSystemWritableFileStreamTarget = class extends Target { + constructor(stream, options) { + super(); + this.stream = stream; + this.options = options; + if (!(stream instanceof FileSystemWritableFileStream)) { + throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance."); + } + if (options !== void 0 && typeof options !== "object") { + throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object."); + } + if (options) { + if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize <= 0)) { + throw new TypeError("options.chunkSize, when provided, must be a positive integer"); + } + } + } + }; + + // src/writer.ts + var _helper, _helperView; + var Writer = class { + constructor() { + this.pos = 0; + __privateAdd(this, _helper, new Uint8Array(8)); + __privateAdd(this, _helperView, new DataView(__privateGet(this, _helper).buffer)); + /** + * Stores the position from the start of the file to where boxes elements have been written. This is used to + * rewrite/edit elements that were already added before, and to measure sizes of things. + */ + this.offsets = /* @__PURE__ */ new WeakMap(); + } + /** Sets the current position for future writes to a new one. */ + seek(newPos) { + this.pos = newPos; + } + writeU32(value) { + __privateGet(this, _helperView).setUint32(0, value, false); + this.write(__privateGet(this, _helper).subarray(0, 4)); + } + writeU64(value) { + __privateGet(this, _helperView).setUint32(0, Math.floor(value / 2 ** 32), false); + __privateGet(this, _helperView).setUint32(4, value, false); + this.write(__privateGet(this, _helper).subarray(0, 8)); + } + writeAscii(text) { + for (let i = 0; i < text.length; i++) { + __privateGet(this, _helperView).setUint8(i % 8, text.charCodeAt(i)); + if (i % 8 === 7) + this.write(__privateGet(this, _helper)); + } + if (text.length % 8 !== 0) { + this.write(__privateGet(this, _helper).subarray(0, text.length % 8)); + } + } + writeBox(box2) { + this.offsets.set(box2, this.pos); + if (box2.contents && !box2.children) { + this.writeBoxHeader(box2, box2.size ?? box2.contents.byteLength + 8); + this.write(box2.contents); + } else { + let startPos = this.pos; + this.writeBoxHeader(box2, 0); + if (box2.contents) + this.write(box2.contents); + if (box2.children) { + for (let child of box2.children) + if (child) + this.writeBox(child); + } + let endPos = this.pos; + let size = box2.size ?? endPos - startPos; + this.seek(startPos); + this.writeBoxHeader(box2, size); + this.seek(endPos); + } + } + writeBoxHeader(box2, size) { + this.writeU32(box2.largeSize ? 1 : size); + this.writeAscii(box2.type); + if (box2.largeSize) + this.writeU64(size); + } + measureBoxHeader(box2) { + return 8 + (box2.largeSize ? 8 : 0); + } + patchBox(box2) { + let endPos = this.pos; + this.seek(this.offsets.get(box2)); + this.writeBox(box2); + this.seek(endPos); + } + measureBox(box2) { + if (box2.contents && !box2.children) { + let headerSize = this.measureBoxHeader(box2); + return headerSize + box2.contents.byteLength; + } else { + let result = this.measureBoxHeader(box2); + if (box2.contents) + result += box2.contents.byteLength; + if (box2.children) { + for (let child of box2.children) + if (child) + result += this.measureBox(child); + } + return result; + } + } + }; + _helper = new WeakMap(); + _helperView = new WeakMap(); + var _target, _buffer, _bytes, _maxPos, _ensureSize, ensureSize_fn; + var ArrayBufferTargetWriter = class extends Writer { + constructor(target) { + super(); + __privateAdd(this, _ensureSize); + __privateAdd(this, _target, void 0); + __privateAdd(this, _buffer, new ArrayBuffer(2 ** 16)); + __privateAdd(this, _bytes, new Uint8Array(__privateGet(this, _buffer))); + __privateAdd(this, _maxPos, 0); + __privateSet(this, _target, target); + } + write(data) { + __privateMethod(this, _ensureSize, ensureSize_fn).call(this, this.pos + data.byteLength); + __privateGet(this, _bytes).set(data, this.pos); + this.pos += data.byteLength; + __privateSet(this, _maxPos, Math.max(__privateGet(this, _maxPos), this.pos)); + } + finalize() { + __privateMethod(this, _ensureSize, ensureSize_fn).call(this, this.pos); + __privateGet(this, _target).buffer = __privateGet(this, _buffer).slice(0, Math.max(__privateGet(this, _maxPos), this.pos)); + } + }; + _target = new WeakMap(); + _buffer = new WeakMap(); + _bytes = new WeakMap(); + _maxPos = new WeakMap(); + _ensureSize = new WeakSet(); + ensureSize_fn = function(size) { + let newLength = __privateGet(this, _buffer).byteLength; + while (newLength < size) + newLength *= 2; + if (newLength === __privateGet(this, _buffer).byteLength) + return; + let newBuffer = new ArrayBuffer(newLength); + let newBytes = new Uint8Array(newBuffer); + newBytes.set(__privateGet(this, _bytes), 0); + __privateSet(this, _buffer, newBuffer); + __privateSet(this, _bytes, newBytes); + }; + var DEFAULT_CHUNK_SIZE = 2 ** 24; + var MAX_CHUNKS_AT_ONCE = 2; + var _target2, _sections, _chunked, _chunkSize, _chunks, _writeDataIntoChunks, writeDataIntoChunks_fn, _insertSectionIntoChunk, insertSectionIntoChunk_fn, _createChunk, createChunk_fn, _flushChunks, flushChunks_fn; + var StreamTargetWriter = class extends Writer { + constructor(target) { + super(); + __privateAdd(this, _writeDataIntoChunks); + __privateAdd(this, _insertSectionIntoChunk); + __privateAdd(this, _createChunk); + __privateAdd(this, _flushChunks); + __privateAdd(this, _target2, void 0); + __privateAdd(this, _sections, []); + __privateAdd(this, _chunked, void 0); + __privateAdd(this, _chunkSize, void 0); + /** + * The data is divided up into fixed-size chunks, whose contents are first filled in RAM and then flushed out. + * A chunk is flushed if all of its contents have been written. + */ + __privateAdd(this, _chunks, []); + __privateSet(this, _target2, target); + __privateSet(this, _chunked, target.options?.chunked ?? false); + __privateSet(this, _chunkSize, target.options?.chunkSize ?? DEFAULT_CHUNK_SIZE); + } + write(data) { + __privateGet(this, _sections).push({ + data: data.slice(), + start: this.pos + }); + this.pos += data.byteLength; + } + flush() { + if (__privateGet(this, _sections).length === 0) + return; + let chunks = []; + let sorted = [...__privateGet(this, _sections)].sort((a, b) => a.start - b.start); + chunks.push({ + start: sorted[0].start, + size: sorted[0].data.byteLength + }); + for (let i = 1; i < sorted.length; i++) { + let lastChunk = chunks[chunks.length - 1]; + let section = sorted[i]; + if (section.start <= lastChunk.start + lastChunk.size) { + lastChunk.size = Math.max(lastChunk.size, section.start + section.data.byteLength - lastChunk.start); + } else { + chunks.push({ + start: section.start, + size: section.data.byteLength + }); + } + } + for (let chunk of chunks) { + chunk.data = new Uint8Array(chunk.size); + for (let section of __privateGet(this, _sections)) { + if (chunk.start <= section.start && section.start < chunk.start + chunk.size) { + chunk.data.set(section.data, section.start - chunk.start); + } + } + if (__privateGet(this, _chunked)) { + __privateMethod(this, _writeDataIntoChunks, writeDataIntoChunks_fn).call(this, chunk.data, chunk.start); + __privateMethod(this, _flushChunks, flushChunks_fn).call(this); + } else { + __privateGet(this, _target2).options.onData?.(chunk.data, chunk.start); + } + } + __privateGet(this, _sections).length = 0; + } + finalize() { + if (__privateGet(this, _chunked)) { + __privateMethod(this, _flushChunks, flushChunks_fn).call(this, true); + } + } + }; + _target2 = new WeakMap(); + _sections = new WeakMap(); + _chunked = new WeakMap(); + _chunkSize = new WeakMap(); + _chunks = new WeakMap(); + _writeDataIntoChunks = new WeakSet(); + writeDataIntoChunks_fn = function(data, position) { + let chunkIndex = __privateGet(this, _chunks).findIndex((x) => x.start <= position && position < x.start + __privateGet(this, _chunkSize)); + if (chunkIndex === -1) + chunkIndex = __privateMethod(this, _createChunk, createChunk_fn).call(this, position); + let chunk = __privateGet(this, _chunks)[chunkIndex]; + let relativePosition = position - chunk.start; + let toWrite = data.subarray(0, Math.min(__privateGet(this, _chunkSize) - relativePosition, data.byteLength)); + chunk.data.set(toWrite, relativePosition); + let section = { + start: relativePosition, + end: relativePosition + toWrite.byteLength + }; + __privateMethod(this, _insertSectionIntoChunk, insertSectionIntoChunk_fn).call(this, chunk, section); + if (chunk.written[0].start === 0 && chunk.written[0].end === __privateGet(this, _chunkSize)) { + chunk.shouldFlush = true; + } + if (__privateGet(this, _chunks).length > MAX_CHUNKS_AT_ONCE) { + for (let i = 0; i < __privateGet(this, _chunks).length - 1; i++) { + __privateGet(this, _chunks)[i].shouldFlush = true; + } + __privateMethod(this, _flushChunks, flushChunks_fn).call(this); + } + if (toWrite.byteLength < data.byteLength) { + __privateMethod(this, _writeDataIntoChunks, writeDataIntoChunks_fn).call(this, data.subarray(toWrite.byteLength), position + toWrite.byteLength); + } + }; + _insertSectionIntoChunk = new WeakSet(); + insertSectionIntoChunk_fn = function(chunk, section) { + let low = 0; + let high = chunk.written.length - 1; + let index = -1; + while (low <= high) { + let mid = Math.floor(low + (high - low + 1) / 2); + if (chunk.written[mid].start <= section.start) { + low = mid + 1; + index = mid; + } else { + high = mid - 1; + } + } + chunk.written.splice(index + 1, 0, section); + if (index === -1 || chunk.written[index].end < section.start) + index++; + while (index < chunk.written.length - 1 && chunk.written[index].end >= chunk.written[index + 1].start) { + chunk.written[index].end = Math.max(chunk.written[index].end, chunk.written[index + 1].end); + chunk.written.splice(index + 1, 1); + } + }; + _createChunk = new WeakSet(); + createChunk_fn = function(includesPosition) { + let start = Math.floor(includesPosition / __privateGet(this, _chunkSize)) * __privateGet(this, _chunkSize); + let chunk = { + start, + data: new Uint8Array(__privateGet(this, _chunkSize)), + written: [], + shouldFlush: false + }; + __privateGet(this, _chunks).push(chunk); + __privateGet(this, _chunks).sort((a, b) => a.start - b.start); + return __privateGet(this, _chunks).indexOf(chunk); + }; + _flushChunks = new WeakSet(); + flushChunks_fn = function(force = false) { + for (let i = 0; i < __privateGet(this, _chunks).length; i++) { + let chunk = __privateGet(this, _chunks)[i]; + if (!chunk.shouldFlush && !force) + continue; + for (let section of chunk.written) { + __privateGet(this, _target2).options.onData?.( + chunk.data.subarray(section.start, section.end), + chunk.start + section.start + ); + } + __privateGet(this, _chunks).splice(i--, 1); + } + }; + var FileSystemWritableFileStreamTargetWriter = class extends StreamTargetWriter { + constructor(target) { + super(new StreamTarget({ + onData: (data, position) => target.stream.write({ + type: "write", + data, + position + }), + chunked: true, + chunkSize: target.options?.chunkSize + })); + } + }; + + // src/muxer.ts + var GLOBAL_TIMESCALE = 1e3; + var SUPPORTED_VIDEO_CODECS = ["avc", "hevc", "vp9", "av1"]; + var SUPPORTED_AUDIO_CODECS = ["aac", "opus"]; + var TIMESTAMP_OFFSET = 2082844800; + var FIRST_TIMESTAMP_BEHAVIORS = ["strict", "offset", "cross-track-offset"]; + 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; + var Muxer = class { + constructor(options) { + __privateAdd(this, _validateOptions); + __privateAdd(this, _writeHeader); + __privateAdd(this, _computeMoovSizeUpperBound); + __privateAdd(this, _prepareTracks); + // https://wiki.multimedia.cx/index.php/MPEG-4_Audio + __privateAdd(this, _generateMpeg4AudioSpecificConfig); + __privateAdd(this, _createSampleForTrack); + __privateAdd(this, _addSampleToTrack); + __privateAdd(this, _validateTimestamp); + __privateAdd(this, _finalizeCurrentChunk); + __privateAdd(this, _finalizeFragment); + __privateAdd(this, _maybeFlushStreamingTargetWriter); + __privateAdd(this, _ensureNotFinalized); + __privateAdd(this, _options, void 0); + __privateAdd(this, _writer, void 0); + __privateAdd(this, _ftypSize, void 0); + __privateAdd(this, _mdat, void 0); + __privateAdd(this, _videoTrack, null); + __privateAdd(this, _audioTrack, null); + __privateAdd(this, _creationTime, Math.floor(Date.now() / 1e3) + TIMESTAMP_OFFSET); + __privateAdd(this, _finalizedChunks, []); + // Fields for fragmented MP4: + __privateAdd(this, _nextFragmentNumber, 1); + __privateAdd(this, _videoSampleQueue, []); + __privateAdd(this, _audioSampleQueue, []); + __privateAdd(this, _finalized, false); + __privateMethod(this, _validateOptions, validateOptions_fn).call(this, options); + options.video = deepClone(options.video); + options.audio = deepClone(options.audio); + options.fastStart = deepClone(options.fastStart); + this.target = options.target; + __privateSet(this, _options, { + firstTimestampBehavior: "strict", + ...options + }); + if (options.target instanceof ArrayBufferTarget) { + __privateSet(this, _writer, new ArrayBufferTargetWriter(options.target)); + } else if (options.target instanceof StreamTarget) { + __privateSet(this, _writer, new StreamTargetWriter(options.target)); + } else if (options.target instanceof FileSystemWritableFileStreamTarget) { + __privateSet(this, _writer, new FileSystemWritableFileStreamTargetWriter(options.target)); + } else { + throw new Error(`Invalid target: ${options.target}`); + } + __privateMethod(this, _prepareTracks, prepareTracks_fn).call(this); + __privateMethod(this, _writeHeader, writeHeader_fn).call(this); + } + addVideoChunk(sample, meta, timestamp, compositionTimeOffset) { + if (!(sample instanceof EncodedVideoChunk)) { + throw new TypeError("addVideoChunk's first argument (sample) must be of type EncodedVideoChunk."); + } + if (meta && typeof meta !== "object") { + throw new TypeError("addVideoChunk's second argument (meta), when provided, must be an object."); + } + if (timestamp !== void 0 && (!Number.isFinite(timestamp) || timestamp < 0)) { + throw new TypeError( + "addVideoChunk's third argument (timestamp), when provided, must be a non-negative real number." + ); + } + if (compositionTimeOffset !== void 0 && !Number.isFinite(compositionTimeOffset)) { + throw new TypeError( + "addVideoChunk's fourth argument (compositionTimeOffset), when provided, must be a real number." + ); + } + let data = new Uint8Array(sample.byteLength); + sample.copyTo(data); + this.addVideoChunkRaw( + data, + sample.type, + timestamp ?? sample.timestamp, + sample.duration, + meta, + compositionTimeOffset + ); + } + addVideoChunkRaw(data, type, timestamp, duration, meta, compositionTimeOffset) { + if (!(data instanceof Uint8Array)) { + throw new TypeError("addVideoChunkRaw's first argument (data) must be an instance of Uint8Array."); + } + if (type !== "key" && type !== "delta") { + throw new TypeError("addVideoChunkRaw's second argument (type) must be either 'key' or 'delta'."); + } + if (!Number.isFinite(timestamp) || timestamp < 0) { + throw new TypeError("addVideoChunkRaw's third argument (timestamp) must be a non-negative real number."); + } + if (!Number.isFinite(duration) || duration < 0) { + throw new TypeError("addVideoChunkRaw's fourth argument (duration) must be a non-negative real number."); + } + if (meta && typeof meta !== "object") { + throw new TypeError("addVideoChunkRaw's fifth argument (meta), when provided, must be an object."); + } + if (compositionTimeOffset !== void 0 && !Number.isFinite(compositionTimeOffset)) { + throw new TypeError( + "addVideoChunkRaw's sixth argument (compositionTimeOffset), when provided, must be a real number." + ); + } + __privateMethod(this, _ensureNotFinalized, ensureNotFinalized_fn).call(this); + if (!__privateGet(this, _options).video) + throw new Error("No video track declared."); + if (typeof __privateGet(this, _options).fastStart === "object" && __privateGet(this, _videoTrack).samples.length === __privateGet(this, _options).fastStart.expectedVideoChunks) { + throw new Error(`Cannot add more video chunks than specified in 'fastStart' (${__privateGet(this, _options).fastStart.expectedVideoChunks}).`); + } + let videoSample = __privateMethod(this, _createSampleForTrack, createSampleForTrack_fn).call(this, __privateGet(this, _videoTrack), data, type, timestamp, duration, meta, compositionTimeOffset); + if (__privateGet(this, _options).fastStart === "fragmented" && __privateGet(this, _audioTrack)) { + while (__privateGet(this, _audioSampleQueue).length > 0 && __privateGet(this, _audioSampleQueue)[0].decodeTimestamp <= videoSample.decodeTimestamp) { + let audioSample = __privateGet(this, _audioSampleQueue).shift(); + __privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample); + } + if (videoSample.decodeTimestamp <= __privateGet(this, _audioTrack).lastDecodeTimestamp) { + __privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample); + } else { + __privateGet(this, _videoSampleQueue).push(videoSample); + } + } else { + __privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample); + } + } + addAudioChunk(sample, meta, timestamp) { + if (!(sample instanceof EncodedAudioChunk)) { + throw new TypeError("addAudioChunk's first argument (sample) must be of type EncodedAudioChunk."); + } + if (meta && typeof meta !== "object") { + throw new TypeError("addAudioChunk's second argument (meta), when provided, must be an object."); + } + if (timestamp !== void 0 && (!Number.isFinite(timestamp) || timestamp < 0)) { + throw new TypeError( + "addAudioChunk's third argument (timestamp), when provided, must be a non-negative real number." + ); + } + let data = new Uint8Array(sample.byteLength); + sample.copyTo(data); + this.addAudioChunkRaw(data, sample.type, timestamp ?? sample.timestamp, sample.duration, meta); + } + addAudioChunkRaw(data, type, timestamp, duration, meta) { + if (!(data instanceof Uint8Array)) { + throw new TypeError("addAudioChunkRaw's first argument (data) must be an instance of Uint8Array."); + } + if (type !== "key" && type !== "delta") { + throw new TypeError("addAudioChunkRaw's second argument (type) must be either 'key' or 'delta'."); + } + if (!Number.isFinite(timestamp) || timestamp < 0) { + throw new TypeError("addAudioChunkRaw's third argument (timestamp) must be a non-negative real number."); + } + if (!Number.isFinite(duration) || duration < 0) { + throw new TypeError("addAudioChunkRaw's fourth argument (duration) must be a non-negative real number."); + } + if (meta && typeof meta !== "object") { + throw new TypeError("addAudioChunkRaw's fifth argument (meta), when provided, must be an object."); + } + __privateMethod(this, _ensureNotFinalized, ensureNotFinalized_fn).call(this); + if (!__privateGet(this, _options).audio) + throw new Error("No audio track declared."); + if (typeof __privateGet(this, _options).fastStart === "object" && __privateGet(this, _audioTrack).samples.length === __privateGet(this, _options).fastStart.expectedAudioChunks) { + throw new Error(`Cannot add more audio chunks than specified in 'fastStart' (${__privateGet(this, _options).fastStart.expectedAudioChunks}).`); + } + let audioSample = __privateMethod(this, _createSampleForTrack, createSampleForTrack_fn).call(this, __privateGet(this, _audioTrack), data, type, timestamp, duration, meta); + if (__privateGet(this, _options).fastStart === "fragmented" && __privateGet(this, _videoTrack)) { + while (__privateGet(this, _videoSampleQueue).length > 0 && __privateGet(this, _videoSampleQueue)[0].decodeTimestamp <= audioSample.decodeTimestamp) { + let videoSample = __privateGet(this, _videoSampleQueue).shift(); + __privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample); + } + if (audioSample.decodeTimestamp <= __privateGet(this, _videoTrack).lastDecodeTimestamp) { + __privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample); + } else { + __privateGet(this, _audioSampleQueue).push(audioSample); + } + } else { + __privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample); + } + } + /** Finalizes the file, making it ready for use. Must be called after all video and audio chunks have been added. */ + finalize() { + if (__privateGet(this, _finalized)) { + throw new Error("Cannot finalize a muxer more than once."); + } + if (__privateGet(this, _options).fastStart === "fragmented") { + for (let videoSample of __privateGet(this, _videoSampleQueue)) + __privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _videoTrack), videoSample); + for (let audioSample of __privateGet(this, _audioSampleQueue)) + __privateMethod(this, _addSampleToTrack, addSampleToTrack_fn).call(this, __privateGet(this, _audioTrack), audioSample); + __privateMethod(this, _finalizeFragment, finalizeFragment_fn).call(this, false); + } else { + if (__privateGet(this, _videoTrack)) + __privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, __privateGet(this, _videoTrack)); + if (__privateGet(this, _audioTrack)) + __privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, __privateGet(this, _audioTrack)); + } + let tracks = [__privateGet(this, _videoTrack), __privateGet(this, _audioTrack)].filter(Boolean); + if (__privateGet(this, _options).fastStart === "in-memory") { + let mdatSize; + for (let i = 0; i < 2; i++) { + let movieBox2 = moov(tracks, __privateGet(this, _creationTime)); + let movieBoxSize = __privateGet(this, _writer).measureBox(movieBox2); + mdatSize = __privateGet(this, _writer).measureBox(__privateGet(this, _mdat)); + let currentChunkPos = __privateGet(this, _writer).pos + movieBoxSize + mdatSize; + for (let chunk of __privateGet(this, _finalizedChunks)) { + chunk.offset = currentChunkPos; + for (let { data } of chunk.samples) { + currentChunkPos += data.byteLength; + mdatSize += data.byteLength; + } + } + if (currentChunkPos < 2 ** 32) + break; + if (mdatSize >= 2 ** 32) + __privateGet(this, _mdat).largeSize = true; + } + let movieBox = moov(tracks, __privateGet(this, _creationTime)); + __privateGet(this, _writer).writeBox(movieBox); + __privateGet(this, _mdat).size = mdatSize; + __privateGet(this, _writer).writeBox(__privateGet(this, _mdat)); + for (let chunk of __privateGet(this, _finalizedChunks)) { + for (let sample of chunk.samples) { + __privateGet(this, _writer).write(sample.data); + sample.data = null; + } + } + } else if (__privateGet(this, _options).fastStart === "fragmented") { + let startPos = __privateGet(this, _writer).pos; + let mfraBox = mfra(tracks); + __privateGet(this, _writer).writeBox(mfraBox); + let mfraBoxSize = __privateGet(this, _writer).pos - startPos; + __privateGet(this, _writer).seek(__privateGet(this, _writer).pos - 4); + __privateGet(this, _writer).writeU32(mfraBoxSize); + } else { + let mdatPos = __privateGet(this, _writer).offsets.get(__privateGet(this, _mdat)); + let mdatSize = __privateGet(this, _writer).pos - mdatPos; + __privateGet(this, _mdat).size = mdatSize; + __privateGet(this, _mdat).largeSize = mdatSize >= 2 ** 32; + __privateGet(this, _writer).patchBox(__privateGet(this, _mdat)); + let movieBox = moov(tracks, __privateGet(this, _creationTime)); + if (typeof __privateGet(this, _options).fastStart === "object") { + __privateGet(this, _writer).seek(__privateGet(this, _ftypSize)); + __privateGet(this, _writer).writeBox(movieBox); + let remainingBytes = mdatPos - __privateGet(this, _writer).pos; + __privateGet(this, _writer).writeBox(free(remainingBytes)); + } else { + __privateGet(this, _writer).writeBox(movieBox); + } + } + __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); + __privateGet(this, _writer).finalize(); + __privateSet(this, _finalized, true); + } + }; + _options = new WeakMap(); + _writer = new WeakMap(); + _ftypSize = new WeakMap(); + _mdat = new WeakMap(); + _videoTrack = new WeakMap(); + _audioTrack = new WeakMap(); + _creationTime = new WeakMap(); + _finalizedChunks = new WeakMap(); + _nextFragmentNumber = new WeakMap(); + _videoSampleQueue = new WeakMap(); + _audioSampleQueue = new WeakMap(); + _finalized = new WeakMap(); + _validateOptions = new WeakSet(); + validateOptions_fn = function(options) { + if (typeof options !== "object") { + throw new TypeError("The muxer requires an options object to be passed to its constructor."); + } + if (!(options.target instanceof Target)) { + throw new TypeError("The target must be provided and an instance of Target."); + } + if (options.video) { + if (!SUPPORTED_VIDEO_CODECS.includes(options.video.codec)) { + throw new TypeError(`Unsupported video codec: ${options.video.codec}`); + } + if (!Number.isInteger(options.video.width) || options.video.width <= 0) { + throw new TypeError(`Invalid video width: ${options.video.width}. Must be a positive integer.`); + } + if (!Number.isInteger(options.video.height) || options.video.height <= 0) { + throw new TypeError(`Invalid video height: ${options.video.height}. Must be a positive integer.`); + } + const videoRotation = options.video.rotation; + if (typeof videoRotation === "number" && ![0, 90, 180, 270].includes(videoRotation)) { + throw new TypeError(`Invalid video rotation: ${videoRotation}. Has to be 0, 90, 180 or 270.`); + } else if (Array.isArray(videoRotation) && (videoRotation.length !== 9 || videoRotation.some((value) => typeof value !== "number"))) { + throw new TypeError(`Invalid video transformation matrix: ${videoRotation.join()}`); + } + if (options.video.frameRate !== void 0 && (!Number.isInteger(options.video.frameRate) || options.video.frameRate <= 0)) { + throw new TypeError( + `Invalid video frame rate: ${options.video.frameRate}. Must be a positive integer.` + ); + } + } + if (options.audio) { + if (!SUPPORTED_AUDIO_CODECS.includes(options.audio.codec)) { + throw new TypeError(`Unsupported audio codec: ${options.audio.codec}`); + } + if (!Number.isInteger(options.audio.numberOfChannels) || options.audio.numberOfChannels <= 0) { + throw new TypeError( + `Invalid number of audio channels: ${options.audio.numberOfChannels}. Must be a positive integer.` + ); + } + if (!Number.isInteger(options.audio.sampleRate) || options.audio.sampleRate <= 0) { + throw new TypeError( + `Invalid audio sample rate: ${options.audio.sampleRate}. Must be a positive integer.` + ); + } + } + if (options.firstTimestampBehavior && !FIRST_TIMESTAMP_BEHAVIORS.includes(options.firstTimestampBehavior)) { + throw new TypeError(`Invalid first timestamp behavior: ${options.firstTimestampBehavior}`); + } + if (typeof options.fastStart === "object") { + if (options.video) { + if (options.fastStart.expectedVideoChunks === void 0) { + throw new TypeError(`'fastStart' is an object but is missing property 'expectedVideoChunks'.`); + } else if (!Number.isInteger(options.fastStart.expectedVideoChunks) || options.fastStart.expectedVideoChunks < 0) { + throw new TypeError(`'expectedVideoChunks' must be a non-negative integer.`); + } + } + if (options.audio) { + if (options.fastStart.expectedAudioChunks === void 0) { + throw new TypeError(`'fastStart' is an object but is missing property 'expectedAudioChunks'.`); + } else if (!Number.isInteger(options.fastStart.expectedAudioChunks) || options.fastStart.expectedAudioChunks < 0) { + throw new TypeError(`'expectedAudioChunks' must be a non-negative integer.`); + } + } + } else if (![false, "in-memory", "fragmented"].includes(options.fastStart)) { + throw new TypeError(`'fastStart' option must be false, 'in-memory', 'fragmented' or an object.`); + } + if (options.minFragmentDuration !== void 0 && (!Number.isFinite(options.minFragmentDuration) || options.minFragmentDuration < 0)) { + throw new TypeError(`'minFragmentDuration' must be a non-negative number.`); + } + }; + _writeHeader = new WeakSet(); + writeHeader_fn = function() { + __privateGet(this, _writer).writeBox(ftyp({ + holdsAvc: __privateGet(this, _options).video?.codec === "avc", + fragmented: __privateGet(this, _options).fastStart === "fragmented" + })); + __privateSet(this, _ftypSize, __privateGet(this, _writer).pos); + if (__privateGet(this, _options).fastStart === "in-memory") { + __privateSet(this, _mdat, mdat(false)); + } else if (__privateGet(this, _options).fastStart === "fragmented") { + } else { + if (typeof __privateGet(this, _options).fastStart === "object") { + let moovSizeUpperBound = __privateMethod(this, _computeMoovSizeUpperBound, computeMoovSizeUpperBound_fn).call(this); + __privateGet(this, _writer).seek(__privateGet(this, _writer).pos + moovSizeUpperBound); + } + __privateSet(this, _mdat, mdat(true)); + __privateGet(this, _writer).writeBox(__privateGet(this, _mdat)); + } + __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); + }; + _computeMoovSizeUpperBound = new WeakSet(); + computeMoovSizeUpperBound_fn = function() { + if (typeof __privateGet(this, _options).fastStart !== "object") + return; + let upperBound = 0; + let sampleCounts = [ + __privateGet(this, _options).fastStart.expectedVideoChunks, + __privateGet(this, _options).fastStart.expectedAudioChunks + ]; + for (let n of sampleCounts) { + if (!n) + continue; + upperBound += (4 + 4) * Math.ceil(2 / 3 * n); + upperBound += 4 * n; + upperBound += (4 + 4 + 4) * Math.ceil(2 / 3 * n); + upperBound += 4 * n; + upperBound += 8 * n; + } + upperBound += 4096; + return upperBound; + }; + _prepareTracks = new WeakSet(); + prepareTracks_fn = function() { + if (__privateGet(this, _options).video) { + __privateSet(this, _videoTrack, { + id: 1, + info: { + type: "video", + codec: __privateGet(this, _options).video.codec, + width: __privateGet(this, _options).video.width, + height: __privateGet(this, _options).video.height, + rotation: __privateGet(this, _options).video.rotation ?? 0, + decoderConfig: null + }, + // The fallback contains many common frame rates as factors + timescale: __privateGet(this, _options).video.frameRate ?? 57600, + samples: [], + finalizedChunks: [], + currentChunk: null, + firstDecodeTimestamp: void 0, + lastDecodeTimestamp: -1, + timeToSampleTable: [], + compositionTimeOffsetTable: [], + lastTimescaleUnits: null, + lastSample: null, + compactlyCodedChunkTable: [] + }); + } + if (__privateGet(this, _options).audio) { + __privateSet(this, _audioTrack, { + id: __privateGet(this, _options).video ? 2 : 1, + info: { + type: "audio", + codec: __privateGet(this, _options).audio.codec, + numberOfChannels: __privateGet(this, _options).audio.numberOfChannels, + sampleRate: __privateGet(this, _options).audio.sampleRate, + decoderConfig: null + }, + timescale: __privateGet(this, _options).audio.sampleRate, + samples: [], + finalizedChunks: [], + currentChunk: null, + firstDecodeTimestamp: void 0, + lastDecodeTimestamp: -1, + timeToSampleTable: [], + compositionTimeOffsetTable: [], + lastTimescaleUnits: null, + lastSample: null, + compactlyCodedChunkTable: [] + }); + if (__privateGet(this, _options).audio.codec === "aac") { + let guessedCodecPrivate = __privateMethod(this, _generateMpeg4AudioSpecificConfig, generateMpeg4AudioSpecificConfig_fn).call( + this, + 2, + // Object type for AAC-LC, since it's the most common + __privateGet(this, _options).audio.sampleRate, + __privateGet(this, _options).audio.numberOfChannels + ); + __privateGet(this, _audioTrack).info.decoderConfig = { + codec: __privateGet(this, _options).audio.codec, + description: guessedCodecPrivate, + numberOfChannels: __privateGet(this, _options).audio.numberOfChannels, + sampleRate: __privateGet(this, _options).audio.sampleRate + }; + } + } + }; + _generateMpeg4AudioSpecificConfig = new WeakSet(); + generateMpeg4AudioSpecificConfig_fn = function(objectType, sampleRate, numberOfChannels) { + let frequencyIndices = [96e3, 88200, 64e3, 48e3, 44100, 32e3, 24e3, 22050, 16e3, 12e3, 11025, 8e3, 7350]; + let frequencyIndex = frequencyIndices.indexOf(sampleRate); + let channelConfig = numberOfChannels; + let configBits = ""; + configBits += objectType.toString(2).padStart(5, "0"); + configBits += frequencyIndex.toString(2).padStart(4, "0"); + if (frequencyIndex === 15) + configBits += sampleRate.toString(2).padStart(24, "0"); + configBits += channelConfig.toString(2).padStart(4, "0"); + let paddingLength = Math.ceil(configBits.length / 8) * 8; + configBits = configBits.padEnd(paddingLength, "0"); + let configBytes = new Uint8Array(configBits.length / 8); + for (let i = 0; i < configBits.length; i += 8) { + configBytes[i / 8] = parseInt(configBits.slice(i, i + 8), 2); + } + return configBytes; + }; + _createSampleForTrack = new WeakSet(); + createSampleForTrack_fn = function(track, data, type, timestamp, duration, meta, compositionTimeOffset) { + let presentationTimestampInSeconds = timestamp / 1e6; + let decodeTimestampInSeconds = (timestamp - (compositionTimeOffset ?? 0)) / 1e6; + let durationInSeconds = duration / 1e6; + let adjusted = __privateMethod(this, _validateTimestamp, validateTimestamp_fn).call(this, presentationTimestampInSeconds, decodeTimestampInSeconds, track); + presentationTimestampInSeconds = adjusted.presentationTimestamp; + decodeTimestampInSeconds = adjusted.decodeTimestamp; + if (meta?.decoderConfig) { + if (track.info.decoderConfig === null) { + track.info.decoderConfig = meta.decoderConfig; + } else { + Object.assign(track.info.decoderConfig, meta.decoderConfig); + } + } + let sample = { + presentationTimestamp: presentationTimestampInSeconds, + decodeTimestamp: decodeTimestampInSeconds, + duration: durationInSeconds, + data, + size: data.byteLength, + type, + // Will be refined once the next sample comes in + timescaleUnitsToNextSample: intoTimescale(durationInSeconds, track.timescale) + }; + return sample; + }; + _addSampleToTrack = new WeakSet(); + addSampleToTrack_fn = function(track, sample) { + if (__privateGet(this, _options).fastStart !== "fragmented") { + track.samples.push(sample); + } + const sampleCompositionTimeOffset = intoTimescale(sample.presentationTimestamp - sample.decodeTimestamp, track.timescale); + if (track.lastTimescaleUnits !== null) { + let timescaleUnits = intoTimescale(sample.decodeTimestamp, track.timescale, false); + let delta = Math.round(timescaleUnits - track.lastTimescaleUnits); + track.lastTimescaleUnits += delta; + track.lastSample.timescaleUnitsToNextSample = delta; + if (__privateGet(this, _options).fastStart !== "fragmented") { + let lastTableEntry = last(track.timeToSampleTable); + if (lastTableEntry.sampleCount === 1) { + lastTableEntry.sampleDelta = delta; + lastTableEntry.sampleCount++; + } else if (lastTableEntry.sampleDelta === delta) { + lastTableEntry.sampleCount++; + } else { + lastTableEntry.sampleCount--; + track.timeToSampleTable.push({ + sampleCount: 2, + sampleDelta: delta + }); + } + const lastCompositionTimeOffsetTableEntry = last(track.compositionTimeOffsetTable); + if (lastCompositionTimeOffsetTableEntry.sampleCompositionTimeOffset === sampleCompositionTimeOffset) { + lastCompositionTimeOffsetTableEntry.sampleCount++; + } else { + track.compositionTimeOffsetTable.push({ + sampleCount: 1, + sampleCompositionTimeOffset + }); + } + } + } else { + track.lastTimescaleUnits = 0; + if (__privateGet(this, _options).fastStart !== "fragmented") { + track.timeToSampleTable.push({ + sampleCount: 1, + sampleDelta: intoTimescale(sample.duration, track.timescale) + }); + track.compositionTimeOffsetTable.push({ + sampleCount: 1, + sampleCompositionTimeOffset + }); + } + } + track.lastSample = sample; + let beginNewChunk = false; + if (!track.currentChunk) { + beginNewChunk = true; + } else { + let currentChunkDuration = sample.presentationTimestamp - track.currentChunk.startTimestamp; + if (__privateGet(this, _options).fastStart === "fragmented") { + let mostImportantTrack = __privateGet(this, _videoTrack) ?? __privateGet(this, _audioTrack); + const chunkDuration = __privateGet(this, _options).minFragmentDuration ?? 1; + if (track === mostImportantTrack && sample.type === "key" && currentChunkDuration >= chunkDuration) { + beginNewChunk = true; + __privateMethod(this, _finalizeFragment, finalizeFragment_fn).call(this); + } + } else { + beginNewChunk = currentChunkDuration >= 0.5; + } + } + if (beginNewChunk) { + if (track.currentChunk) { + __privateMethod(this, _finalizeCurrentChunk, finalizeCurrentChunk_fn).call(this, track); + } + track.currentChunk = { + startTimestamp: sample.presentationTimestamp, + samples: [] + }; + } + track.currentChunk.samples.push(sample); + }; + _validateTimestamp = new WeakSet(); + validateTimestamp_fn = function(presentationTimestamp, decodeTimestamp, track) { + const strictTimestampBehavior = __privateGet(this, _options).firstTimestampBehavior === "strict"; + const noLastDecodeTimestamp = track.lastDecodeTimestamp === -1; + const timestampNonZero = decodeTimestamp !== 0; + if (strictTimestampBehavior && noLastDecodeTimestamp && timestampNonZero) { + throw new Error( + `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. + +If you want to offset all timestamps of a track such that the first one is zero, set firstTimestampBehavior: 'offset' in the options. +` + ); + } else if (__privateGet(this, _options).firstTimestampBehavior === "offset" || __privateGet(this, _options).firstTimestampBehavior === "cross-track-offset") { + if (track.firstDecodeTimestamp === void 0) { + track.firstDecodeTimestamp = decodeTimestamp; + } + let baseDecodeTimestamp; + if (__privateGet(this, _options).firstTimestampBehavior === "offset") { + baseDecodeTimestamp = track.firstDecodeTimestamp; + } else { + baseDecodeTimestamp = Math.min( + __privateGet(this, _videoTrack)?.firstDecodeTimestamp ?? Infinity, + __privateGet(this, _audioTrack)?.firstDecodeTimestamp ?? Infinity + ); + } + decodeTimestamp -= baseDecodeTimestamp; + presentationTimestamp -= baseDecodeTimestamp; + } + if (decodeTimestamp < track.lastDecodeTimestamp) { + throw new Error( + `Timestamps must be monotonically increasing (DTS went from ${track.lastDecodeTimestamp * 1e6} to ${decodeTimestamp * 1e6}).` + ); + } + track.lastDecodeTimestamp = decodeTimestamp; + return { presentationTimestamp, decodeTimestamp }; + }; + _finalizeCurrentChunk = new WeakSet(); + finalizeCurrentChunk_fn = function(track) { + if (__privateGet(this, _options).fastStart === "fragmented") { + throw new Error("Can't finalize individual chunks if 'fastStart' is set to 'fragmented'."); + } + if (!track.currentChunk) + return; + track.finalizedChunks.push(track.currentChunk); + __privateGet(this, _finalizedChunks).push(track.currentChunk); + if (track.compactlyCodedChunkTable.length === 0 || last(track.compactlyCodedChunkTable).samplesPerChunk !== track.currentChunk.samples.length) { + track.compactlyCodedChunkTable.push({ + firstChunk: track.finalizedChunks.length, + // 1-indexed + samplesPerChunk: track.currentChunk.samples.length + }); + } + if (__privateGet(this, _options).fastStart === "in-memory") { + track.currentChunk.offset = 0; + return; + } + track.currentChunk.offset = __privateGet(this, _writer).pos; + for (let sample of track.currentChunk.samples) { + __privateGet(this, _writer).write(sample.data); + sample.data = null; + } + __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); + }; + _finalizeFragment = new WeakSet(); + finalizeFragment_fn = function(flushStreamingWriter = true) { + if (__privateGet(this, _options).fastStart !== "fragmented") { + throw new Error("Can't finalize a fragment unless 'fastStart' is set to 'fragmented'."); + } + let tracks = [__privateGet(this, _videoTrack), __privateGet(this, _audioTrack)].filter((track) => track && track.currentChunk); + if (tracks.length === 0) + return; + let fragmentNumber = __privateWrapper(this, _nextFragmentNumber)._++; + if (fragmentNumber === 1) { + let movieBox = moov(tracks, __privateGet(this, _creationTime), true); + __privateGet(this, _writer).writeBox(movieBox); + } + let moofOffset = __privateGet(this, _writer).pos; + let moofBox = moof(fragmentNumber, tracks); + __privateGet(this, _writer).writeBox(moofBox); + { + let mdatBox = mdat(false); + let totalTrackSampleSize = 0; + for (let track of tracks) { + for (let sample of track.currentChunk.samples) { + totalTrackSampleSize += sample.size; + } + } + let mdatSize = __privateGet(this, _writer).measureBox(mdatBox) + totalTrackSampleSize; + if (mdatSize >= 2 ** 32) { + mdatBox.largeSize = true; + mdatSize = __privateGet(this, _writer).measureBox(mdatBox) + totalTrackSampleSize; + } + mdatBox.size = mdatSize; + __privateGet(this, _writer).writeBox(mdatBox); + } + for (let track of tracks) { + track.currentChunk.offset = __privateGet(this, _writer).pos; + track.currentChunk.moofOffset = moofOffset; + for (let sample of track.currentChunk.samples) { + __privateGet(this, _writer).write(sample.data); + sample.data = null; + } + } + let endPos = __privateGet(this, _writer).pos; + __privateGet(this, _writer).seek(__privateGet(this, _writer).offsets.get(moofBox)); + let newMoofBox = moof(fragmentNumber, tracks); + __privateGet(this, _writer).writeBox(newMoofBox); + __privateGet(this, _writer).seek(endPos); + for (let track of tracks) { + track.finalizedChunks.push(track.currentChunk); + __privateGet(this, _finalizedChunks).push(track.currentChunk); + track.currentChunk = null; + } + if (flushStreamingWriter) { + __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); + } + }; + _maybeFlushStreamingTargetWriter = new WeakSet(); + maybeFlushStreamingTargetWriter_fn = function() { + if (__privateGet(this, _writer) instanceof StreamTargetWriter) { + __privateGet(this, _writer).flush(); + } + }; + _ensureNotFinalized = new WeakSet(); + ensureNotFinalized_fn = function() { + if (__privateGet(this, _finalized)) { + throw new Error("Cannot add new video or audio chunks after the file has been finalized."); + } + }; + return __toCommonJS(src_exports); +})(); +if (typeof module === "object" && typeof module.exports === "object") Object.assign(module.exports, Mp4Muxer) diff --git a/public/style.css b/public/style.css index c66d0cb..6e8ce5b 100644 --- a/public/style.css +++ b/public/style.css @@ -1133,6 +1133,50 @@ body { color: var(--accent-cyan); } +/* Export Engine Badge */ +.export-engine-badge { + display: flex; + align-items: center; + gap: 12px; + background: rgba(0, 240, 255, 0.08); + border: 1px solid rgba(0, 240, 255, 0.3); + border-radius: var(--radius-md); + padding: 10px 14px; + margin-bottom: 12px; + transition: all 0.3s ease; +} + +.export-engine-badge.badge-server { + background: rgba(255, 170, 0, 0.08); + border-color: rgba(255, 170, 0, 0.3); +} + +.export-engine-badge .badge-icon { + font-size: 1.5rem; + line-height: 1; +} + +.export-engine-badge .badge-content { + display: flex; + flex-direction: column; + gap: 2px; +} + +.export-engine-badge strong { + font-size: 0.85rem; + color: var(--accent-cyan); +} + +.export-engine-badge.badge-server strong { + color: #ffaa00; +} + +.export-engine-badge span { + font-size: 0.75rem; + color: var(--text-muted); + line-height: 1.25; +} + /* Export Progress */ .export-progress-card { background: var(--bg-input); diff --git a/public/webm-muxer.js b/public/webm-muxer.js new file mode 100644 index 0000000..91999c1 --- /dev/null +++ b/public/webm-muxer.js @@ -0,0 +1,1475 @@ +"use strict"; +var WebMMuxer = (() => { + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + var __accessCheck = (obj, member, msg) => { + if (!member.has(obj)) + throw TypeError("Cannot " + msg); + }; + var __privateGet = (obj, member, getter) => { + __accessCheck(obj, member, "read from private field"); + return getter ? getter.call(obj) : member.get(obj); + }; + var __privateAdd = (obj, member, value) => { + if (member.has(obj)) + throw TypeError("Cannot add the same private member more than once"); + member instanceof WeakSet ? member.add(obj) : member.set(obj, value); + }; + var __privateSet = (obj, member, value, setter) => { + __accessCheck(obj, member, "write to private field"); + setter ? setter.call(obj, value) : member.set(obj, value); + return value; + }; + var __privateMethod = (obj, member, method) => { + __accessCheck(obj, member, "access private method"); + return method; + }; + + // src/index.ts + var src_exports = {}; + __export(src_exports, { + ArrayBufferTarget: () => ArrayBufferTarget, + FileSystemWritableFileStreamTarget: () => FileSystemWritableFileStreamTarget, + Muxer: () => Muxer, + StreamTarget: () => StreamTarget, + SubtitleEncoder: () => SubtitleEncoder + }); + + // src/ebml.ts + var EBMLFloat32 = class { + constructor(value) { + this.value = value; + } + }; + var EBMLFloat64 = class { + constructor(value) { + this.value = value; + } + }; + var measureUnsignedInt = (value) => { + if (value < 1 << 8) { + return 1; + } else if (value < 1 << 16) { + return 2; + } else if (value < 1 << 24) { + return 3; + } else if (value < 2 ** 32) { + return 4; + } else if (value < 2 ** 40) { + return 5; + } else { + return 6; + } + }; + var measureEBMLVarInt = (value) => { + if (value < (1 << 7) - 1) { + return 1; + } else if (value < (1 << 14) - 1) { + return 2; + } else if (value < (1 << 21) - 1) { + return 3; + } else if (value < (1 << 28) - 1) { + return 4; + } else if (value < 2 ** 35 - 1) { + return 5; + } else if (value < 2 ** 42 - 1) { + return 6; + } else { + throw new Error("EBML VINT size not supported " + value); + } + }; + + // src/misc.ts + var readBits = (bytes, start, end) => { + let result = 0; + for (let i = start; i < end; i++) { + let byteIndex = Math.floor(i / 8); + let byte = bytes[byteIndex]; + let bitIndex = 7 - (i & 7); + let bit = (byte & 1 << bitIndex) >> bitIndex; + result <<= 1; + result |= bit; + } + return result; + }; + var writeBits = (bytes, start, end, value) => { + for (let i = start; i < end; i++) { + let byteIndex = Math.floor(i / 8); + let byte = bytes[byteIndex]; + let bitIndex = 7 - (i & 7); + byte &= ~(1 << bitIndex); + byte |= (value & 1 << end - i - 1) >> end - i - 1 << bitIndex; + bytes[byteIndex] = byte; + } + }; + + // src/target.ts + var isTarget = Symbol("isTarget"); + var Target = class { + }; + isTarget; + var ArrayBufferTarget = class extends Target { + constructor() { + super(...arguments); + this.buffer = null; + } + }; + var StreamTarget = class extends Target { + constructor(options) { + super(); + this.options = options; + if (typeof options !== "object") { + throw new TypeError("StreamTarget requires an options object to be passed to its constructor."); + } + if (options.onData) { + if (typeof options.onData !== "function") { + throw new TypeError("options.onData, when provided, must be a function."); + } + if (options.onData.length < 2) { + throw new TypeError( + "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." + ); + } + } + if (options.onHeader && typeof options.onHeader !== "function") { + throw new TypeError("options.onHeader, when provided, must be a function."); + } + if (options.onCluster && typeof options.onCluster !== "function") { + throw new TypeError("options.onCluster, when provided, must be a function."); + } + if (options.chunked !== void 0 && typeof options.chunked !== "boolean") { + throw new TypeError("options.chunked, when provided, must be a boolean."); + } + if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize < 1024)) { + throw new TypeError("options.chunkSize, when provided, must be an integer and not smaller than 1024."); + } + } + }; + var FileSystemWritableFileStreamTarget = class extends Target { + constructor(stream, options) { + super(); + this.stream = stream; + this.options = options; + if (!(stream instanceof FileSystemWritableFileStream)) { + throw new TypeError("FileSystemWritableFileStreamTarget requires a FileSystemWritableFileStream instance."); + } + if (options !== void 0 && typeof options !== "object") { + throw new TypeError("FileSystemWritableFileStreamTarget's options, when provided, must be an object."); + } + if (options) { + if (options.chunkSize !== void 0 && (!Number.isInteger(options.chunkSize) || options.chunkSize <= 0)) { + throw new TypeError("options.chunkSize, when provided, must be a positive integer"); + } + } + } + }; + + // src/writer.ts + var _helper, _helperView, _writeByte, writeByte_fn, _writeFloat32, writeFloat32_fn, _writeFloat64, writeFloat64_fn, _writeUnsignedInt, writeUnsignedInt_fn, _writeString, writeString_fn; + var Writer = class { + constructor() { + __privateAdd(this, _writeByte); + __privateAdd(this, _writeFloat32); + __privateAdd(this, _writeFloat64); + __privateAdd(this, _writeUnsignedInt); + __privateAdd(this, _writeString); + this.pos = 0; + __privateAdd(this, _helper, new Uint8Array(8)); + __privateAdd(this, _helperView, new DataView(__privateGet(this, _helper).buffer)); + this.offsets = /* @__PURE__ */ new WeakMap(); + this.dataOffsets = /* @__PURE__ */ new WeakMap(); + } + seek(newPos) { + this.pos = newPos; + } + writeEBMLVarInt(value, width = measureEBMLVarInt(value)) { + let pos = 0; + switch (width) { + case 1: + __privateGet(this, _helperView).setUint8(pos++, 1 << 7 | value); + break; + case 2: + __privateGet(this, _helperView).setUint8(pos++, 1 << 6 | value >> 8); + __privateGet(this, _helperView).setUint8(pos++, value); + break; + case 3: + __privateGet(this, _helperView).setUint8(pos++, 1 << 5 | value >> 16); + __privateGet(this, _helperView).setUint8(pos++, value >> 8); + __privateGet(this, _helperView).setUint8(pos++, value); + break; + case 4: + __privateGet(this, _helperView).setUint8(pos++, 1 << 4 | value >> 24); + __privateGet(this, _helperView).setUint8(pos++, value >> 16); + __privateGet(this, _helperView).setUint8(pos++, value >> 8); + __privateGet(this, _helperView).setUint8(pos++, value); + break; + case 5: + __privateGet(this, _helperView).setUint8(pos++, 1 << 3 | value / 2 ** 32 & 7); + __privateGet(this, _helperView).setUint8(pos++, value >> 24); + __privateGet(this, _helperView).setUint8(pos++, value >> 16); + __privateGet(this, _helperView).setUint8(pos++, value >> 8); + __privateGet(this, _helperView).setUint8(pos++, value); + break; + case 6: + __privateGet(this, _helperView).setUint8(pos++, 1 << 2 | value / 2 ** 40 & 3); + __privateGet(this, _helperView).setUint8(pos++, value / 2 ** 32 | 0); + __privateGet(this, _helperView).setUint8(pos++, value >> 24); + __privateGet(this, _helperView).setUint8(pos++, value >> 16); + __privateGet(this, _helperView).setUint8(pos++, value >> 8); + __privateGet(this, _helperView).setUint8(pos++, value); + break; + default: + throw new Error("Bad EBML VINT size " + width); + } + this.write(__privateGet(this, _helper).subarray(0, pos)); + } + writeEBML(data) { + if (data === null) + return; + if (data instanceof Uint8Array) { + this.write(data); + } else if (Array.isArray(data)) { + for (let elem of data) { + this.writeEBML(elem); + } + } else { + this.offsets.set(data, this.pos); + __privateMethod(this, _writeUnsignedInt, writeUnsignedInt_fn).call(this, data.id); + if (Array.isArray(data.data)) { + let sizePos = this.pos; + let sizeSize = data.size === -1 ? 1 : data.size ?? 4; + if (data.size === -1) { + __privateMethod(this, _writeByte, writeByte_fn).call(this, 255); + } else { + this.seek(this.pos + sizeSize); + } + let startPos = this.pos; + this.dataOffsets.set(data, startPos); + this.writeEBML(data.data); + if (data.size !== -1) { + let size = this.pos - startPos; + let endPos = this.pos; + this.seek(sizePos); + this.writeEBMLVarInt(size, sizeSize); + this.seek(endPos); + } + } else if (typeof data.data === "number") { + let size = data.size ?? measureUnsignedInt(data.data); + this.writeEBMLVarInt(size); + __privateMethod(this, _writeUnsignedInt, writeUnsignedInt_fn).call(this, data.data, size); + } else if (typeof data.data === "string") { + this.writeEBMLVarInt(data.data.length); + __privateMethod(this, _writeString, writeString_fn).call(this, data.data); + } else if (data.data instanceof Uint8Array) { + this.writeEBMLVarInt(data.data.byteLength, data.size); + this.write(data.data); + } else if (data.data instanceof EBMLFloat32) { + this.writeEBMLVarInt(4); + __privateMethod(this, _writeFloat32, writeFloat32_fn).call(this, data.data.value); + } else if (data.data instanceof EBMLFloat64) { + this.writeEBMLVarInt(8); + __privateMethod(this, _writeFloat64, writeFloat64_fn).call(this, data.data.value); + } + } + } + }; + _helper = new WeakMap(); + _helperView = new WeakMap(); + _writeByte = new WeakSet(); + writeByte_fn = function(value) { + __privateGet(this, _helperView).setUint8(0, value); + this.write(__privateGet(this, _helper).subarray(0, 1)); + }; + _writeFloat32 = new WeakSet(); + writeFloat32_fn = function(value) { + __privateGet(this, _helperView).setFloat32(0, value, false); + this.write(__privateGet(this, _helper).subarray(0, 4)); + }; + _writeFloat64 = new WeakSet(); + writeFloat64_fn = function(value) { + __privateGet(this, _helperView).setFloat64(0, value, false); + this.write(__privateGet(this, _helper)); + }; + _writeUnsignedInt = new WeakSet(); + writeUnsignedInt_fn = function(value, width = measureUnsignedInt(value)) { + let pos = 0; + switch (width) { + case 6: + __privateGet(this, _helperView).setUint8(pos++, value / 2 ** 40 | 0); + case 5: + __privateGet(this, _helperView).setUint8(pos++, value / 2 ** 32 | 0); + case 4: + __privateGet(this, _helperView).setUint8(pos++, value >> 24); + case 3: + __privateGet(this, _helperView).setUint8(pos++, value >> 16); + case 2: + __privateGet(this, _helperView).setUint8(pos++, value >> 8); + case 1: + __privateGet(this, _helperView).setUint8(pos++, value); + break; + default: + throw new Error("Bad UINT size " + width); + } + this.write(__privateGet(this, _helper).subarray(0, pos)); + }; + _writeString = new WeakSet(); + writeString_fn = function(str) { + this.write(new Uint8Array(str.split("").map((x) => x.charCodeAt(0)))); + }; + var _target, _buffer, _bytes, _ensureSize, ensureSize_fn; + var ArrayBufferTargetWriter = class extends Writer { + constructor(target) { + super(); + __privateAdd(this, _ensureSize); + __privateAdd(this, _target, void 0); + __privateAdd(this, _buffer, new ArrayBuffer(2 ** 16)); + __privateAdd(this, _bytes, new Uint8Array(__privateGet(this, _buffer))); + __privateSet(this, _target, target); + } + write(data) { + __privateMethod(this, _ensureSize, ensureSize_fn).call(this, this.pos + data.byteLength); + __privateGet(this, _bytes).set(data, this.pos); + this.pos += data.byteLength; + } + finalize() { + __privateMethod(this, _ensureSize, ensureSize_fn).call(this, this.pos); + __privateGet(this, _target).buffer = __privateGet(this, _buffer).slice(0, this.pos); + } + }; + _target = new WeakMap(); + _buffer = new WeakMap(); + _bytes = new WeakMap(); + _ensureSize = new WeakSet(); + ensureSize_fn = function(size) { + let newLength = __privateGet(this, _buffer).byteLength; + while (newLength < size) + newLength *= 2; + if (newLength === __privateGet(this, _buffer).byteLength) + return; + let newBuffer = new ArrayBuffer(newLength); + let newBytes = new Uint8Array(newBuffer); + newBytes.set(__privateGet(this, _bytes), 0); + __privateSet(this, _buffer, newBuffer); + __privateSet(this, _bytes, newBytes); + }; + var _trackingWrites, _trackedWrites, _trackedStart, _trackedEnd; + var BaseStreamTargetWriter = class extends Writer { + constructor(target) { + super(); + this.target = target; + __privateAdd(this, _trackingWrites, false); + __privateAdd(this, _trackedWrites, void 0); + __privateAdd(this, _trackedStart, void 0); + __privateAdd(this, _trackedEnd, void 0); + } + write(data) { + if (!__privateGet(this, _trackingWrites)) + return; + let pos = this.pos; + if (pos < __privateGet(this, _trackedStart)) { + if (pos + data.byteLength <= __privateGet(this, _trackedStart)) + return; + data = data.subarray(__privateGet(this, _trackedStart) - pos); + pos = 0; + } + let neededSize = pos + data.byteLength - __privateGet(this, _trackedStart); + let newLength = __privateGet(this, _trackedWrites).byteLength; + while (newLength < neededSize) + newLength *= 2; + if (newLength !== __privateGet(this, _trackedWrites).byteLength) { + let copy = new Uint8Array(newLength); + copy.set(__privateGet(this, _trackedWrites), 0); + __privateSet(this, _trackedWrites, copy); + } + __privateGet(this, _trackedWrites).set(data, pos - __privateGet(this, _trackedStart)); + __privateSet(this, _trackedEnd, Math.max(__privateGet(this, _trackedEnd), pos + data.byteLength)); + } + startTrackingWrites() { + __privateSet(this, _trackingWrites, true); + __privateSet(this, _trackedWrites, new Uint8Array(2 ** 10)); + __privateSet(this, _trackedStart, this.pos); + __privateSet(this, _trackedEnd, this.pos); + } + getTrackedWrites() { + if (!__privateGet(this, _trackingWrites)) { + throw new Error("Can't get tracked writes since nothing was tracked."); + } + let slice = __privateGet(this, _trackedWrites).subarray(0, __privateGet(this, _trackedEnd) - __privateGet(this, _trackedStart)); + let result = { + data: slice, + start: __privateGet(this, _trackedStart), + end: __privateGet(this, _trackedEnd) + }; + __privateSet(this, _trackedWrites, void 0); + __privateSet(this, _trackingWrites, false); + return result; + } + }; + _trackingWrites = new WeakMap(); + _trackedWrites = new WeakMap(); + _trackedStart = new WeakMap(); + _trackedEnd = new WeakMap(); + var DEFAULT_CHUNK_SIZE = 2 ** 24; + var MAX_CHUNKS_AT_ONCE = 2; + var _sections, _lastFlushEnd, _ensureMonotonicity, _chunked, _chunkSize, _chunks, _writeDataIntoChunks, writeDataIntoChunks_fn, _insertSectionIntoChunk, insertSectionIntoChunk_fn, _createChunk, createChunk_fn, _flushChunks, flushChunks_fn; + var StreamTargetWriter = class extends BaseStreamTargetWriter { + constructor(target, ensureMonotonicity) { + super(target); + __privateAdd(this, _writeDataIntoChunks); + __privateAdd(this, _insertSectionIntoChunk); + __privateAdd(this, _createChunk); + __privateAdd(this, _flushChunks); + __privateAdd(this, _sections, []); + __privateAdd(this, _lastFlushEnd, 0); + __privateAdd(this, _ensureMonotonicity, void 0); + __privateAdd(this, _chunked, void 0); + __privateAdd(this, _chunkSize, void 0); + __privateAdd(this, _chunks, []); + __privateSet(this, _ensureMonotonicity, ensureMonotonicity); + __privateSet(this, _chunked, target.options?.chunked ?? false); + __privateSet(this, _chunkSize, target.options?.chunkSize ?? DEFAULT_CHUNK_SIZE); + } + write(data) { + super.write(data); + __privateGet(this, _sections).push({ + data: data.slice(), + start: this.pos + }); + this.pos += data.byteLength; + } + flush() { + if (__privateGet(this, _sections).length === 0) + return; + let chunks = []; + let sorted = [...__privateGet(this, _sections)].sort((a, b) => a.start - b.start); + chunks.push({ + start: sorted[0].start, + size: sorted[0].data.byteLength + }); + for (let i = 1; i < sorted.length; i++) { + let lastChunk = chunks[chunks.length - 1]; + let section = sorted[i]; + if (section.start <= lastChunk.start + lastChunk.size) { + lastChunk.size = Math.max(lastChunk.size, section.start + section.data.byteLength - lastChunk.start); + } else { + chunks.push({ + start: section.start, + size: section.data.byteLength + }); + } + } + for (let chunk of chunks) { + chunk.data = new Uint8Array(chunk.size); + for (let section of __privateGet(this, _sections)) { + if (chunk.start <= section.start && section.start < chunk.start + chunk.size) { + chunk.data.set(section.data, section.start - chunk.start); + } + } + if (__privateGet(this, _chunked)) { + __privateMethod(this, _writeDataIntoChunks, writeDataIntoChunks_fn).call(this, chunk.data, chunk.start); + __privateMethod(this, _flushChunks, flushChunks_fn).call(this); + } else { + if (__privateGet(this, _ensureMonotonicity) && chunk.start < __privateGet(this, _lastFlushEnd)) { + throw new Error("Internal error: Monotonicity violation."); + } + this.target.options.onData?.(chunk.data, chunk.start); + __privateSet(this, _lastFlushEnd, chunk.start + chunk.data.byteLength); + } + } + __privateGet(this, _sections).length = 0; + } + finalize() { + if (__privateGet(this, _chunked)) { + __privateMethod(this, _flushChunks, flushChunks_fn).call(this, true); + } + } + }; + _sections = new WeakMap(); + _lastFlushEnd = new WeakMap(); + _ensureMonotonicity = new WeakMap(); + _chunked = new WeakMap(); + _chunkSize = new WeakMap(); + _chunks = new WeakMap(); + _writeDataIntoChunks = new WeakSet(); + writeDataIntoChunks_fn = function(data, position) { + let chunkIndex = __privateGet(this, _chunks).findIndex((x) => x.start <= position && position < x.start + __privateGet(this, _chunkSize)); + if (chunkIndex === -1) + chunkIndex = __privateMethod(this, _createChunk, createChunk_fn).call(this, position); + let chunk = __privateGet(this, _chunks)[chunkIndex]; + let relativePosition = position - chunk.start; + let toWrite = data.subarray(0, Math.min(__privateGet(this, _chunkSize) - relativePosition, data.byteLength)); + chunk.data.set(toWrite, relativePosition); + let section = { + start: relativePosition, + end: relativePosition + toWrite.byteLength + }; + __privateMethod(this, _insertSectionIntoChunk, insertSectionIntoChunk_fn).call(this, chunk, section); + if (chunk.written[0].start === 0 && chunk.written[0].end === __privateGet(this, _chunkSize)) { + chunk.shouldFlush = true; + } + if (__privateGet(this, _chunks).length > MAX_CHUNKS_AT_ONCE) { + for (let i = 0; i < __privateGet(this, _chunks).length - 1; i++) { + __privateGet(this, _chunks)[i].shouldFlush = true; + } + __privateMethod(this, _flushChunks, flushChunks_fn).call(this); + } + if (toWrite.byteLength < data.byteLength) { + __privateMethod(this, _writeDataIntoChunks, writeDataIntoChunks_fn).call(this, data.subarray(toWrite.byteLength), position + toWrite.byteLength); + } + }; + _insertSectionIntoChunk = new WeakSet(); + insertSectionIntoChunk_fn = function(chunk, section) { + let low = 0; + let high = chunk.written.length - 1; + let index = -1; + while (low <= high) { + let mid = Math.floor(low + (high - low + 1) / 2); + if (chunk.written[mid].start <= section.start) { + low = mid + 1; + index = mid; + } else { + high = mid - 1; + } + } + chunk.written.splice(index + 1, 0, section); + if (index === -1 || chunk.written[index].end < section.start) + index++; + while (index < chunk.written.length - 1 && chunk.written[index].end >= chunk.written[index + 1].start) { + chunk.written[index].end = Math.max(chunk.written[index].end, chunk.written[index + 1].end); + chunk.written.splice(index + 1, 1); + } + }; + _createChunk = new WeakSet(); + createChunk_fn = function(includesPosition) { + let start = Math.floor(includesPosition / __privateGet(this, _chunkSize)) * __privateGet(this, _chunkSize); + let chunk = { + start, + data: new Uint8Array(__privateGet(this, _chunkSize)), + written: [], + shouldFlush: false + }; + __privateGet(this, _chunks).push(chunk); + __privateGet(this, _chunks).sort((a, b) => a.start - b.start); + return __privateGet(this, _chunks).indexOf(chunk); + }; + _flushChunks = new WeakSet(); + flushChunks_fn = function(force = false) { + for (let i = 0; i < __privateGet(this, _chunks).length; i++) { + let chunk = __privateGet(this, _chunks)[i]; + if (!chunk.shouldFlush && !force) + continue; + for (let section of chunk.written) { + if (__privateGet(this, _ensureMonotonicity) && chunk.start + section.start < __privateGet(this, _lastFlushEnd)) { + throw new Error("Internal error: Monotonicity violation."); + } + this.target.options.onData?.( + chunk.data.subarray(section.start, section.end), + chunk.start + section.start + ); + __privateSet(this, _lastFlushEnd, chunk.start + section.end); + } + __privateGet(this, _chunks).splice(i--, 1); + } + }; + var FileSystemWritableFileStreamTargetWriter = class extends StreamTargetWriter { + constructor(target, ensureMonotonicity) { + super(new StreamTarget({ + onData: (data, position) => target.stream.write({ + type: "write", + data, + position + }), + chunked: true, + chunkSize: target.options?.chunkSize + }), ensureMonotonicity); + } + }; + + // src/muxer.ts + var VIDEO_TRACK_NUMBER = 1; + var AUDIO_TRACK_NUMBER = 2; + var SUBTITLE_TRACK_NUMBER = 3; + var VIDEO_TRACK_TYPE = 1; + var AUDIO_TRACK_TYPE = 2; + var SUBTITLE_TRACK_TYPE = 17; + var MAX_CHUNK_LENGTH_MS = 2 ** 15; + var CODEC_PRIVATE_MAX_SIZE = 2 ** 13; + var APP_NAME = "https://github.com/Vanilagy/webm-muxer"; + var SEGMENT_SIZE_BYTES = 6; + var CLUSTER_SIZE_BYTES = 5; + var FIRST_TIMESTAMP_BEHAVIORS = ["strict", "offset", "permissive"]; + var _options, _writer, _segment, _segmentInfo, _seekHead, _tracksElement, _segmentDuration, _colourElement, _videoCodecPrivate, _audioCodecPrivate, _subtitleCodecPrivate, _cues, _currentCluster, _currentClusterTimestamp, _duration, _videoChunkQueue, _audioChunkQueue, _subtitleChunkQueue, _firstVideoTimestamp, _firstAudioTimestamp, _lastVideoTimestamp, _lastAudioTimestamp, _lastSubtitleTimestamp, _colorSpace, _finalized, _validateOptions, validateOptions_fn, _createFileHeader, createFileHeader_fn, _writeEBMLHeader, writeEBMLHeader_fn, _createCodecPrivatePlaceholders, createCodecPrivatePlaceholders_fn, _createColourElement, createColourElement_fn, _createSeekHead, createSeekHead_fn, _createSegmentInfo, createSegmentInfo_fn, _createTracks, createTracks_fn, _createSegment, createSegment_fn, _createCues, createCues_fn, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn, _segmentDataOffset, segmentDataOffset_get, _writeVideoDecoderConfig, writeVideoDecoderConfig_fn, _fixVP9ColorSpace, fixVP9ColorSpace_fn, _writeSubtitleChunks, writeSubtitleChunks_fn, _createInternalChunk, createInternalChunk_fn, _validateTimestamp, validateTimestamp_fn, _writeBlock, writeBlock_fn, _createCodecPrivateElement, createCodecPrivateElement_fn, _writeCodecPrivate, writeCodecPrivate_fn, _createNewCluster, createNewCluster_fn, _finalizeCurrentCluster, finalizeCurrentCluster_fn, _ensureNotFinalized, ensureNotFinalized_fn; + var Muxer = class { + constructor(options) { + __privateAdd(this, _validateOptions); + __privateAdd(this, _createFileHeader); + __privateAdd(this, _writeEBMLHeader); + __privateAdd(this, _createCodecPrivatePlaceholders); + __privateAdd(this, _createColourElement); + __privateAdd(this, _createSeekHead); + __privateAdd(this, _createSegmentInfo); + __privateAdd(this, _createTracks); + __privateAdd(this, _createSegment); + __privateAdd(this, _createCues); + __privateAdd(this, _maybeFlushStreamingTargetWriter); + __privateAdd(this, _segmentDataOffset); + __privateAdd(this, _writeVideoDecoderConfig); + __privateAdd(this, _fixVP9ColorSpace); + __privateAdd(this, _writeSubtitleChunks); + __privateAdd(this, _createInternalChunk); + __privateAdd(this, _validateTimestamp); + __privateAdd(this, _writeBlock); + __privateAdd(this, _createCodecPrivateElement); + __privateAdd(this, _writeCodecPrivate); + __privateAdd(this, _createNewCluster); + __privateAdd(this, _finalizeCurrentCluster); + __privateAdd(this, _ensureNotFinalized); + __privateAdd(this, _options, void 0); + __privateAdd(this, _writer, void 0); + __privateAdd(this, _segment, void 0); + __privateAdd(this, _segmentInfo, void 0); + __privateAdd(this, _seekHead, void 0); + __privateAdd(this, _tracksElement, void 0); + __privateAdd(this, _segmentDuration, void 0); + __privateAdd(this, _colourElement, void 0); + __privateAdd(this, _videoCodecPrivate, void 0); + __privateAdd(this, _audioCodecPrivate, void 0); + __privateAdd(this, _subtitleCodecPrivate, void 0); + __privateAdd(this, _cues, void 0); + __privateAdd(this, _currentCluster, void 0); + __privateAdd(this, _currentClusterTimestamp, void 0); + __privateAdd(this, _duration, 0); + __privateAdd(this, _videoChunkQueue, []); + __privateAdd(this, _audioChunkQueue, []); + __privateAdd(this, _subtitleChunkQueue, []); + __privateAdd(this, _firstVideoTimestamp, void 0); + __privateAdd(this, _firstAudioTimestamp, void 0); + __privateAdd(this, _lastVideoTimestamp, -1); + __privateAdd(this, _lastAudioTimestamp, -1); + __privateAdd(this, _lastSubtitleTimestamp, -1); + __privateAdd(this, _colorSpace, void 0); + __privateAdd(this, _finalized, false); + __privateMethod(this, _validateOptions, validateOptions_fn).call(this, options); + __privateSet(this, _options, { + type: "webm", + firstTimestampBehavior: "strict", + ...options + }); + this.target = options.target; + let ensureMonotonicity = !!__privateGet(this, _options).streaming; + if (options.target instanceof ArrayBufferTarget) { + __privateSet(this, _writer, new ArrayBufferTargetWriter(options.target)); + } else if (options.target instanceof StreamTarget) { + __privateSet(this, _writer, new StreamTargetWriter(options.target, ensureMonotonicity)); + } else if (options.target instanceof FileSystemWritableFileStreamTarget) { + __privateSet(this, _writer, new FileSystemWritableFileStreamTargetWriter(options.target, ensureMonotonicity)); + } else { + throw new Error(`Invalid target: ${options.target}`); + } + __privateMethod(this, _createFileHeader, createFileHeader_fn).call(this); + } + addVideoChunk(chunk, meta, timestamp) { + if (!(chunk instanceof EncodedVideoChunk)) { + throw new TypeError("addVideoChunk's first argument (chunk) must be of type EncodedVideoChunk."); + } + if (meta && typeof meta !== "object") { + throw new TypeError("addVideoChunk's second argument (meta), when provided, must be an object."); + } + if (timestamp !== void 0 && (!Number.isFinite(timestamp) || timestamp < 0)) { + throw new TypeError( + "addVideoChunk's third argument (timestamp), when provided, must be a non-negative real number." + ); + } + let data = new Uint8Array(chunk.byteLength); + chunk.copyTo(data); + this.addVideoChunkRaw(data, chunk.type, timestamp ?? chunk.timestamp, meta); + } + addVideoChunkRaw(data, type, timestamp, meta) { + if (!(data instanceof Uint8Array)) { + throw new TypeError("addVideoChunkRaw's first argument (data) must be an instance of Uint8Array."); + } + if (type !== "key" && type !== "delta") { + throw new TypeError("addVideoChunkRaw's second argument (type) must be either 'key' or 'delta'."); + } + if (!Number.isFinite(timestamp) || timestamp < 0) { + throw new TypeError("addVideoChunkRaw's third argument (timestamp) must be a non-negative real number."); + } + if (meta && typeof meta !== "object") { + throw new TypeError("addVideoChunkRaw's fourth argument (meta), when provided, must be an object."); + } + __privateMethod(this, _ensureNotFinalized, ensureNotFinalized_fn).call(this); + if (!__privateGet(this, _options).video) + throw new Error("No video track declared."); + if (__privateGet(this, _firstVideoTimestamp) === void 0) + __privateSet(this, _firstVideoTimestamp, timestamp); + if (meta) + __privateMethod(this, _writeVideoDecoderConfig, writeVideoDecoderConfig_fn).call(this, meta); + let videoChunk = __privateMethod(this, _createInternalChunk, createInternalChunk_fn).call(this, data, type, timestamp, VIDEO_TRACK_NUMBER); + if (__privateGet(this, _options).video.codec === "V_VP9") + __privateMethod(this, _fixVP9ColorSpace, fixVP9ColorSpace_fn).call(this, videoChunk); + __privateSet(this, _lastVideoTimestamp, videoChunk.timestamp); + while (__privateGet(this, _audioChunkQueue).length > 0 && __privateGet(this, _audioChunkQueue)[0].timestamp <= videoChunk.timestamp) { + let audioChunk = __privateGet(this, _audioChunkQueue).shift(); + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, audioChunk, false); + } + if (!__privateGet(this, _options).audio || videoChunk.timestamp <= __privateGet(this, _lastAudioTimestamp)) { + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, videoChunk, true); + } else { + __privateGet(this, _videoChunkQueue).push(videoChunk); + } + __privateMethod(this, _writeSubtitleChunks, writeSubtitleChunks_fn).call(this); + __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); + } + addAudioChunk(chunk, meta, timestamp) { + if (!(chunk instanceof EncodedAudioChunk)) { + throw new TypeError("addAudioChunk's first argument (chunk) must be of type EncodedAudioChunk."); + } + if (meta && typeof meta !== "object") { + throw new TypeError("addAudioChunk's second argument (meta), when provided, must be an object."); + } + if (timestamp !== void 0 && (!Number.isFinite(timestamp) || timestamp < 0)) { + throw new TypeError( + "addAudioChunk's third argument (timestamp), when provided, must be a non-negative real number." + ); + } + let data = new Uint8Array(chunk.byteLength); + chunk.copyTo(data); + this.addAudioChunkRaw(data, chunk.type, timestamp ?? chunk.timestamp, meta); + } + addAudioChunkRaw(data, type, timestamp, meta) { + if (!(data instanceof Uint8Array)) { + throw new TypeError("addAudioChunkRaw's first argument (data) must be an instance of Uint8Array."); + } + if (type !== "key" && type !== "delta") { + throw new TypeError("addAudioChunkRaw's second argument (type) must be either 'key' or 'delta'."); + } + if (!Number.isFinite(timestamp) || timestamp < 0) { + throw new TypeError("addAudioChunkRaw's third argument (timestamp) must be a non-negative real number."); + } + if (meta && typeof meta !== "object") { + throw new TypeError("addAudioChunkRaw's fourth argument (meta), when provided, must be an object."); + } + __privateMethod(this, _ensureNotFinalized, ensureNotFinalized_fn).call(this); + if (!__privateGet(this, _options).audio) + throw new Error("No audio track declared."); + if (__privateGet(this, _firstAudioTimestamp) === void 0) + __privateSet(this, _firstAudioTimestamp, timestamp); + if (meta?.decoderConfig) { + if (__privateGet(this, _options).streaming) { + __privateSet(this, _audioCodecPrivate, __privateMethod(this, _createCodecPrivateElement, createCodecPrivateElement_fn).call(this, meta.decoderConfig.description)); + } else { + __privateMethod(this, _writeCodecPrivate, writeCodecPrivate_fn).call(this, __privateGet(this, _audioCodecPrivate), meta.decoderConfig.description); + } + } + let audioChunk = __privateMethod(this, _createInternalChunk, createInternalChunk_fn).call(this, data, type, timestamp, AUDIO_TRACK_NUMBER); + __privateSet(this, _lastAudioTimestamp, audioChunk.timestamp); + while (__privateGet(this, _videoChunkQueue).length > 0 && __privateGet(this, _videoChunkQueue)[0].timestamp <= audioChunk.timestamp) { + let videoChunk = __privateGet(this, _videoChunkQueue).shift(); + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, videoChunk, true); + } + if (!__privateGet(this, _options).video || audioChunk.timestamp <= __privateGet(this, _lastVideoTimestamp)) { + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, audioChunk, !__privateGet(this, _options).video); + } else { + __privateGet(this, _audioChunkQueue).push(audioChunk); + } + __privateMethod(this, _writeSubtitleChunks, writeSubtitleChunks_fn).call(this); + __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); + } + addSubtitleChunk(chunk, meta, timestamp) { + if (typeof chunk !== "object" || !chunk) { + throw new TypeError("addSubtitleChunk's first argument (chunk) must be an object."); + } else { + if (!(chunk.body instanceof Uint8Array)) { + throw new TypeError("body must be an instance of Uint8Array."); + } + if (!Number.isFinite(chunk.timestamp) || chunk.timestamp < 0) { + throw new TypeError("timestamp must be a non-negative real number."); + } + if (!Number.isFinite(chunk.duration) || chunk.duration < 0) { + throw new TypeError("duration must be a non-negative real number."); + } + if (chunk.additions && !(chunk.additions instanceof Uint8Array)) { + throw new TypeError("additions, when present, must be an instance of Uint8Array."); + } + } + if (typeof meta !== "object") { + throw new TypeError("addSubtitleChunk's second argument (meta) must be an object."); + } + __privateMethod(this, _ensureNotFinalized, ensureNotFinalized_fn).call(this); + if (!__privateGet(this, _options).subtitles) + throw new Error("No subtitle track declared."); + if (meta?.decoderConfig) { + if (__privateGet(this, _options).streaming) { + __privateSet(this, _subtitleCodecPrivate, __privateMethod(this, _createCodecPrivateElement, createCodecPrivateElement_fn).call(this, meta.decoderConfig.description)); + } else { + __privateMethod(this, _writeCodecPrivate, writeCodecPrivate_fn).call(this, __privateGet(this, _subtitleCodecPrivate), meta.decoderConfig.description); + } + } + let subtitleChunk = __privateMethod(this, _createInternalChunk, createInternalChunk_fn).call(this, chunk.body, "key", timestamp ?? chunk.timestamp, SUBTITLE_TRACK_NUMBER, chunk.duration, chunk.additions); + __privateSet(this, _lastSubtitleTimestamp, subtitleChunk.timestamp); + __privateGet(this, _subtitleChunkQueue).push(subtitleChunk); + __privateMethod(this, _writeSubtitleChunks, writeSubtitleChunks_fn).call(this); + __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); + } + finalize() { + if (__privateGet(this, _finalized)) { + throw new Error("Cannot finalize a muxer more than once."); + } + while (__privateGet(this, _videoChunkQueue).length > 0) + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, __privateGet(this, _videoChunkQueue).shift(), true); + while (__privateGet(this, _audioChunkQueue).length > 0) + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, __privateGet(this, _audioChunkQueue).shift(), true); + while (__privateGet(this, _subtitleChunkQueue).length > 0 && __privateGet(this, _subtitleChunkQueue)[0].timestamp <= __privateGet(this, _duration)) { + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, __privateGet(this, _subtitleChunkQueue).shift(), false); + } + if (__privateGet(this, _currentCluster)) { + __privateMethod(this, _finalizeCurrentCluster, finalizeCurrentCluster_fn).call(this); + } + __privateGet(this, _writer).writeEBML(__privateGet(this, _cues)); + if (!__privateGet(this, _options).streaming) { + let endPos = __privateGet(this, _writer).pos; + let segmentSize = __privateGet(this, _writer).pos - __privateGet(this, _segmentDataOffset, segmentDataOffset_get); + __privateGet(this, _writer).seek(__privateGet(this, _writer).offsets.get(__privateGet(this, _segment)) + 4); + __privateGet(this, _writer).writeEBMLVarInt(segmentSize, SEGMENT_SIZE_BYTES); + __privateGet(this, _segmentDuration).data = new EBMLFloat64(__privateGet(this, _duration)); + __privateGet(this, _writer).seek(__privateGet(this, _writer).offsets.get(__privateGet(this, _segmentDuration))); + __privateGet(this, _writer).writeEBML(__privateGet(this, _segmentDuration)); + __privateGet(this, _seekHead).data[0].data[1].data = __privateGet(this, _writer).offsets.get(__privateGet(this, _cues)) - __privateGet(this, _segmentDataOffset, segmentDataOffset_get); + __privateGet(this, _seekHead).data[1].data[1].data = __privateGet(this, _writer).offsets.get(__privateGet(this, _segmentInfo)) - __privateGet(this, _segmentDataOffset, segmentDataOffset_get); + __privateGet(this, _seekHead).data[2].data[1].data = __privateGet(this, _writer).offsets.get(__privateGet(this, _tracksElement)) - __privateGet(this, _segmentDataOffset, segmentDataOffset_get); + __privateGet(this, _writer).seek(__privateGet(this, _writer).offsets.get(__privateGet(this, _seekHead))); + __privateGet(this, _writer).writeEBML(__privateGet(this, _seekHead)); + __privateGet(this, _writer).seek(endPos); + } + __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); + __privateGet(this, _writer).finalize(); + __privateSet(this, _finalized, true); + } + }; + _options = new WeakMap(); + _writer = new WeakMap(); + _segment = new WeakMap(); + _segmentInfo = new WeakMap(); + _seekHead = new WeakMap(); + _tracksElement = new WeakMap(); + _segmentDuration = new WeakMap(); + _colourElement = new WeakMap(); + _videoCodecPrivate = new WeakMap(); + _audioCodecPrivate = new WeakMap(); + _subtitleCodecPrivate = new WeakMap(); + _cues = new WeakMap(); + _currentCluster = new WeakMap(); + _currentClusterTimestamp = new WeakMap(); + _duration = new WeakMap(); + _videoChunkQueue = new WeakMap(); + _audioChunkQueue = new WeakMap(); + _subtitleChunkQueue = new WeakMap(); + _firstVideoTimestamp = new WeakMap(); + _firstAudioTimestamp = new WeakMap(); + _lastVideoTimestamp = new WeakMap(); + _lastAudioTimestamp = new WeakMap(); + _lastSubtitleTimestamp = new WeakMap(); + _colorSpace = new WeakMap(); + _finalized = new WeakMap(); + _validateOptions = new WeakSet(); + validateOptions_fn = function(options) { + if (typeof options !== "object") { + throw new TypeError("The muxer requires an options object to be passed to its constructor."); + } + if (!(options.target instanceof Target)) { + throw new TypeError("The target must be provided and an instance of Target."); + } + if (options.video) { + if (typeof options.video.codec !== "string") { + throw new TypeError(`Invalid video codec: ${options.video.codec}. Must be a string.`); + } + if (!Number.isInteger(options.video.width) || options.video.width <= 0) { + throw new TypeError(`Invalid video width: ${options.video.width}. Must be a positive integer.`); + } + if (!Number.isInteger(options.video.height) || options.video.height <= 0) { + throw new TypeError(`Invalid video height: ${options.video.height}. Must be a positive integer.`); + } + if (options.video.frameRate !== void 0) { + if (!Number.isFinite(options.video.frameRate) || options.video.frameRate <= 0) { + throw new TypeError( + `Invalid video frame rate: ${options.video.frameRate}. Must be a positive number.` + ); + } + } + if (options.video.alpha !== void 0 && typeof options.video.alpha !== "boolean") { + throw new TypeError(`Invalid video alpha: ${options.video.alpha}. Must be a boolean.`); + } + } + if (options.audio) { + if (typeof options.audio.codec !== "string") { + throw new TypeError(`Invalid audio codec: ${options.audio.codec}. Must be a string.`); + } + if (!Number.isInteger(options.audio.numberOfChannels) || options.audio.numberOfChannels <= 0) { + throw new TypeError( + `Invalid number of audio channels: ${options.audio.numberOfChannels}. Must be a positive integer.` + ); + } + if (!Number.isInteger(options.audio.sampleRate) || options.audio.sampleRate <= 0) { + throw new TypeError( + `Invalid audio sample rate: ${options.audio.sampleRate}. Must be a positive integer.` + ); + } + if (options.audio.bitDepth !== void 0) { + if (!Number.isInteger(options.audio.bitDepth) || options.audio.bitDepth <= 0) { + throw new TypeError( + `Invalid audio bit depth: ${options.audio.bitDepth}. Must be a positive integer.` + ); + } + } + } + if (options.subtitles) { + if (typeof options.subtitles.codec !== "string") { + throw new TypeError(`Invalid subtitles codec: ${options.subtitles.codec}. Must be a string.`); + } + } + if (options.type !== void 0 && !["webm", "matroska"].includes(options.type)) { + throw new TypeError(`Invalid type: ${options.type}. Must be 'webm' or 'matroska'.`); + } + if (options.firstTimestampBehavior && !FIRST_TIMESTAMP_BEHAVIORS.includes(options.firstTimestampBehavior)) { + throw new TypeError(`Invalid first timestamp behavior: ${options.firstTimestampBehavior}`); + } + if (options.streaming !== void 0 && typeof options.streaming !== "boolean") { + throw new TypeError(`Invalid streaming option: ${options.streaming}. Must be a boolean.`); + } + }; + _createFileHeader = new WeakSet(); + createFileHeader_fn = function() { + if (__privateGet(this, _writer) instanceof BaseStreamTargetWriter && __privateGet(this, _writer).target.options.onHeader) { + __privateGet(this, _writer).startTrackingWrites(); + } + __privateMethod(this, _writeEBMLHeader, writeEBMLHeader_fn).call(this); + if (!__privateGet(this, _options).streaming) { + __privateMethod(this, _createSeekHead, createSeekHead_fn).call(this); + } + __privateMethod(this, _createSegmentInfo, createSegmentInfo_fn).call(this); + __privateMethod(this, _createCodecPrivatePlaceholders, createCodecPrivatePlaceholders_fn).call(this); + __privateMethod(this, _createColourElement, createColourElement_fn).call(this); + if (!__privateGet(this, _options).streaming) { + __privateMethod(this, _createTracks, createTracks_fn).call(this); + __privateMethod(this, _createSegment, createSegment_fn).call(this); + } else { + } + __privateMethod(this, _createCues, createCues_fn).call(this); + __privateMethod(this, _maybeFlushStreamingTargetWriter, maybeFlushStreamingTargetWriter_fn).call(this); + }; + _writeEBMLHeader = new WeakSet(); + writeEBMLHeader_fn = function() { + let ebmlHeader = { id: 440786851 /* EBML */, data: [ + { id: 17030 /* EBMLVersion */, data: 1 }, + { id: 17143 /* EBMLReadVersion */, data: 1 }, + { id: 17138 /* EBMLMaxIDLength */, data: 4 }, + { id: 17139 /* EBMLMaxSizeLength */, data: 8 }, + { id: 17026 /* DocType */, data: __privateGet(this, _options).type ?? "webm" }, + { id: 17031 /* DocTypeVersion */, data: 2 }, + { id: 17029 /* DocTypeReadVersion */, data: 2 } + ] }; + __privateGet(this, _writer).writeEBML(ebmlHeader); + }; + _createCodecPrivatePlaceholders = new WeakSet(); + createCodecPrivatePlaceholders_fn = function() { + __privateSet(this, _videoCodecPrivate, { id: 236 /* Void */, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) }); + __privateSet(this, _audioCodecPrivate, { id: 236 /* Void */, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) }); + __privateSet(this, _subtitleCodecPrivate, { id: 236 /* Void */, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) }); + }; + _createColourElement = new WeakSet(); + createColourElement_fn = function() { + __privateSet(this, _colourElement, { id: 21936 /* Colour */, data: [ + { id: 21937 /* MatrixCoefficients */, data: 2 }, + { id: 21946 /* TransferCharacteristics */, data: 2 }, + { id: 21947 /* Primaries */, data: 2 }, + { id: 21945 /* Range */, data: 0 } + ] }); + }; + _createSeekHead = new WeakSet(); + createSeekHead_fn = function() { + const kaxCues = new Uint8Array([28, 83, 187, 107]); + const kaxInfo = new Uint8Array([21, 73, 169, 102]); + const kaxTracks = new Uint8Array([22, 84, 174, 107]); + let seekHead = { id: 290298740 /* SeekHead */, data: [ + { id: 19899 /* Seek */, data: [ + { id: 21419 /* SeekID */, data: kaxCues }, + { id: 21420 /* SeekPosition */, size: 5, data: 0 } + ] }, + { id: 19899 /* Seek */, data: [ + { id: 21419 /* SeekID */, data: kaxInfo }, + { id: 21420 /* SeekPosition */, size: 5, data: 0 } + ] }, + { id: 19899 /* Seek */, data: [ + { id: 21419 /* SeekID */, data: kaxTracks }, + { id: 21420 /* SeekPosition */, size: 5, data: 0 } + ] } + ] }; + __privateSet(this, _seekHead, seekHead); + }; + _createSegmentInfo = new WeakSet(); + createSegmentInfo_fn = function() { + let segmentDuration = { id: 17545 /* Duration */, data: new EBMLFloat64(0) }; + __privateSet(this, _segmentDuration, segmentDuration); + let segmentInfo = { id: 357149030 /* Info */, data: [ + { id: 2807729 /* TimestampScale */, data: 1e6 }, + { id: 19840 /* MuxingApp */, data: APP_NAME }, + { id: 22337 /* WritingApp */, data: APP_NAME }, + !__privateGet(this, _options).streaming ? segmentDuration : null + ] }; + __privateSet(this, _segmentInfo, segmentInfo); + }; + _createTracks = new WeakSet(); + createTracks_fn = function() { + let tracksElement = { id: 374648427 /* Tracks */, data: [] }; + __privateSet(this, _tracksElement, tracksElement); + if (__privateGet(this, _options).video) { + tracksElement.data.push({ id: 174 /* TrackEntry */, data: [ + { id: 215 /* TrackNumber */, data: VIDEO_TRACK_NUMBER }, + { id: 29637 /* TrackUID */, data: VIDEO_TRACK_NUMBER }, + { id: 131 /* TrackType */, data: VIDEO_TRACK_TYPE }, + { id: 134 /* CodecID */, data: __privateGet(this, _options).video.codec }, + __privateGet(this, _videoCodecPrivate), + __privateGet(this, _options).video.frameRate ? { id: 2352003 /* DefaultDuration */, data: 1e9 / __privateGet(this, _options).video.frameRate } : null, + { id: 224 /* Video */, data: [ + { id: 176 /* PixelWidth */, data: __privateGet(this, _options).video.width }, + { id: 186 /* PixelHeight */, data: __privateGet(this, _options).video.height }, + __privateGet(this, _options).video.alpha ? { id: 21440 /* AlphaMode */, data: 1 } : null, + __privateGet(this, _colourElement) + ] } + ] }); + } + if (__privateGet(this, _options).audio) { + __privateSet(this, _audioCodecPrivate, __privateGet(this, _options).streaming ? __privateGet(this, _audioCodecPrivate) || null : { id: 236 /* Void */, size: 4, data: new Uint8Array(CODEC_PRIVATE_MAX_SIZE) }); + tracksElement.data.push({ id: 174 /* TrackEntry */, data: [ + { id: 215 /* TrackNumber */, data: AUDIO_TRACK_NUMBER }, + { id: 29637 /* TrackUID */, data: AUDIO_TRACK_NUMBER }, + { id: 131 /* TrackType */, data: AUDIO_TRACK_TYPE }, + { id: 134 /* CodecID */, data: __privateGet(this, _options).audio.codec }, + __privateGet(this, _audioCodecPrivate), + { id: 225 /* Audio */, data: [ + { id: 181 /* SamplingFrequency */, data: new EBMLFloat32(__privateGet(this, _options).audio.sampleRate) }, + { id: 159 /* Channels */, data: __privateGet(this, _options).audio.numberOfChannels }, + __privateGet(this, _options).audio.bitDepth ? { id: 25188 /* BitDepth */, data: __privateGet(this, _options).audio.bitDepth } : null + ] } + ] }); + } + if (__privateGet(this, _options).subtitles) { + tracksElement.data.push({ id: 174 /* TrackEntry */, data: [ + { id: 215 /* TrackNumber */, data: SUBTITLE_TRACK_NUMBER }, + { id: 29637 /* TrackUID */, data: SUBTITLE_TRACK_NUMBER }, + { id: 131 /* TrackType */, data: SUBTITLE_TRACK_TYPE }, + { id: 134 /* CodecID */, data: __privateGet(this, _options).subtitles.codec }, + __privateGet(this, _subtitleCodecPrivate) + ] }); + } + }; + _createSegment = new WeakSet(); + createSegment_fn = function() { + let segment = { + id: 408125543 /* Segment */, + size: __privateGet(this, _options).streaming ? -1 : SEGMENT_SIZE_BYTES, + data: [ + !__privateGet(this, _options).streaming ? __privateGet(this, _seekHead) : null, + __privateGet(this, _segmentInfo), + __privateGet(this, _tracksElement) + ] + }; + __privateSet(this, _segment, segment); + __privateGet(this, _writer).writeEBML(segment); + if (__privateGet(this, _writer) instanceof BaseStreamTargetWriter && __privateGet(this, _writer).target.options.onHeader) { + let { data, start } = __privateGet(this, _writer).getTrackedWrites(); + __privateGet(this, _writer).target.options.onHeader(data, start); + } + }; + _createCues = new WeakSet(); + createCues_fn = function() { + __privateSet(this, _cues, { id: 475249515 /* Cues */, data: [] }); + }; + _maybeFlushStreamingTargetWriter = new WeakSet(); + maybeFlushStreamingTargetWriter_fn = function() { + if (__privateGet(this, _writer) instanceof StreamTargetWriter) { + __privateGet(this, _writer).flush(); + } + }; + _segmentDataOffset = new WeakSet(); + segmentDataOffset_get = function() { + return __privateGet(this, _writer).dataOffsets.get(__privateGet(this, _segment)); + }; + _writeVideoDecoderConfig = new WeakSet(); + writeVideoDecoderConfig_fn = function(meta) { + if (!meta.decoderConfig) + return; + if (meta.decoderConfig.colorSpace) { + let colorSpace = meta.decoderConfig.colorSpace; + __privateSet(this, _colorSpace, colorSpace); + __privateGet(this, _colourElement).data = [ + { id: 21937 /* MatrixCoefficients */, data: { + "rgb": 1, + "bt709": 1, + "bt470bg": 5, + "smpte170m": 6 + }[colorSpace.matrix] }, + { id: 21946 /* TransferCharacteristics */, data: { + "bt709": 1, + "smpte170m": 6, + "iec61966-2-1": 13 + }[colorSpace.transfer] }, + { id: 21947 /* Primaries */, data: { + "bt709": 1, + "bt470bg": 5, + "smpte170m": 6 + }[colorSpace.primaries] }, + { id: 21945 /* Range */, data: [1, 2][Number(colorSpace.fullRange)] } + ]; + if (!__privateGet(this, _options).streaming) { + let endPos = __privateGet(this, _writer).pos; + __privateGet(this, _writer).seek(__privateGet(this, _writer).offsets.get(__privateGet(this, _colourElement))); + __privateGet(this, _writer).writeEBML(__privateGet(this, _colourElement)); + __privateGet(this, _writer).seek(endPos); + } + } + if (meta.decoderConfig.description) { + if (__privateGet(this, _options).streaming) { + __privateSet(this, _videoCodecPrivate, __privateMethod(this, _createCodecPrivateElement, createCodecPrivateElement_fn).call(this, meta.decoderConfig.description)); + } else { + __privateMethod(this, _writeCodecPrivate, writeCodecPrivate_fn).call(this, __privateGet(this, _videoCodecPrivate), meta.decoderConfig.description); + } + } + }; + _fixVP9ColorSpace = new WeakSet(); + fixVP9ColorSpace_fn = function(chunk) { + if (chunk.type !== "key") + return; + if (!__privateGet(this, _colorSpace)) + return; + let i = 0; + if (readBits(chunk.data, 0, 2) !== 2) + return; + i += 2; + let profile = (readBits(chunk.data, i + 1, i + 2) << 1) + readBits(chunk.data, i + 0, i + 1); + i += 2; + if (profile === 3) + i++; + let showExistingFrame = readBits(chunk.data, i + 0, i + 1); + i++; + if (showExistingFrame) + return; + let frameType = readBits(chunk.data, i + 0, i + 1); + i++; + if (frameType !== 0) + return; + i += 2; + let syncCode = readBits(chunk.data, i + 0, i + 24); + i += 24; + if (syncCode !== 4817730) + return; + if (profile >= 2) + i++; + let colorSpaceID = { + "rgb": 7, + "bt709": 2, + "bt470bg": 1, + "smpte170m": 3 + }[__privateGet(this, _colorSpace).matrix]; + writeBits(chunk.data, i + 0, i + 3, colorSpaceID); + }; + _writeSubtitleChunks = new WeakSet(); + writeSubtitleChunks_fn = function() { + let lastWrittenMediaTimestamp = Math.min( + __privateGet(this, _options).video ? __privateGet(this, _lastVideoTimestamp) : Infinity, + __privateGet(this, _options).audio ? __privateGet(this, _lastAudioTimestamp) : Infinity + ); + let queue = __privateGet(this, _subtitleChunkQueue); + while (queue.length > 0 && queue[0].timestamp <= lastWrittenMediaTimestamp) { + __privateMethod(this, _writeBlock, writeBlock_fn).call(this, queue.shift(), !__privateGet(this, _options).video && !__privateGet(this, _options).audio); + } + }; + _createInternalChunk = new WeakSet(); + createInternalChunk_fn = function(data, type, timestamp, trackNumber, duration, additions) { + let adjustedTimestamp = __privateMethod(this, _validateTimestamp, validateTimestamp_fn).call(this, timestamp, trackNumber); + let internalChunk = { + data, + additions, + type, + timestamp: adjustedTimestamp, + duration, + trackNumber + }; + return internalChunk; + }; + _validateTimestamp = new WeakSet(); + validateTimestamp_fn = function(timestamp, trackNumber) { + let lastTimestamp = trackNumber === VIDEO_TRACK_NUMBER ? __privateGet(this, _lastVideoTimestamp) : trackNumber === AUDIO_TRACK_NUMBER ? __privateGet(this, _lastAudioTimestamp) : __privateGet(this, _lastSubtitleTimestamp); + if (trackNumber !== SUBTITLE_TRACK_NUMBER) { + let firstTimestamp = trackNumber === VIDEO_TRACK_NUMBER ? __privateGet(this, _firstVideoTimestamp) : __privateGet(this, _firstAudioTimestamp); + if (__privateGet(this, _options).firstTimestampBehavior === "strict" && lastTimestamp === -1 && timestamp !== 0) { + throw new Error( + `The first chunk for your media track must have a timestamp of 0 (received ${timestamp}). 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 the document, which is probably what you want. + +If you want to offset all timestamps of a track such that the first one is zero, set firstTimestampBehavior: 'offset' in the options. +If you want to allow non-zero first timestamps, set firstTimestampBehavior: 'permissive'. +` + ); + } else if (__privateGet(this, _options).firstTimestampBehavior === "offset") { + timestamp -= firstTimestamp; + } + } + if (timestamp < lastTimestamp) { + throw new Error( + `Timestamps must be monotonically increasing (went from ${lastTimestamp} to ${timestamp}).` + ); + } + if (timestamp < 0) { + throw new Error(`Timestamps must be non-negative (received ${timestamp}).`); + } + return timestamp; + }; + _writeBlock = new WeakSet(); + writeBlock_fn = function(chunk, canCreateNewCluster) { + if (__privateGet(this, _options).streaming && !__privateGet(this, _tracksElement)) { + __privateMethod(this, _createTracks, createTracks_fn).call(this); + __privateMethod(this, _createSegment, createSegment_fn).call(this); + } + let msTimestamp = Math.floor(chunk.timestamp / 1e3); + let relativeTimestamp = msTimestamp - __privateGet(this, _currentClusterTimestamp); + let shouldCreateNewClusterFromKeyFrame = canCreateNewCluster && chunk.type === "key" && relativeTimestamp >= 1e3; + let clusterWouldBeTooLong = relativeTimestamp >= MAX_CHUNK_LENGTH_MS; + if (!__privateGet(this, _currentCluster) || shouldCreateNewClusterFromKeyFrame || clusterWouldBeTooLong) { + __privateMethod(this, _createNewCluster, createNewCluster_fn).call(this, msTimestamp); + relativeTimestamp = 0; + } + if (relativeTimestamp < 0) { + return; + } + let prelude = new Uint8Array(4); + let view = new DataView(prelude.buffer); + view.setUint8(0, 128 | chunk.trackNumber); + view.setInt16(1, relativeTimestamp, false); + if (chunk.duration === void 0 && !chunk.additions) { + view.setUint8(3, Number(chunk.type === "key") << 7); + let simpleBlock = { id: 163 /* SimpleBlock */, data: [ + prelude, + chunk.data + ] }; + __privateGet(this, _writer).writeEBML(simpleBlock); + } else { + let msDuration = Math.floor(chunk.duration / 1e3); + let blockGroup = { id: 160 /* BlockGroup */, data: [ + { id: 161 /* Block */, data: [ + prelude, + chunk.data + ] }, + chunk.duration !== void 0 ? { id: 155 /* BlockDuration */, data: msDuration } : null, + chunk.additions ? { id: 30113 /* BlockAdditions */, data: chunk.additions } : null + ] }; + __privateGet(this, _writer).writeEBML(blockGroup); + } + __privateSet(this, _duration, Math.max(__privateGet(this, _duration), msTimestamp)); + }; + _createCodecPrivateElement = new WeakSet(); + createCodecPrivateElement_fn = function(data) { + return { id: 25506 /* CodecPrivate */, size: 4, data: new Uint8Array(data) }; + }; + _writeCodecPrivate = new WeakSet(); + writeCodecPrivate_fn = function(element, data) { + let endPos = __privateGet(this, _writer).pos; + __privateGet(this, _writer).seek(__privateGet(this, _writer).offsets.get(element)); + let codecPrivateElementSize = 2 + 4 + data.byteLength; + let voidDataSize = CODEC_PRIVATE_MAX_SIZE - codecPrivateElementSize; + if (voidDataSize < 0) { + let newByteLength = data.byteLength + voidDataSize; + if (data instanceof ArrayBuffer) { + data = data.slice(0, newByteLength); + } else { + data = data.buffer.slice(0, newByteLength); + } + voidDataSize = 0; + } + element = [ + __privateMethod(this, _createCodecPrivateElement, createCodecPrivateElement_fn).call(this, data), + { id: 236 /* Void */, size: 4, data: new Uint8Array(voidDataSize) } + ]; + __privateGet(this, _writer).writeEBML(element); + __privateGet(this, _writer).seek(endPos); + }; + _createNewCluster = new WeakSet(); + createNewCluster_fn = function(timestamp) { + if (__privateGet(this, _currentCluster)) { + __privateMethod(this, _finalizeCurrentCluster, finalizeCurrentCluster_fn).call(this); + } + if (__privateGet(this, _writer) instanceof BaseStreamTargetWriter && __privateGet(this, _writer).target.options.onCluster) { + __privateGet(this, _writer).startTrackingWrites(); + } + __privateSet(this, _currentCluster, { + id: 524531317 /* Cluster */, + size: __privateGet(this, _options).streaming ? -1 : CLUSTER_SIZE_BYTES, + data: [ + { id: 231 /* Timestamp */, data: timestamp } + ] + }); + __privateGet(this, _writer).writeEBML(__privateGet(this, _currentCluster)); + __privateSet(this, _currentClusterTimestamp, timestamp); + let clusterOffsetFromSegment = __privateGet(this, _writer).offsets.get(__privateGet(this, _currentCluster)) - __privateGet(this, _segmentDataOffset, segmentDataOffset_get); + __privateGet(this, _cues).data.push({ id: 187 /* CuePoint */, data: [ + { id: 179 /* CueTime */, data: timestamp }, + __privateGet(this, _options).video ? { id: 183 /* CueTrackPositions */, data: [ + { id: 247 /* CueTrack */, data: VIDEO_TRACK_NUMBER }, + { id: 241 /* CueClusterPosition */, data: clusterOffsetFromSegment } + ] } : null, + __privateGet(this, _options).audio ? { id: 183 /* CueTrackPositions */, data: [ + { id: 247 /* CueTrack */, data: AUDIO_TRACK_NUMBER }, + { id: 241 /* CueClusterPosition */, data: clusterOffsetFromSegment } + ] } : null + ] }); + }; + _finalizeCurrentCluster = new WeakSet(); + finalizeCurrentCluster_fn = function() { + if (!__privateGet(this, _options).streaming) { + let clusterSize = __privateGet(this, _writer).pos - __privateGet(this, _writer).dataOffsets.get(__privateGet(this, _currentCluster)); + let endPos = __privateGet(this, _writer).pos; + __privateGet(this, _writer).seek(__privateGet(this, _writer).offsets.get(__privateGet(this, _currentCluster)) + 4); + __privateGet(this, _writer).writeEBMLVarInt(clusterSize, CLUSTER_SIZE_BYTES); + __privateGet(this, _writer).seek(endPos); + } + if (__privateGet(this, _writer) instanceof BaseStreamTargetWriter && __privateGet(this, _writer).target.options.onCluster) { + let { data, start } = __privateGet(this, _writer).getTrackedWrites(); + __privateGet(this, _writer).target.options.onCluster(data, start, __privateGet(this, _currentClusterTimestamp)); + } + }; + _ensureNotFinalized = new WeakSet(); + ensureNotFinalized_fn = function() { + if (__privateGet(this, _finalized)) { + throw new Error("Cannot add new video or audio chunks after the file has been finalized."); + } + }; + + // src/subtitles.ts + var cueBlockHeaderRegex = /(?:(.+?)\n)?((?:\d{2}:)?\d{2}:\d{2}.\d{3})\s+-->\s+((?:\d{2}:)?\d{2}:\d{2}.\d{3})/g; + var preambleStartRegex = /^WEBVTT.*?\n{2}/; + var timestampRegex = /(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})/; + var inlineTimestampRegex = /<(?:(\d{2}):)?(\d{2}):(\d{2}).(\d{3})>/g; + var textEncoder = new TextEncoder(); + var _options2, _config, _preambleSeen, _preambleBytes, _preambleEmitted, _parseTimestamp, parseTimestamp_fn, _formatTimestamp, formatTimestamp_fn; + var SubtitleEncoder = class { + constructor(options) { + __privateAdd(this, _parseTimestamp); + __privateAdd(this, _formatTimestamp); + __privateAdd(this, _options2, void 0); + __privateAdd(this, _config, void 0); + __privateAdd(this, _preambleSeen, false); + __privateAdd(this, _preambleBytes, void 0); + __privateAdd(this, _preambleEmitted, false); + __privateSet(this, _options2, options); + } + configure(config) { + if (config.codec !== "webvtt") { + throw new Error("Codec must be 'webvtt'."); + } + __privateSet(this, _config, config); + } + encode(text) { + if (!__privateGet(this, _config)) { + throw new Error("Encoder not configured."); + } + text = text.replace("\r\n", "\n").replace("\r", "\n"); + cueBlockHeaderRegex.lastIndex = 0; + let match; + if (!__privateGet(this, _preambleSeen)) { + if (!preambleStartRegex.test(text)) { + let error = new Error("WebVTT preamble incorrect."); + __privateGet(this, _options2).error(error); + throw error; + } + match = cueBlockHeaderRegex.exec(text); + let preamble = text.slice(0, match?.index ?? text.length).trimEnd(); + if (!preamble) { + let error = new Error("No WebVTT preamble provided."); + __privateGet(this, _options2).error(error); + throw error; + } + __privateSet(this, _preambleBytes, textEncoder.encode(preamble)); + __privateSet(this, _preambleSeen, true); + if (match) { + text = text.slice(match.index); + cueBlockHeaderRegex.lastIndex = 0; + } + } + while (match = cueBlockHeaderRegex.exec(text)) { + let notes = text.slice(0, match.index); + let cueIdentifier = match[1] || ""; + let matchEnd = match.index + match[0].length; + let bodyStart = text.indexOf("\n", matchEnd) + 1; + let cueSettings = text.slice(matchEnd, bodyStart).trim(); + let bodyEnd = text.indexOf("\n\n", matchEnd); + if (bodyEnd === -1) + bodyEnd = text.length; + let startTime = __privateMethod(this, _parseTimestamp, parseTimestamp_fn).call(this, match[2]); + let endTime = __privateMethod(this, _parseTimestamp, parseTimestamp_fn).call(this, match[3]); + let duration = endTime - startTime; + let body = text.slice(bodyStart, bodyEnd); + let additions = `${cueSettings} +${cueIdentifier} +${notes}`; + inlineTimestampRegex.lastIndex = 0; + body = body.replace(inlineTimestampRegex, (match2) => { + let time = __privateMethod(this, _parseTimestamp, parseTimestamp_fn).call(this, match2.slice(1, -1)); + let offsetTime = time - startTime; + return `<${__privateMethod(this, _formatTimestamp, formatTimestamp_fn).call(this, offsetTime)}>`; + }); + text = text.slice(bodyEnd).trimStart(); + cueBlockHeaderRegex.lastIndex = 0; + let chunk = { + body: textEncoder.encode(body), + additions: additions.trim() === "" ? void 0 : textEncoder.encode(additions), + timestamp: startTime * 1e3, + duration: duration * 1e3 + }; + let meta = {}; + if (!__privateGet(this, _preambleEmitted)) { + meta.decoderConfig = { + description: __privateGet(this, _preambleBytes) + }; + __privateSet(this, _preambleEmitted, true); + } + __privateGet(this, _options2).output(chunk, meta); + } + } + }; + _options2 = new WeakMap(); + _config = new WeakMap(); + _preambleSeen = new WeakMap(); + _preambleBytes = new WeakMap(); + _preambleEmitted = new WeakMap(); + _parseTimestamp = new WeakSet(); + parseTimestamp_fn = function(string) { + let match = timestampRegex.exec(string); + if (!match) + throw new Error("Expected match."); + return 60 * 60 * 1e3 * Number(match[1] || "0") + 60 * 1e3 * Number(match[2]) + 1e3 * Number(match[3]) + Number(match[4]); + }; + _formatTimestamp = new WeakSet(); + formatTimestamp_fn = function(timestamp) { + let hours = Math.floor(timestamp / (60 * 60 * 1e3)); + let minutes = Math.floor(timestamp % (60 * 60 * 1e3) / (60 * 1e3)); + let seconds = Math.floor(timestamp % (60 * 1e3) / 1e3); + let milliseconds = timestamp % 1e3; + return hours.toString().padStart(2, "0") + ":" + minutes.toString().padStart(2, "0") + ":" + seconds.toString().padStart(2, "0") + "." + milliseconds.toString().padStart(3, "0"); + }; + return __toCommonJS(src_exports); +})(); +if (typeof module === "object" && typeof module.exports === "object") Object.assign(module.exports, WebMMuxer) diff --git a/server.mjs b/server.mjs index 92ac11c..8566a2c 100644 --- a/server.mjs +++ b/server.mjs @@ -229,8 +229,8 @@ app.post('/api/export/start', upload.single('audio'), async (req, res) => { } }); -// Stream raw RGBA frame batch -app.post('/api/export/frame/:id', express.raw({ type: 'application/octet-stream', limit: '200mb' }), (req, res) => { +// Stream raw RGBA frame or multi-frame batch +app.post(['/api/export/frame/:id', '/api/export/frame-batch/:id'], express.raw({ type: 'application/octet-stream', limit: '500mb' }), (req, res) => { const session = activeSessions.get(req.params.id); if (!session || !session.ffmpegProc || session.done) { return res.status(404).json({ error: 'Session nicht gefunden oder bereits beendet.' });