From 3f35add587da23d025fa1bded3cf53cd619c07c4 Mon Sep 17 00:00:00 2001 From: Tim <135014430+nagisa77@users.noreply.github.com> Date: Thu, 11 Sep 2025 17:01:54 +0800 Subject: [PATCH 01/12] feat: switch video compression to webcodecs --- frontend_nuxt/nuxt.config.ts | 14 - frontend_nuxt/package-lock.json | 42 +-- frontend_nuxt/package.json | 3 +- frontend_nuxt/plugins/ffmpeg.client.ts | 37 -- frontend_nuxt/utils/ffmpegVideoCompressor.js | 327 ------------------ frontend_nuxt/utils/vditor.js | 7 +- frontend_nuxt/utils/videoCompressor.js | 31 +- .../utils/webcodecVideoCompressor.js | 108 ++++++ 8 files changed, 134 insertions(+), 435 deletions(-) delete mode 100644 frontend_nuxt/plugins/ffmpeg.client.ts delete mode 100644 frontend_nuxt/utils/ffmpegVideoCompressor.js create mode 100644 frontend_nuxt/utils/webcodecVideoCompressor.js diff --git a/frontend_nuxt/nuxt.config.ts b/frontend_nuxt/nuxt.config.ts index dad43963c..f0adf3c98 100644 --- a/frontend_nuxt/nuxt.config.ts +++ b/frontend_nuxt/nuxt.config.ts @@ -1,17 +1,4 @@ import { defineNuxtConfig } from 'nuxt/config' -import { createRequire } from 'node:module' - -const require = createRequire(import.meta.url) -const appPkg = require('./package.json') as { - dependencies?: Record - devDependencies?: Record -} -const ffmpegVersion = ( - process.env.NUXT_PUBLIC_FFMPEG_VERSION || - appPkg.dependencies?.['@ffmpeg/ffmpeg'] || - appPkg.devDependencies?.['@ffmpeg/ffmpeg'] || - '0.12.15' -).replace(/^[^\d]*/, '') export default defineNuxtConfig({ devServer: { host: '0.0.0.0', @@ -29,7 +16,6 @@ export default defineNuxtConfig({ discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID || '', twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '', telegramBotId: process.env.NUXT_PUBLIC_TELEGRAM_BOT_ID || '', - ffmpegVersion, }, }, css: [ diff --git a/frontend_nuxt/package-lock.json b/frontend_nuxt/package-lock.json index 39770a80c..a7a28be3d 100644 --- a/frontend_nuxt/package-lock.json +++ b/frontend_nuxt/package-lock.json @@ -6,8 +6,6 @@ "": { "name": "frontend_nuxt", "dependencies": { - "@ffmpeg/ffmpeg": "^0.12.15", - "@ffmpeg/util": "^0.12.2", "@icon-park/vue-next": "^1.4.2", "@nuxt/image": "^1.11.0", "@stomp/stompjs": "^7.0.0", @@ -21,6 +19,7 @@ "ldrs": "^1.0.0", "markdown-it": "^14.1.0", "mermaid": "^10.9.4", + "mp4box": "^2.1.1", "nanoid": "^5.1.5", "nprogress": "^0.2.0", "nuxt": "latest", @@ -999,36 +998,6 @@ "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", "license": "MIT" }, - "node_modules/@ffmpeg/ffmpeg": { - "version": "0.12.15", - "resolved": "https://registry.npmmirror.com/@ffmpeg/ffmpeg/-/ffmpeg-0.12.15.tgz", - "integrity": "sha512-1C8Obr4GsN3xw+/1Ww6PFM84wSQAGsdoTuTWPOj2OizsRDLT4CXTaVjPhkw6ARyDus1B9X/L2LiXHqYYsGnRFw==", - "license": "MIT", - "dependencies": { - "@ffmpeg/types": "^0.12.4" - }, - "engines": { - "node": ">=18.x" - } - }, - "node_modules/@ffmpeg/types": { - "version": "0.12.4", - "resolved": "https://registry.npmmirror.com/@ffmpeg/types/-/types-0.12.4.tgz", - "integrity": "sha512-k9vJQNBGTxE5AhYDtOYR5rO5fKsspbg51gbcwtbkw2lCdoIILzklulcjJfIDwrtn7XhDeF2M+THwJ2FGrLeV6A==", - "license": "MIT", - "engines": { - "node": ">=16.x" - } - }, - "node_modules/@ffmpeg/util": { - "version": "0.12.2", - "resolved": "https://registry.npmmirror.com/@ffmpeg/util/-/util-0.12.2.tgz", - "integrity": "sha512-ouyoW+4JB7WxjeZ2y6KpRvB+dLp7Cp4ro8z0HIVpZVCM7AwFlHa0c4R8Y/a4M3wMqATpYKhC7lSFHQ0T11MEDw==", - "license": "MIT", - "engines": { - "node": ">=18.x" - } - }, "node_modules/@icon-park/vue-next": { "version": "1.4.2", "resolved": "https://registry.npmmirror.com/@icon-park/vue-next/-/vue-next-1.4.2.tgz", @@ -10172,6 +10141,15 @@ "node": ">=18" } }, + "node_modules/mp4box": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mp4box/-/mp4box-2.1.1.tgz", + "integrity": "sha512-gttYFNmlCjredsdnxqNC6ho0bx6zEwOqAwSKZNQXtsBqvSN1CjtzlTLY9Kfhvt14Co8Iu+qMuOOpnPIRjvvFtw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.8.1" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", diff --git a/frontend_nuxt/package.json b/frontend_nuxt/package.json index 31f873a29..935d6db93 100644 --- a/frontend_nuxt/package.json +++ b/frontend_nuxt/package.json @@ -13,8 +13,6 @@ }, "dependencies": { "@icon-park/vue-next": "^1.4.2", - "@ffmpeg/ffmpeg": "^0.12.15", - "@ffmpeg/util": "^0.12.2", "@nuxt/image": "^1.11.0", "@stomp/stompjs": "^7.0.0", "cropperjs": "^1.6.2", @@ -27,6 +25,7 @@ "ldrs": "^1.0.0", "markdown-it": "^14.1.0", "mermaid": "^10.9.4", + "mp4box": "^2.1.1", "nanoid": "^5.1.5", "nprogress": "^0.2.0", "nuxt": "latest", diff --git a/frontend_nuxt/plugins/ffmpeg.client.ts b/frontend_nuxt/plugins/ffmpeg.client.ts deleted file mode 100644 index 562a93aa2..000000000 --- a/frontend_nuxt/plugins/ffmpeg.client.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { FFmpeg } from '@ffmpeg/ffmpeg' -import { toBlobURL } from '@ffmpeg/util' -import { defineNuxtPlugin, useRuntimeConfig } from 'nuxt/app' - -let ffmpeg: FFmpeg | null = null - -export default defineNuxtPlugin(() => { - const { - public: { ffmpegVersion }, - } = useRuntimeConfig() - - return { - provide: { - ffmpeg: async () => { - if (ffmpeg) return ffmpeg - - ffmpeg = new FFmpeg() - - const mtOk = - typeof crossOriginIsolated !== 'undefined' && - crossOriginIsolated && - typeof SharedArrayBuffer !== 'undefined' - - const pkg = mtOk ? '@ffmpeg/core-mt' : '@ffmpeg/core-st' - const base = `https://unpkg.com/${pkg}@${ffmpegVersion}/dist/umd` - - await ffmpeg.load({ - coreURL: await toBlobURL(`${base}/ffmpeg-core.js`, 'text/javascript'), - wasmURL: await toBlobURL(`${base}/ffmpeg-core.wasm`, 'application/wasm'), - workerURL: await toBlobURL(`${base}/ffmpeg-core.worker.js`, 'text/javascript'), - }) - - return ffmpeg - }, - }, - } -}) diff --git a/frontend_nuxt/utils/ffmpegVideoCompressor.js b/frontend_nuxt/utils/ffmpegVideoCompressor.js deleted file mode 100644 index 2aa155049..000000000 --- a/frontend_nuxt/utils/ffmpegVideoCompressor.js +++ /dev/null @@ -1,327 +0,0 @@ -/** - * FFmpeg.wasm 视频压缩器 - * - * 用法: - * const { $ffmpeg } = useNuxtApp() - * const ff = await $ffmpeg() // 插件里已完成 ffmpeg.load() - * const out = await compressVideoWithFFmpeg(ff, file, { onProgress, strictSize: false }) - * - * 设计要点: - * - 本文件不再负责加载/初始化,只负责转码逻辑;和 Nuxt 插件解耦。 - * - 针对【同一个 ffmpeg 实例】做串行队列,避免并发 exec 踩内存文件系统。 - * - 使用 nanoid 生成唯一文件名;日志环形缓冲;默认 CRF+VBV,可选 strictSize(two-pass)。 - * - 体积明显小于目标时直通返回,减少无谓重编码。 - */ - -import { fetchFile } from '@ffmpeg/util' -import { nanoid } from 'nanoid' -import { UPLOAD_CONFIG } from '../config/uploadConfig.js' - -/************************* - * 每实例一个串行队列 * - *************************/ -// WeakMapPromise)[], running: boolean, resolvers: {res,rej}[] }> -const queues = new WeakMap() - -function enqueueOn(instance, taskFn) { - return new Promise((res, rej) => { - let st = queues.get(instance) - if (!st) { - st = { q: [], running: false, resolvers: [] } - queues.set(instance, st) - } - st.q.push(taskFn) - st.resolvers.push({ res, rej }) - drain(instance) - }) -} - -async function drain(instance) { - const st = queues.get(instance) - if (!st || st.running) return - st.running = true - try { - while (st.q.length) { - const task = st.q.shift() - const rr = st.resolvers.shift() - try { - rr.res(await task()) - } catch (e) { - rr.rej(e) - } - } - } finally { - st.running = false - } -} - -/***************** - * 工具函数 * - *****************/ -function decideScale(widthHint) { - if (!widthHint) return { filter: null, width: null } - const evenW = widthHint % 2 === 0 ? widthHint : widthHint - 1 - return { filter: `scale=${evenW}:-2:flags=bicubic,setsar=1`, width: evenW } -} - -function calculateParamsByRatio(originalSize, targetSize) { - const ratio = Math.min(targetSize / originalSize, 1) - const crf = ratio < 0.35 ? 29 : ratio < 0.5 ? 27 : ratio < 0.7 ? 25 : 23 - const preset = ratio < 0.35 ? 'slow' : ratio < 0.5 ? 'medium' : 'veryfast' - const s = - ratio < 0.35 - ? decideScale(720) - : ratio < 0.6 - ? decideScale(960) - : ratio < 0.8 - ? decideScale(1280) - : { filter: null, width: null } - const audioBitrateK = ratio < 0.5 ? 96 : ratio < 0.7 ? 128 : 160 - const profile = s.width && s.width <= 1280 ? 'main' : 'high' - return { crf, preset, scaleFilter: s.filter, scaleWidth: s.width, audioBitrateK, profile } -} - -function makeRingLogger(capBytes = 4000) { - const buf = [] - let total = 0 - function push(s) { - if (!s) return - buf.push(s) - total += s.length - while (total > capBytes) total -= buf.shift().length - } - return { push, dump: () => buf.slice() } -} - -function parseDurationFromLogs(logs) { - // 避免正则:查找 Duration: 后的 00:00:00.xx - const text = logs.join(' ') - const idx = text.indexOf('Duration:') - if (idx === -1) return null - let i = idx + 'Duration:'.length - while (i < text.length && text[i] === ' ') i++ - function read2(start) { - const a = text.charCodeAt(start) - 48 - const b = text.charCodeAt(start + 1) - 48 - if (a < 0 || a > 9 || b < 0 || b > 9) return null - return a * 10 + b - } - const hh = read2(i) - if (hh === null) return null - i += 2 - if (text[i++] !== ':') return null - const mm = read2(i) - if (mm === null) return null - i += 2 - if (text[i++] !== ':') return null - const s1 = read2(i) - if (s1 === null) return null - i += 2 - if (text[i++] !== '.') return null - let j = i - while (j < text.length && text.charCodeAt(j) >= 48 && text.charCodeAt(j) <= 57) j++ - const frac = parseFloat('0.' + text.slice(i, j) || '0') - return hh * 3600 + mm * 60 + s1 + frac -} - -export function isFFmpegSupported() { - return typeof WebAssembly !== 'undefined' && typeof Worker !== 'undefined' -} - -/** - * 读取 ffmpeg 核心版本(通过 -version),会进入队列避免并发冲突 - */ -export async function getFFmpegInfo(ffmpegInstance) { - return enqueueOn(ffmpegInstance, async () => { - const logs = [] - const onLog = ({ type, message }) => { - if (type === 'info' || type === 'fferr') logs.push(message) - } - ffmpegInstance.on('log', onLog) - try { - await ffmpegInstance.exec(['-version']) - } finally { - ffmpegInstance.off('log', onLog) - } - const line = logs.find((l) => l.toLowerCase().includes('ffmpeg version')) || '' - const parts = line.trim().split(' ').filter(Boolean) - const version = parts.length > 2 ? parts[2] : parts[1] || null - return { version } - }) -} - -/** - * 压缩:接受一个已经 load() 完成的 ffmpeg 实例 - * @param {*} ffmpegInstance 已初始化的 FFmpeg 实例(来自 Nuxt 插件) - * @param {File|Blob} file 输入文件 - * @param {{ onProgress?:(p:{stage:string,progress:number})=>void, signal?:AbortSignal, strictSize?:boolean, targetSize?:number }} opts - */ -export async function compressVideoWithFFmpeg(ffmpegInstance, file, opts = {}) { - return enqueueOn(ffmpegInstance, () => doCompress(ffmpegInstance, file, opts)) -} - -async function doCompress(ffmpegInstance, file, opts) { - const onProgress = opts.onProgress || (() => {}) - const { signal, strictSize = false } = opts - - onProgress({ stage: 'preparing', progress: 10 }) - - const targetSize = opts.targetSize ?? UPLOAD_CONFIG?.VIDEO?.TARGET_SIZE ?? 12 * 1024 * 1024 - - // 小体积直通 - const sizeKnown = 'size' in file && typeof file.size === 'number' - if (sizeKnown && file.size <= targetSize * 0.9) { - onProgress({ stage: 'skipped', progress: 100 }) - return file - } - - const params = calculateParamsByRatio(sizeKnown ? file.size : targetSize * 2, targetSize) - const { crf, preset, scaleFilter, audioBitrateK, profile } = params - - const name = 'name' in file && typeof file.name === 'string' ? file.name : 'input.mp4' - const dot = name.lastIndexOf('.') - const outName = (dot > -1 ? name.slice(0, dot) : name) + '.mp4' - - const ext = dot > -1 ? name.slice(dot + 1).toLowerCase() : 'mp4' - const id = nanoid() - const inputName = `input-${id}.${ext}` - const outputName = `output-${id}.mp4` - const passlog = `ffpass-${id}` - - // 监听 - const ring = makeRingLogger() - const onFfmpegProgress = ({ progress: p }) => { - const adjusted = 20 + p * 70 - onProgress({ stage: 'compressing', progress: Math.min(90, adjusted) }) - } - const onFfmpegLog = ({ type, message }) => { - if (type === 'fferr' || type === 'info') ring.push(message) - } - ffmpegInstance.on('progress', onFfmpegProgress) - ffmpegInstance.on('log', onFfmpegLog) - - let aborted = false - const abortHandler = () => { - aborted = true - } - if (signal) signal.addEventListener('abort', abortHandler, { once: true }) - - try { - await ffmpegInstance.writeFile(inputName, await fetchFile(file)) - onProgress({ stage: 'analyzing', progress: 20 }) - - let durationSec = null - try { - await ffmpegInstance.exec(['-hide_banner', '-i', inputName, '-f', 'null', '-']) - durationSec = parseDurationFromLogs(ring.dump()) - } catch { - durationSec = durationSec ?? parseDurationFromLogs(ring.dump()) - } - - let videoBitrate = null - if (durationSec && sizeKnown && targetSize < file.size) { - const totalTargetBits = targetSize * 8 - const audioBits = audioBitrateK * 1000 * durationSec - const maxVideoBits = Math.max(totalTargetBits - audioBits, totalTargetBits * 0.7) - const bps = Math.max(180000, Math.floor(maxVideoBits / durationSec)) - videoBitrate = String(Math.min(bps, 5000000)) - } - - const baseArgs = [ - '-hide_banner', - '-i', - inputName, - '-c:v', - 'libx264', - '-pix_fmt', - 'yuv420p', - '-profile:v', - profile, - '-movflags', - '+faststart', - '-preset', - preset, - '-c:a', - 'aac', - '-b:a', - `${audioBitrateK}k`, - '-ac', - '2', - ] - if (scaleFilter) baseArgs.push('-vf', scaleFilter) - - const onePassArgs = [...baseArgs, '-crf', String(crf)] - if (videoBitrate) - onePassArgs.push('-maxrate', videoBitrate, '-bufsize', String(parseInt(videoBitrate, 10) * 2)) - - const twoPassFirst = [ - '-y', - '-hide_banner', - '-i', - inputName, - '-c:v', - 'libx264', - '-b:v', - `${videoBitrate || '1000000'}`, - '-pass', - '1', - '-passlogfile', - passlog, - '-an', - '-f', - 'mp4', - '/dev/null', - ] - const twoPassSecond = [ - ...baseArgs, - '-b:v', - `${videoBitrate || '1000000'}`, - '-pass', - '2', - '-passlogfile', - passlog, - outputName, - ] - - if (aborted) throw new DOMException('Aborted', 'AbortError') - - if (!strictSize) { - await ffmpegInstance.exec([...onePassArgs, outputName]) - } else { - if (!videoBitrate) videoBitrate = '1000000' - await ffmpegInstance.exec(twoPassFirst) - onProgress({ stage: 'second-pass', progress: 85 }) - await ffmpegInstance.exec(twoPassSecond) - } - - if (aborted) throw new DOMException('Aborted', 'AbortError') - - onProgress({ stage: 'finalizing', progress: 95 }) - const out = await ffmpegInstance.readFile(outputName) - - const mime = 'video/mp4' - const blob = new Blob([out], { type: mime }) - const hasFileCtor = typeof File === 'function' - const result = hasFileCtor ? new File([blob], outName, { type: mime }) : blob - - onProgress({ stage: 'completed', progress: 100 }) - return result - } finally { - try { - await ffmpegInstance.deleteFile(inputName) - } catch {} - try { - await ffmpegInstance.deleteFile(outputName) - } catch {} - try { - await ffmpegInstance.deleteFile(`${passlog}-0.log`) - } catch {} - try { - await ffmpegInstance.deleteFile(`${passlog}-0.log.mbtree`) - } catch {} - - ffmpegInstance.off('progress', onFfmpegProgress) - ffmpegInstance.off('log', onFfmpegLog) - if (signal) signal.removeEventListener('abort', abortHandler) - } -} diff --git a/frontend_nuxt/utils/vditor.js b/frontend_nuxt/utils/vditor.js index c2c6d7d9a..dbe6ae2bb 100644 --- a/frontend_nuxt/utils/vditor.js +++ b/frontend_nuxt/utils/vditor.js @@ -121,14 +121,13 @@ export function createVditor(editorId, options = {}) { vditor.tip('视频压缩中...', 0) vditor.disabled() - // 使用 FFmpeg 压缩视频 + // 使用 WebCodecs 压缩视频 processedFile = await compressVideo(file, (progress) => { const messages = { - initializing: '初始化 FFmpeg', + initializing: '初始化编解码器', preparing: '准备压缩', - analyzing: '分析视频', compressing: '压缩中', - finalizing: '完成压缩', + packaging: '封装中', completed: '压缩完成', } const message = messages[progress.stage] || progress.stage diff --git a/frontend_nuxt/utils/videoCompressor.js b/frontend_nuxt/utils/videoCompressor.js index 584d8a487..943551283 100644 --- a/frontend_nuxt/utils/videoCompressor.js +++ b/frontend_nuxt/utils/videoCompressor.js @@ -1,11 +1,10 @@ /** - * 基于 FFmpeg.wasm 的视频压缩工具 + * 基于 WebCodecs + MP4Box.js 的视频压缩工具 * 专为现代浏览器 (Chrome/Safari) 优化 */ import { UPLOAD_CONFIG } from '../config/uploadConfig.js' -import { compressVideoWithFFmpeg, isFFmpegSupported } from './ffmpegVideoCompressor.js' -import { useNuxtApp } from '#app' +import { compressVideoWithWebCodecs, isWebCodecSupported } from './webcodecVideoCompressor.js' // 导出配置供外部使用 export const VIDEO_CONFIG = UPLOAD_CONFIG.VIDEO @@ -34,7 +33,7 @@ export function formatFileSize(bytes) { } /** - * 压缩视频文件 - 使用 FFmpeg.wasm + * 压缩视频文件 - 使用 WebCodecs */ export async function compressVideo(file, onProgress = () => {}) { // 检查是否需要压缩 @@ -44,36 +43,30 @@ export async function compressVideo(file, onProgress = () => {}) { return file } - // 检查 FFmpeg 支持 - if (!isFFmpegSupported()) { + // 检查 WebCodecs 支持 + if (!isWebCodecSupported()) { throw new Error('当前浏览器不支持视频压缩功能,请使用 Chrome 或 Safari 浏览器') } try { - const { $ffmpeg } = useNuxtApp() - const ff = await $ffmpeg() - return await compressVideoWithFFmpeg(ff, file, { onProgress }) + return await compressVideoWithWebCodecs(file, { onProgress }) } catch (error) { - console.error('FFmpeg 压缩失败:', error) + console.error('WebCodecs 压缩失败:', error) throw new Error(`视频压缩失败: ${error.message}`) } } /** - * 预加载 FFmpeg(可选的性能优化) + * 预加载 WebCodecs(可选的性能优化) */ export async function preloadVideoCompressor() { try { - // FFmpeg 初始化现在通过 Nuxt 插件处理 - // 这里只需要检查支持性 - if (!isFFmpegSupported()) { - throw new Error('当前浏览器不支持 FFmpeg') + if (!isWebCodecSupported()) { + throw new Error('当前浏览器不支持 WebCodecs') } - const { $ffmpeg } = useNuxtApp() - await $ffmpeg() - return { success: true, message: 'FFmpeg 预加载成功' } + return { success: true, message: 'WebCodecs 可用' } } catch (error) { - console.warn('FFmpeg 预加载失败:', error) + console.warn('WebCodecs 预加载失败:', error) return { success: false, error: error.message } } } diff --git a/frontend_nuxt/utils/webcodecVideoCompressor.js b/frontend_nuxt/utils/webcodecVideoCompressor.js new file mode 100644 index 000000000..eef17e2e6 --- /dev/null +++ b/frontend_nuxt/utils/webcodecVideoCompressor.js @@ -0,0 +1,108 @@ +/** + * WebCodecs + MP4Box.js video compressor + * Simplified transcoding using browser WebCodecs API + */ +import { createFile } from 'mp4box' +import { UPLOAD_CONFIG } from '../config/uploadConfig.js' + +export function isWebCodecSupported() { + return ( + typeof window !== 'undefined' && + 'VideoEncoder' in window && + 'MediaStreamTrackProcessor' in window && + 'VideoFrame' in window + ) +} + +/** + * Compress a video File using WebCodecs and MP4Box.js + * @param {File} file original video file + * @param {Object} options optional callbacks + * @param {Function} options.onProgress progress callback + * @returns {Promise} compressed file + */ +export async function compressVideoWithWebCodecs(file, { onProgress = () => {} } = {}) { + if (!isWebCodecSupported()) { + throw new Error('当前浏览器不支持 WebCodecs') + } + + onProgress({ stage: 'initializing', progress: 0 }) + + const url = URL.createObjectURL(file) + const video = document.createElement('video') + video.src = url + await video.play() + video.pause() + + onProgress({ stage: 'preparing', progress: 10 }) + + const stream = video.captureStream() + const track = stream.getVideoTracks()[0] + const processor = new MediaStreamTrackProcessor({ track }) + const reader = processor.readable.getReader() + + const { width, height, frameRate = 30 } = track.getSettings() + const bitrate = UPLOAD_CONFIG.VIDEO.TARGET_BITRATE || 1_000_000 + + const chunks = [] + const encoder = new VideoEncoder({ + output: (chunk) => { + const copy = new Uint8Array(chunk.byteLength) + chunk.copyTo(copy) + chunks.push({ type: chunk.type, timestamp: chunk.timestamp, data: copy }) + }, + error: (e) => console.error('编码失败', e), + }) + + encoder.configure({ + codec: 'avc1.42001E', + width, + height, + bitrate, + framerate: frameRate, + }) + + let processed = 0 + const totalFrames = Math.ceil(video.duration * frameRate) + + while (true) { + const { done, value } = await reader.read() + if (done) break + encoder.encode(value) + value.close() + processed++ + onProgress({ stage: 'compressing', progress: Math.round((processed / totalFrames) * 80) }) + } + + await encoder.flush() + + onProgress({ stage: 'packaging', progress: 90 }) + + const mp4 = createFile() + const trackId = mp4.addTrack({ + id: 1, + type: 'avc1', + width, + height, + timescale: frameRate, + }) + + chunks.forEach((chunk) => { + mp4.addSample(trackId, chunk.data.buffer, { + duration: 1, + dts: chunk.timestamp, + cts: 0, + is_sync: chunk.type === 'key', + }) + }) + + const streamOut = mp4.getBuffer() + const outBuffer = streamOut.buffer.slice(0, streamOut.position) + const outFile = new File([outBuffer], file.name.replace(/\.[^.]+$/, '.mp4'), { + type: 'video/mp4', + }) + + onProgress({ stage: 'completed', progress: 100 }) + + return outFile +} From 90bd41e74014fc1334408098ea41f4a787f9d047 Mon Sep 17 00:00:00 2001 From: tim Date: Thu, 11 Sep 2025 17:20:08 +0800 Subject: [PATCH 02/12] Revert "feat: switch video compression to webcodecs" This reverts commit 3f35add587da23d025fa1bded3cf53cd619c07c4. --- frontend_nuxt/nuxt.config.ts | 14 + frontend_nuxt/package-lock.json | 42 ++- frontend_nuxt/package.json | 3 +- frontend_nuxt/plugins/ffmpeg.client.ts | 37 ++ frontend_nuxt/utils/ffmpegVideoCompressor.js | 327 ++++++++++++++++++ frontend_nuxt/utils/vditor.js | 7 +- frontend_nuxt/utils/videoCompressor.js | 31 +- .../utils/webcodecVideoCompressor.js | 108 ------ 8 files changed, 435 insertions(+), 134 deletions(-) create mode 100644 frontend_nuxt/plugins/ffmpeg.client.ts create mode 100644 frontend_nuxt/utils/ffmpegVideoCompressor.js delete mode 100644 frontend_nuxt/utils/webcodecVideoCompressor.js diff --git a/frontend_nuxt/nuxt.config.ts b/frontend_nuxt/nuxt.config.ts index f0adf3c98..dad43963c 100644 --- a/frontend_nuxt/nuxt.config.ts +++ b/frontend_nuxt/nuxt.config.ts @@ -1,4 +1,17 @@ import { defineNuxtConfig } from 'nuxt/config' +import { createRequire } from 'node:module' + +const require = createRequire(import.meta.url) +const appPkg = require('./package.json') as { + dependencies?: Record + devDependencies?: Record +} +const ffmpegVersion = ( + process.env.NUXT_PUBLIC_FFMPEG_VERSION || + appPkg.dependencies?.['@ffmpeg/ffmpeg'] || + appPkg.devDependencies?.['@ffmpeg/ffmpeg'] || + '0.12.15' +).replace(/^[^\d]*/, '') export default defineNuxtConfig({ devServer: { host: '0.0.0.0', @@ -16,6 +29,7 @@ export default defineNuxtConfig({ discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID || '', twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '', telegramBotId: process.env.NUXT_PUBLIC_TELEGRAM_BOT_ID || '', + ffmpegVersion, }, }, css: [ diff --git a/frontend_nuxt/package-lock.json b/frontend_nuxt/package-lock.json index a7a28be3d..39770a80c 100644 --- a/frontend_nuxt/package-lock.json +++ b/frontend_nuxt/package-lock.json @@ -6,6 +6,8 @@ "": { "name": "frontend_nuxt", "dependencies": { + "@ffmpeg/ffmpeg": "^0.12.15", + "@ffmpeg/util": "^0.12.2", "@icon-park/vue-next": "^1.4.2", "@nuxt/image": "^1.11.0", "@stomp/stompjs": "^7.0.0", @@ -19,7 +21,6 @@ "ldrs": "^1.0.0", "markdown-it": "^14.1.0", "mermaid": "^10.9.4", - "mp4box": "^2.1.1", "nanoid": "^5.1.5", "nprogress": "^0.2.0", "nuxt": "latest", @@ -998,6 +999,36 @@ "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", "license": "MIT" }, + "node_modules/@ffmpeg/ffmpeg": { + "version": "0.12.15", + "resolved": "https://registry.npmmirror.com/@ffmpeg/ffmpeg/-/ffmpeg-0.12.15.tgz", + "integrity": "sha512-1C8Obr4GsN3xw+/1Ww6PFM84wSQAGsdoTuTWPOj2OizsRDLT4CXTaVjPhkw6ARyDus1B9X/L2LiXHqYYsGnRFw==", + "license": "MIT", + "dependencies": { + "@ffmpeg/types": "^0.12.4" + }, + "engines": { + "node": ">=18.x" + } + }, + "node_modules/@ffmpeg/types": { + "version": "0.12.4", + "resolved": "https://registry.npmmirror.com/@ffmpeg/types/-/types-0.12.4.tgz", + "integrity": "sha512-k9vJQNBGTxE5AhYDtOYR5rO5fKsspbg51gbcwtbkw2lCdoIILzklulcjJfIDwrtn7XhDeF2M+THwJ2FGrLeV6A==", + "license": "MIT", + "engines": { + "node": ">=16.x" + } + }, + "node_modules/@ffmpeg/util": { + "version": "0.12.2", + "resolved": "https://registry.npmmirror.com/@ffmpeg/util/-/util-0.12.2.tgz", + "integrity": "sha512-ouyoW+4JB7WxjeZ2y6KpRvB+dLp7Cp4ro8z0HIVpZVCM7AwFlHa0c4R8Y/a4M3wMqATpYKhC7lSFHQ0T11MEDw==", + "license": "MIT", + "engines": { + "node": ">=18.x" + } + }, "node_modules/@icon-park/vue-next": { "version": "1.4.2", "resolved": "https://registry.npmmirror.com/@icon-park/vue-next/-/vue-next-1.4.2.tgz", @@ -10141,15 +10172,6 @@ "node": ">=18" } }, - "node_modules/mp4box": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mp4box/-/mp4box-2.1.1.tgz", - "integrity": "sha512-gttYFNmlCjredsdnxqNC6ho0bx6zEwOqAwSKZNQXtsBqvSN1CjtzlTLY9Kfhvt14Co8Iu+qMuOOpnPIRjvvFtw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=20.8.1" - } - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", diff --git a/frontend_nuxt/package.json b/frontend_nuxt/package.json index 935d6db93..31f873a29 100644 --- a/frontend_nuxt/package.json +++ b/frontend_nuxt/package.json @@ -13,6 +13,8 @@ }, "dependencies": { "@icon-park/vue-next": "^1.4.2", + "@ffmpeg/ffmpeg": "^0.12.15", + "@ffmpeg/util": "^0.12.2", "@nuxt/image": "^1.11.0", "@stomp/stompjs": "^7.0.0", "cropperjs": "^1.6.2", @@ -25,7 +27,6 @@ "ldrs": "^1.0.0", "markdown-it": "^14.1.0", "mermaid": "^10.9.4", - "mp4box": "^2.1.1", "nanoid": "^5.1.5", "nprogress": "^0.2.0", "nuxt": "latest", diff --git a/frontend_nuxt/plugins/ffmpeg.client.ts b/frontend_nuxt/plugins/ffmpeg.client.ts new file mode 100644 index 000000000..562a93aa2 --- /dev/null +++ b/frontend_nuxt/plugins/ffmpeg.client.ts @@ -0,0 +1,37 @@ +import { FFmpeg } from '@ffmpeg/ffmpeg' +import { toBlobURL } from '@ffmpeg/util' +import { defineNuxtPlugin, useRuntimeConfig } from 'nuxt/app' + +let ffmpeg: FFmpeg | null = null + +export default defineNuxtPlugin(() => { + const { + public: { ffmpegVersion }, + } = useRuntimeConfig() + + return { + provide: { + ffmpeg: async () => { + if (ffmpeg) return ffmpeg + + ffmpeg = new FFmpeg() + + const mtOk = + typeof crossOriginIsolated !== 'undefined' && + crossOriginIsolated && + typeof SharedArrayBuffer !== 'undefined' + + const pkg = mtOk ? '@ffmpeg/core-mt' : '@ffmpeg/core-st' + const base = `https://unpkg.com/${pkg}@${ffmpegVersion}/dist/umd` + + await ffmpeg.load({ + coreURL: await toBlobURL(`${base}/ffmpeg-core.js`, 'text/javascript'), + wasmURL: await toBlobURL(`${base}/ffmpeg-core.wasm`, 'application/wasm'), + workerURL: await toBlobURL(`${base}/ffmpeg-core.worker.js`, 'text/javascript'), + }) + + return ffmpeg + }, + }, + } +}) diff --git a/frontend_nuxt/utils/ffmpegVideoCompressor.js b/frontend_nuxt/utils/ffmpegVideoCompressor.js new file mode 100644 index 000000000..2aa155049 --- /dev/null +++ b/frontend_nuxt/utils/ffmpegVideoCompressor.js @@ -0,0 +1,327 @@ +/** + * FFmpeg.wasm 视频压缩器 + * + * 用法: + * const { $ffmpeg } = useNuxtApp() + * const ff = await $ffmpeg() // 插件里已完成 ffmpeg.load() + * const out = await compressVideoWithFFmpeg(ff, file, { onProgress, strictSize: false }) + * + * 设计要点: + * - 本文件不再负责加载/初始化,只负责转码逻辑;和 Nuxt 插件解耦。 + * - 针对【同一个 ffmpeg 实例】做串行队列,避免并发 exec 踩内存文件系统。 + * - 使用 nanoid 生成唯一文件名;日志环形缓冲;默认 CRF+VBV,可选 strictSize(two-pass)。 + * - 体积明显小于目标时直通返回,减少无谓重编码。 + */ + +import { fetchFile } from '@ffmpeg/util' +import { nanoid } from 'nanoid' +import { UPLOAD_CONFIG } from '../config/uploadConfig.js' + +/************************* + * 每实例一个串行队列 * + *************************/ +// WeakMapPromise)[], running: boolean, resolvers: {res,rej}[] }> +const queues = new WeakMap() + +function enqueueOn(instance, taskFn) { + return new Promise((res, rej) => { + let st = queues.get(instance) + if (!st) { + st = { q: [], running: false, resolvers: [] } + queues.set(instance, st) + } + st.q.push(taskFn) + st.resolvers.push({ res, rej }) + drain(instance) + }) +} + +async function drain(instance) { + const st = queues.get(instance) + if (!st || st.running) return + st.running = true + try { + while (st.q.length) { + const task = st.q.shift() + const rr = st.resolvers.shift() + try { + rr.res(await task()) + } catch (e) { + rr.rej(e) + } + } + } finally { + st.running = false + } +} + +/***************** + * 工具函数 * + *****************/ +function decideScale(widthHint) { + if (!widthHint) return { filter: null, width: null } + const evenW = widthHint % 2 === 0 ? widthHint : widthHint - 1 + return { filter: `scale=${evenW}:-2:flags=bicubic,setsar=1`, width: evenW } +} + +function calculateParamsByRatio(originalSize, targetSize) { + const ratio = Math.min(targetSize / originalSize, 1) + const crf = ratio < 0.35 ? 29 : ratio < 0.5 ? 27 : ratio < 0.7 ? 25 : 23 + const preset = ratio < 0.35 ? 'slow' : ratio < 0.5 ? 'medium' : 'veryfast' + const s = + ratio < 0.35 + ? decideScale(720) + : ratio < 0.6 + ? decideScale(960) + : ratio < 0.8 + ? decideScale(1280) + : { filter: null, width: null } + const audioBitrateK = ratio < 0.5 ? 96 : ratio < 0.7 ? 128 : 160 + const profile = s.width && s.width <= 1280 ? 'main' : 'high' + return { crf, preset, scaleFilter: s.filter, scaleWidth: s.width, audioBitrateK, profile } +} + +function makeRingLogger(capBytes = 4000) { + const buf = [] + let total = 0 + function push(s) { + if (!s) return + buf.push(s) + total += s.length + while (total > capBytes) total -= buf.shift().length + } + return { push, dump: () => buf.slice() } +} + +function parseDurationFromLogs(logs) { + // 避免正则:查找 Duration: 后的 00:00:00.xx + const text = logs.join(' ') + const idx = text.indexOf('Duration:') + if (idx === -1) return null + let i = idx + 'Duration:'.length + while (i < text.length && text[i] === ' ') i++ + function read2(start) { + const a = text.charCodeAt(start) - 48 + const b = text.charCodeAt(start + 1) - 48 + if (a < 0 || a > 9 || b < 0 || b > 9) return null + return a * 10 + b + } + const hh = read2(i) + if (hh === null) return null + i += 2 + if (text[i++] !== ':') return null + const mm = read2(i) + if (mm === null) return null + i += 2 + if (text[i++] !== ':') return null + const s1 = read2(i) + if (s1 === null) return null + i += 2 + if (text[i++] !== '.') return null + let j = i + while (j < text.length && text.charCodeAt(j) >= 48 && text.charCodeAt(j) <= 57) j++ + const frac = parseFloat('0.' + text.slice(i, j) || '0') + return hh * 3600 + mm * 60 + s1 + frac +} + +export function isFFmpegSupported() { + return typeof WebAssembly !== 'undefined' && typeof Worker !== 'undefined' +} + +/** + * 读取 ffmpeg 核心版本(通过 -version),会进入队列避免并发冲突 + */ +export async function getFFmpegInfo(ffmpegInstance) { + return enqueueOn(ffmpegInstance, async () => { + const logs = [] + const onLog = ({ type, message }) => { + if (type === 'info' || type === 'fferr') logs.push(message) + } + ffmpegInstance.on('log', onLog) + try { + await ffmpegInstance.exec(['-version']) + } finally { + ffmpegInstance.off('log', onLog) + } + const line = logs.find((l) => l.toLowerCase().includes('ffmpeg version')) || '' + const parts = line.trim().split(' ').filter(Boolean) + const version = parts.length > 2 ? parts[2] : parts[1] || null + return { version } + }) +} + +/** + * 压缩:接受一个已经 load() 完成的 ffmpeg 实例 + * @param {*} ffmpegInstance 已初始化的 FFmpeg 实例(来自 Nuxt 插件) + * @param {File|Blob} file 输入文件 + * @param {{ onProgress?:(p:{stage:string,progress:number})=>void, signal?:AbortSignal, strictSize?:boolean, targetSize?:number }} opts + */ +export async function compressVideoWithFFmpeg(ffmpegInstance, file, opts = {}) { + return enqueueOn(ffmpegInstance, () => doCompress(ffmpegInstance, file, opts)) +} + +async function doCompress(ffmpegInstance, file, opts) { + const onProgress = opts.onProgress || (() => {}) + const { signal, strictSize = false } = opts + + onProgress({ stage: 'preparing', progress: 10 }) + + const targetSize = opts.targetSize ?? UPLOAD_CONFIG?.VIDEO?.TARGET_SIZE ?? 12 * 1024 * 1024 + + // 小体积直通 + const sizeKnown = 'size' in file && typeof file.size === 'number' + if (sizeKnown && file.size <= targetSize * 0.9) { + onProgress({ stage: 'skipped', progress: 100 }) + return file + } + + const params = calculateParamsByRatio(sizeKnown ? file.size : targetSize * 2, targetSize) + const { crf, preset, scaleFilter, audioBitrateK, profile } = params + + const name = 'name' in file && typeof file.name === 'string' ? file.name : 'input.mp4' + const dot = name.lastIndexOf('.') + const outName = (dot > -1 ? name.slice(0, dot) : name) + '.mp4' + + const ext = dot > -1 ? name.slice(dot + 1).toLowerCase() : 'mp4' + const id = nanoid() + const inputName = `input-${id}.${ext}` + const outputName = `output-${id}.mp4` + const passlog = `ffpass-${id}` + + // 监听 + const ring = makeRingLogger() + const onFfmpegProgress = ({ progress: p }) => { + const adjusted = 20 + p * 70 + onProgress({ stage: 'compressing', progress: Math.min(90, adjusted) }) + } + const onFfmpegLog = ({ type, message }) => { + if (type === 'fferr' || type === 'info') ring.push(message) + } + ffmpegInstance.on('progress', onFfmpegProgress) + ffmpegInstance.on('log', onFfmpegLog) + + let aborted = false + const abortHandler = () => { + aborted = true + } + if (signal) signal.addEventListener('abort', abortHandler, { once: true }) + + try { + await ffmpegInstance.writeFile(inputName, await fetchFile(file)) + onProgress({ stage: 'analyzing', progress: 20 }) + + let durationSec = null + try { + await ffmpegInstance.exec(['-hide_banner', '-i', inputName, '-f', 'null', '-']) + durationSec = parseDurationFromLogs(ring.dump()) + } catch { + durationSec = durationSec ?? parseDurationFromLogs(ring.dump()) + } + + let videoBitrate = null + if (durationSec && sizeKnown && targetSize < file.size) { + const totalTargetBits = targetSize * 8 + const audioBits = audioBitrateK * 1000 * durationSec + const maxVideoBits = Math.max(totalTargetBits - audioBits, totalTargetBits * 0.7) + const bps = Math.max(180000, Math.floor(maxVideoBits / durationSec)) + videoBitrate = String(Math.min(bps, 5000000)) + } + + const baseArgs = [ + '-hide_banner', + '-i', + inputName, + '-c:v', + 'libx264', + '-pix_fmt', + 'yuv420p', + '-profile:v', + profile, + '-movflags', + '+faststart', + '-preset', + preset, + '-c:a', + 'aac', + '-b:a', + `${audioBitrateK}k`, + '-ac', + '2', + ] + if (scaleFilter) baseArgs.push('-vf', scaleFilter) + + const onePassArgs = [...baseArgs, '-crf', String(crf)] + if (videoBitrate) + onePassArgs.push('-maxrate', videoBitrate, '-bufsize', String(parseInt(videoBitrate, 10) * 2)) + + const twoPassFirst = [ + '-y', + '-hide_banner', + '-i', + inputName, + '-c:v', + 'libx264', + '-b:v', + `${videoBitrate || '1000000'}`, + '-pass', + '1', + '-passlogfile', + passlog, + '-an', + '-f', + 'mp4', + '/dev/null', + ] + const twoPassSecond = [ + ...baseArgs, + '-b:v', + `${videoBitrate || '1000000'}`, + '-pass', + '2', + '-passlogfile', + passlog, + outputName, + ] + + if (aborted) throw new DOMException('Aborted', 'AbortError') + + if (!strictSize) { + await ffmpegInstance.exec([...onePassArgs, outputName]) + } else { + if (!videoBitrate) videoBitrate = '1000000' + await ffmpegInstance.exec(twoPassFirst) + onProgress({ stage: 'second-pass', progress: 85 }) + await ffmpegInstance.exec(twoPassSecond) + } + + if (aborted) throw new DOMException('Aborted', 'AbortError') + + onProgress({ stage: 'finalizing', progress: 95 }) + const out = await ffmpegInstance.readFile(outputName) + + const mime = 'video/mp4' + const blob = new Blob([out], { type: mime }) + const hasFileCtor = typeof File === 'function' + const result = hasFileCtor ? new File([blob], outName, { type: mime }) : blob + + onProgress({ stage: 'completed', progress: 100 }) + return result + } finally { + try { + await ffmpegInstance.deleteFile(inputName) + } catch {} + try { + await ffmpegInstance.deleteFile(outputName) + } catch {} + try { + await ffmpegInstance.deleteFile(`${passlog}-0.log`) + } catch {} + try { + await ffmpegInstance.deleteFile(`${passlog}-0.log.mbtree`) + } catch {} + + ffmpegInstance.off('progress', onFfmpegProgress) + ffmpegInstance.off('log', onFfmpegLog) + if (signal) signal.removeEventListener('abort', abortHandler) + } +} diff --git a/frontend_nuxt/utils/vditor.js b/frontend_nuxt/utils/vditor.js index dbe6ae2bb..c2c6d7d9a 100644 --- a/frontend_nuxt/utils/vditor.js +++ b/frontend_nuxt/utils/vditor.js @@ -121,13 +121,14 @@ export function createVditor(editorId, options = {}) { vditor.tip('视频压缩中...', 0) vditor.disabled() - // 使用 WebCodecs 压缩视频 + // 使用 FFmpeg 压缩视频 processedFile = await compressVideo(file, (progress) => { const messages = { - initializing: '初始化编解码器', + initializing: '初始化 FFmpeg', preparing: '准备压缩', + analyzing: '分析视频', compressing: '压缩中', - packaging: '封装中', + finalizing: '完成压缩', completed: '压缩完成', } const message = messages[progress.stage] || progress.stage diff --git a/frontend_nuxt/utils/videoCompressor.js b/frontend_nuxt/utils/videoCompressor.js index 943551283..584d8a487 100644 --- a/frontend_nuxt/utils/videoCompressor.js +++ b/frontend_nuxt/utils/videoCompressor.js @@ -1,10 +1,11 @@ /** - * 基于 WebCodecs + MP4Box.js 的视频压缩工具 + * 基于 FFmpeg.wasm 的视频压缩工具 * 专为现代浏览器 (Chrome/Safari) 优化 */ import { UPLOAD_CONFIG } from '../config/uploadConfig.js' -import { compressVideoWithWebCodecs, isWebCodecSupported } from './webcodecVideoCompressor.js' +import { compressVideoWithFFmpeg, isFFmpegSupported } from './ffmpegVideoCompressor.js' +import { useNuxtApp } from '#app' // 导出配置供外部使用 export const VIDEO_CONFIG = UPLOAD_CONFIG.VIDEO @@ -33,7 +34,7 @@ export function formatFileSize(bytes) { } /** - * 压缩视频文件 - 使用 WebCodecs + * 压缩视频文件 - 使用 FFmpeg.wasm */ export async function compressVideo(file, onProgress = () => {}) { // 检查是否需要压缩 @@ -43,30 +44,36 @@ export async function compressVideo(file, onProgress = () => {}) { return file } - // 检查 WebCodecs 支持 - if (!isWebCodecSupported()) { + // 检查 FFmpeg 支持 + if (!isFFmpegSupported()) { throw new Error('当前浏览器不支持视频压缩功能,请使用 Chrome 或 Safari 浏览器') } try { - return await compressVideoWithWebCodecs(file, { onProgress }) + const { $ffmpeg } = useNuxtApp() + const ff = await $ffmpeg() + return await compressVideoWithFFmpeg(ff, file, { onProgress }) } catch (error) { - console.error('WebCodecs 压缩失败:', error) + console.error('FFmpeg 压缩失败:', error) throw new Error(`视频压缩失败: ${error.message}`) } } /** - * 预加载 WebCodecs(可选的性能优化) + * 预加载 FFmpeg(可选的性能优化) */ export async function preloadVideoCompressor() { try { - if (!isWebCodecSupported()) { - throw new Error('当前浏览器不支持 WebCodecs') + // FFmpeg 初始化现在通过 Nuxt 插件处理 + // 这里只需要检查支持性 + if (!isFFmpegSupported()) { + throw new Error('当前浏览器不支持 FFmpeg') } - return { success: true, message: 'WebCodecs 可用' } + const { $ffmpeg } = useNuxtApp() + await $ffmpeg() + return { success: true, message: 'FFmpeg 预加载成功' } } catch (error) { - console.warn('WebCodecs 预加载失败:', error) + console.warn('FFmpeg 预加载失败:', error) return { success: false, error: error.message } } } diff --git a/frontend_nuxt/utils/webcodecVideoCompressor.js b/frontend_nuxt/utils/webcodecVideoCompressor.js deleted file mode 100644 index eef17e2e6..000000000 --- a/frontend_nuxt/utils/webcodecVideoCompressor.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * WebCodecs + MP4Box.js video compressor - * Simplified transcoding using browser WebCodecs API - */ -import { createFile } from 'mp4box' -import { UPLOAD_CONFIG } from '../config/uploadConfig.js' - -export function isWebCodecSupported() { - return ( - typeof window !== 'undefined' && - 'VideoEncoder' in window && - 'MediaStreamTrackProcessor' in window && - 'VideoFrame' in window - ) -} - -/** - * Compress a video File using WebCodecs and MP4Box.js - * @param {File} file original video file - * @param {Object} options optional callbacks - * @param {Function} options.onProgress progress callback - * @returns {Promise} compressed file - */ -export async function compressVideoWithWebCodecs(file, { onProgress = () => {} } = {}) { - if (!isWebCodecSupported()) { - throw new Error('当前浏览器不支持 WebCodecs') - } - - onProgress({ stage: 'initializing', progress: 0 }) - - const url = URL.createObjectURL(file) - const video = document.createElement('video') - video.src = url - await video.play() - video.pause() - - onProgress({ stage: 'preparing', progress: 10 }) - - const stream = video.captureStream() - const track = stream.getVideoTracks()[0] - const processor = new MediaStreamTrackProcessor({ track }) - const reader = processor.readable.getReader() - - const { width, height, frameRate = 30 } = track.getSettings() - const bitrate = UPLOAD_CONFIG.VIDEO.TARGET_BITRATE || 1_000_000 - - const chunks = [] - const encoder = new VideoEncoder({ - output: (chunk) => { - const copy = new Uint8Array(chunk.byteLength) - chunk.copyTo(copy) - chunks.push({ type: chunk.type, timestamp: chunk.timestamp, data: copy }) - }, - error: (e) => console.error('编码失败', e), - }) - - encoder.configure({ - codec: 'avc1.42001E', - width, - height, - bitrate, - framerate: frameRate, - }) - - let processed = 0 - const totalFrames = Math.ceil(video.duration * frameRate) - - while (true) { - const { done, value } = await reader.read() - if (done) break - encoder.encode(value) - value.close() - processed++ - onProgress({ stage: 'compressing', progress: Math.round((processed / totalFrames) * 80) }) - } - - await encoder.flush() - - onProgress({ stage: 'packaging', progress: 90 }) - - const mp4 = createFile() - const trackId = mp4.addTrack({ - id: 1, - type: 'avc1', - width, - height, - timescale: frameRate, - }) - - chunks.forEach((chunk) => { - mp4.addSample(trackId, chunk.data.buffer, { - duration: 1, - dts: chunk.timestamp, - cts: 0, - is_sync: chunk.type === 'key', - }) - }) - - const streamOut = mp4.getBuffer() - const outBuffer = streamOut.buffer.slice(0, streamOut.position) - const outFile = new File([outBuffer], file.name.replace(/\.[^.]+$/, '.mp4'), { - type: 'video/mp4', - }) - - onProgress({ stage: 'completed', progress: 100 }) - - return outFile -} From 9421d004d4f4ba182cedd8cc9c58e58a930e2f87 Mon Sep 17 00:00:00 2001 From: jiahaosheng Date: Thu, 11 Sep 2025 17:27:54 +0800 Subject: [PATCH 03/12] =?UTF-8?q?feat(MessageEditor):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=8F=91=E9=80=81=E6=B6=88=E6=81=AF=E7=9A=84=E5=BF=AB=E6=8D=B7?= =?UTF-8?q?=E9=94=AE=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend_nuxt/components/MessageEditor.vue | 45 +++++++++++++++++++++- frontend_nuxt/utils/is.js | 28 ++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 frontend_nuxt/utils/is.js diff --git a/frontend_nuxt/components/MessageEditor.vue b/frontend_nuxt/components/MessageEditor.vue index ae3fbc9b3..e42331dd8 100644 --- a/frontend_nuxt/components/MessageEditor.vue +++ b/frontend_nuxt/components/MessageEditor.vue @@ -5,7 +5,10 @@
- +
@@ -21,6 +24,8 @@ import { getEditorTheme as getEditorThemeUtil, getPreviewTheme as getPreviewThemeUtil, } from '~/utils/vditor' +import { useIsMobile } from '~/utils/screen' +import { isMac } from '~/utils/is' import '~/assets/global.css' export default { @@ -44,6 +49,7 @@ export default { const vditorInstance = ref(null) const text = ref('') const editorId = ref(props.editorId) + const isMobile = useIsMobile() if (!editorId.value) { editorId.value = 'editor-' + useId() } @@ -84,6 +90,28 @@ export default { applyTheme() }, }) + + // 不是手机的情况下不添加快捷键 + if (!isMobile.value) { + // 添加快捷键监听 (Ctrl+Enter 或 Cmd+Enter) + const handleKeydown = (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + e.preventDefault() + submit() + } + } + + const el = document.getElementById(editorId.value) + if (el) { + el.addEventListener('keydown', handleKeydown) + } + + onUnmounted(() => { + if (el) { + el.removeEventListener('keydown', handleKeydown) + } + }) + } }) onUnmounted(() => { @@ -121,7 +149,7 @@ export default { }, ) - return { submit, isDisabled, editorId } + return { submit, isDisabled, editorId, isMac, isMobile } }, } @@ -168,4 +196,17 @@ export default { .message-submit:not(.disabled):hover { background-color: var(--primary-color-hover); } + +/** 评论按钮快捷键样式 */ +.shortcut-icon { + padding: 2px 6px; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + line-height: 1.2; + background-color: rgba(0, 0, 0, 0.25); +} +.comment-submit.disabled .shortcut-icon { + background-color: rgba(0, 0, 0, 0); +} diff --git a/frontend_nuxt/utils/is.js b/frontend_nuxt/utils/is.js new file mode 100644 index 000000000..e286a6a45 --- /dev/null +++ b/frontend_nuxt/utils/is.js @@ -0,0 +1,28 @@ +export const isClient = typeof window !== 'undefined' && typeof document !== 'undefined' + +export const isMac = getIsMac() + +function getIsMac() { + if (!isClient) { + return false + } + + try { + // 优先使用现代浏览器的 navigator.userAgentData API + if (navigator.userAgentData && navigator.userAgentData.platform) { + return navigator.userAgentData.platform === 'macOS' + } + + // 降级到传统的 User-Agent 检测 + if (navigator.userAgent) { + return /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent) + } + + // 默认返回false + return false + } catch (error) { + // 异常处理,记录错误并返回默认值 + console.warn('检测Mac设备时发生错误:', error) + return false + } +} From dc6478527914faf2d81b34e084101e51d8a59665 Mon Sep 17 00:00:00 2001 From: jiahaosheng Date: Thu, 11 Sep 2025 17:53:08 +0800 Subject: [PATCH 04/12] feat: rename is.js to device.js --- frontend_nuxt/components/MessageEditor.vue | 2 +- frontend_nuxt/utils/{is.js => device.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename frontend_nuxt/utils/{is.js => device.js} (100%) diff --git a/frontend_nuxt/components/MessageEditor.vue b/frontend_nuxt/components/MessageEditor.vue index e42331dd8..c1ee6e7d5 100644 --- a/frontend_nuxt/components/MessageEditor.vue +++ b/frontend_nuxt/components/MessageEditor.vue @@ -25,7 +25,7 @@ import { getPreviewTheme as getPreviewThemeUtil, } from '~/utils/vditor' import { useIsMobile } from '~/utils/screen' -import { isMac } from '~/utils/is' +import { isMac } from '~/utils/device' import '~/assets/global.css' export default { diff --git a/frontend_nuxt/utils/is.js b/frontend_nuxt/utils/device.js similarity index 100% rename from frontend_nuxt/utils/is.js rename to frontend_nuxt/utils/device.js From ea079e8b8a7ef247cf5bf5db1aae0d6b06de46ea Mon Sep 17 00:00:00 2001 From: tim Date: Thu, 11 Sep 2025 18:36:47 +0800 Subject: [PATCH 05/12] =?UTF-8?q?fix:=20=E7=AE=80=E5=8C=96ffmpeg=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend_nuxt/.env.example | 5 - frontend_nuxt/config/uploadConfig.js | 45 +- frontend_nuxt/nuxt.config.ts | 36 +- frontend_nuxt/package-lock.json | 2325 +----------------------- frontend_nuxt/package.json | 2 +- frontend_nuxt/plugins/ffmpeg.client.ts | 17 +- 6 files changed, 9 insertions(+), 2421 deletions(-) diff --git a/frontend_nuxt/.env.example b/frontend_nuxt/.env.example index 2990fd052..dadb36387 100644 --- a/frontend_nuxt/.env.example +++ b/frontend_nuxt/.env.example @@ -17,8 +17,3 @@ NUXT_PUBLIC_GITHUB_CLIENT_ID=Ov23liVkO1NPAX5JyWxJ NUXT_PUBLIC_DISCORD_CLIENT_ID=1394985417044000779 NUXT_PUBLIC_TWITTER_CLIENT_ID=ZTRTU05KSk9KTTJrTTdrVC1tc1E6MTpjaQ NUXT_PUBLIC_TELEGRAM_BOT_ID=8450237135 - -# 视频压缩配置 - FFmpeg.wasm 专用 -# 支持 Chrome 60+ 和 Safari 11.1+ -NUXT_PUBLIC_VIDEO_MAX_SIZE=52428800 # 50MB (字节) -NUXT_PUBLIC_VIDEO_TARGET_SIZE=20971520 # 20MB (字节) \ No newline at end of file diff --git a/frontend_nuxt/config/uploadConfig.js b/frontend_nuxt/config/uploadConfig.js index e98f13d70..0172682b0 100644 --- a/frontend_nuxt/config/uploadConfig.js +++ b/frontend_nuxt/config/uploadConfig.js @@ -3,53 +3,12 @@ * 专注于 FFmpeg.wasm 视频压缩,支持 Chrome/Safari */ -// 声明全局变量以避免 TypeScript 错误 -/* global useRuntimeConfig */ - -// 简化的环境变量读取功能 -function getEnvNumber(key, defaultValue) { - if (typeof window !== 'undefined') { - // 客户端:尝试从 Nuxt 环境获取 - try { - // 使用 globalThis 避免直接引用未定义的变量 - const nuxtApp = globalThis.$nuxt || globalThis.nuxtApp - if (nuxtApp && nuxtApp.$config) { - const value = nuxtApp.$config.public?.[key.replace('NUXT_PUBLIC_', '').toLowerCase()] - return value ? Number(value) : defaultValue - } - return defaultValue - } catch { - return defaultValue - } - } - // 服务端:从 process.env 获取 - return process.env[key] ? Number(process.env[key]) : defaultValue -} - -function getEnvBoolean(key, defaultValue) { - if (typeof window !== 'undefined') { - try { - // 使用 globalThis 避免直接引用未定义的变量 - const nuxtApp = globalThis.$nuxt || globalThis.nuxtApp - if (nuxtApp && nuxtApp.$config) { - const value = nuxtApp.$config.public?.[key.replace('NUXT_PUBLIC_', '').toLowerCase()] - return value === 'true' || value === true - } - return defaultValue - } catch { - return defaultValue - } - } - const envValue = process.env[key] - return envValue ? envValue === 'true' : defaultValue -} - export const UPLOAD_CONFIG = { // 视频文件配置 - 专为 FFmpeg.wasm 优化 VIDEO: { // 文件大小限制 (字节) - MAX_SIZE: getEnvNumber('NUXT_PUBLIC_VIDEO_MAX_SIZE', 20 * 1024 * 1024), // 5MB - TARGET_SIZE: getEnvNumber('NUXT_PUBLIC_VIDEO_TARGET_SIZE', 5 * 1024 * 1024), // 5MB + MAX_SIZE: 20 * 1024 * 1024, + TARGET_SIZE: 5 * 1024 * 1024, // 5MB // 支持的输入格式 (FFmpeg.wasm 支持更多格式) SUPPORTED_FORMATS: ['mp4', 'webm', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'm4v', 'ogv'], diff --git a/frontend_nuxt/nuxt.config.ts b/frontend_nuxt/nuxt.config.ts index dad43963c..5f4264567 100644 --- a/frontend_nuxt/nuxt.config.ts +++ b/frontend_nuxt/nuxt.config.ts @@ -1,17 +1,5 @@ import { defineNuxtConfig } from 'nuxt/config' -import { createRequire } from 'node:module' -const require = createRequire(import.meta.url) -const appPkg = require('./package.json') as { - dependencies?: Record - devDependencies?: Record -} -const ffmpegVersion = ( - process.env.NUXT_PUBLIC_FFMPEG_VERSION || - appPkg.dependencies?.['@ffmpeg/ffmpeg'] || - appPkg.devDependencies?.['@ffmpeg/ffmpeg'] || - '0.12.15' -).replace(/^[^\d]*/, '') export default defineNuxtConfig({ devServer: { host: '0.0.0.0', @@ -29,7 +17,6 @@ export default defineNuxtConfig({ discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID || '', twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '', telegramBotId: process.env.NUXT_PUBLIC_TELEGRAM_BOT_ID || '', - ffmpegVersion, }, }, css: [ @@ -110,26 +97,9 @@ export default defineNuxtConfig({ }, }, vite: { - build: { - // increase warning limit and split large libraries into separate chunks - // chunkSizeWarningLimit: 1024, - // rollupOptions: { - // output: { - // manualChunks(id) { - // if (id.includes('node_modules')) { - // if (id.includes('vditor')) { - // return 'vditor' - // } - // if (id.includes('echarts')) { - // return 'echarts' - // } - // if (id.includes('highlight.js')) { - // return 'highlight' - // } - // } - // }, - // }, - // }, + optimizeDeps: { + include: ['@ffmpeg/ffmpeg', '@ffmpeg/util'], }, + build: {}, }, }) diff --git a/frontend_nuxt/package-lock.json b/frontend_nuxt/package-lock.json index 39770a80c..8493a2590 100644 --- a/frontend_nuxt/package-lock.json +++ b/frontend_nuxt/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "frontend_nuxt", "dependencies": { - "@ffmpeg/ffmpeg": "^0.12.15", + "@ffmpeg/ffmpeg": "^0.12.10", "@ffmpeg/util": "^0.12.2", "@icon-park/vue-next": "^1.4.2", "@nuxt/image": "^1.11.0", @@ -561,70 +561,6 @@ "license": "0BSD", "optional": true }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", @@ -641,342 +577,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@fastify/accept-negotiator": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", @@ -1064,28 +664,6 @@ "@img/sharp-libvips-darwin-arm64": "1.2.0" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" - } - }, "node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", @@ -1102,364 +680,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.4.4" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@ioredis/commands": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.0.tgz", @@ -1783,70 +1003,6 @@ "node": ">=6.9.0" } }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/darwin-arm64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", @@ -1863,326 +1019,6 @@ "node": ">=18" } }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@netlify/zip-it-and-ship-it/node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@netlify/zip-it-and-ship-it/node_modules/@netlify/serverless-functions-api": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@netlify/serverless-functions-api/-/serverless-functions-api-2.3.0.tgz", @@ -2738,22 +1574,6 @@ "node": ">=18.12.0" } }, - "node_modules/@oxc-minify/binding-android-arm64": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm64/-/binding-android-arm64-0.80.0.tgz", - "integrity": "sha512-OLelUqrLkSJwNyjLZHgpKy9n0+zHQiMX8A0GFovJIwhgfPxjT/mt2JMnGkSoDlTnf9cw6nvALFzCsJZLTyl8gg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@oxc-minify/binding-darwin-arm64": { "version": "0.80.0", "resolved": "https://registry.npmjs.org/@oxc-minify/binding-darwin-arm64/-/binding-darwin-arm64-0.80.0.tgz", @@ -2770,230 +1590,6 @@ "node": ">=14.0.0" } }, - "node_modules/@oxc-minify/binding-darwin-x64": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-darwin-x64/-/binding-darwin-x64-0.80.0.tgz", - "integrity": "sha512-jKnRVtwVhspd8djNSQMICOZe6gQBwXTcfHylZ2Azw4ZXvqTyxDqgcEGgx0WyaqvUTLHdX42nJCHRHHy6MOVPOg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-minify/binding-freebsd-x64": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-freebsd-x64/-/binding-freebsd-x64-0.80.0.tgz", - "integrity": "sha512-iO7KjJsFpDtG5w8T6twTxLsvffn8PsjBbBUwjzVPfSD4YlsHDd0GjIVYcP+1TXzLRlV4zWmd67SOBnNyreSGBg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-minify/binding-linux-arm-gnueabihf": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.80.0.tgz", - "integrity": "sha512-uwBdietv8USofOUAOcxyta14VbcJiFizQUMuCB9sLkK+Nh/CV5U2SVjsph5HlARGVu8V2DF+FXROD6sTl9DLiA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-minify/binding-linux-arm-musleabihf": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.80.0.tgz", - "integrity": "sha512-6QAWCjH9in7JvpHRxX8M1IEkf+Eot82Q02xmikcACyJag26196XdVq2T9ITcwFtliozYxYP6yPQ5OzLoeeqdmg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-minify/binding-linux-arm64-gnu": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.80.0.tgz", - "integrity": "sha512-1PxO983GNFSyvY6lpYpH3uA/5NHuei7CHExe+NSB+ZgQ1T/iBMjXxRml1Woedvi8odSSpZlivZxBiEojIcnfqw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-minify/binding-linux-arm64-musl": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.80.0.tgz", - "integrity": "sha512-D2j5L9Z4OO42We0Lo2GkXT/AaNikzZJ8KZ9V2VVwu7kofI4RsO8kSu8ydWlqRlRdiAprmUpRZU/pNW0ZA7A68w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-minify/binding-linux-riscv64-gnu": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.80.0.tgz", - "integrity": "sha512-2AztlLcio5OGil70wjRLbxbjlfS1yCTzO+CYan49vfUOCXpwSWwwLD2WDzFokhEXAzf8epbbu7pruYk8qorRRg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-minify/binding-linux-s390x-gnu": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.80.0.tgz", - "integrity": "sha512-5GMKARe4gYHhA7utM8qOgv3WM7KAXGZGG3Jhvk4UQSRBp0v6PKFmHmz8Q93+Ep8w1m4NqRL30Zk9CZHMH/qi5g==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-minify/binding-linux-x64-gnu": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.80.0.tgz", - "integrity": "sha512-iw45N+OVnPioRQXLHfrsqEcTpydcGSHLphilS3aSpc4uVKnOqCybskKnbEnxsIJqHWbzDZeJgzuRuQa7EhNcqg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-minify/binding-linux-x64-musl": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-linux-x64-musl/-/binding-linux-x64-musl-0.80.0.tgz", - "integrity": "sha512-4+dhYznVM+L9Jh855JBbqVyDjwi3p8rpL7RfgN+Ee1oQMaZl2ZPy2shS1Kj56Xr5haTTVGdRKcIqTU8SuF37UQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-minify/binding-wasm32-wasi": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-wasm32-wasi/-/binding-wasm32-wasi-0.80.0.tgz", - "integrity": "sha512-flADFeNwC1/XsBBsESAigsJZyONEBloQO86Z38ZNzLSuMmpGRdwB9gUwlPCQgDRND/aB+tvR29hKTSuQoS3yrg==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.0.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-minify/binding-win32-arm64-msvc": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.80.0.tgz", - "integrity": "sha512-wFjaEHzczIG9GqnL4c4C3PoThzf1640weQ1eEjh96TnHVdZmiNT5lpGoziJhO/c+g9+6sNrTdz9sqsiVgKwdOg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-minify/binding-win32-x64-msvc": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-minify/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.80.0.tgz", - "integrity": "sha512-PjMi5B3MvOmfZk5LTie6g3RHhhujFwgR4VbCrWUNNwSzdxzy3dULPT4PWGVbpTas/QLJzXs/CXlQfnaMeJZHKQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-parser/binding-android-arm64": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.80.0.tgz", - "integrity": "sha512-H0S4QTRFhct1uO1ZOnzGQAoHSJVHCyZa+oivovHkbqA0z271ppRkXmJuLfjW+9CBW0577JNAhjTflKUDpCO4lg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@oxc-parser/binding-darwin-arm64": { "version": "0.80.0", "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.80.0.tgz", @@ -3010,214 +1606,6 @@ "node": ">=20.0.0" } }, - "node_modules/@oxc-parser/binding-darwin-x64": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.80.0.tgz", - "integrity": "sha512-h7wRo10ywI2vLz9VljFeIaUh9u7l2l3kvF6FAteY3cPqbCA6JYUZGJaykhMqTxJoG6wrzf35sMA2ubvq67iAMA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-freebsd-x64": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.80.0.tgz", - "integrity": "sha512-KcJ+8w/wVwd/XfDmgA9QZJAWML3vPu2O2Y8XRkf3U9VsN5n8cZ5PXMbH4NBSb3O7ctdDSvwnnuApLOz3sTHsUw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.80.0.tgz", - "integrity": "sha512-5OCRxV5fX5RkVqsag55m4EFeudSZ0nSMYXgdtfR/5JZSiYmIYyPycafNNa52liqC2gx27vzrDRE4FdlG+5fhww==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.80.0.tgz", - "integrity": "sha512-kMa2PeA2GHMhvV617WdFzDAWCo2A00knPEe6rxFUO/Gr8TTLv1/LlEY6UqGseWrRfkkhFiAO496nRPW/6B5DCg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm64-gnu": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.80.0.tgz", - "integrity": "sha512-y2NEhbFfKPdOkf3ZR/3xwJFJVji6IKxwXKHUN4bEdqpcO0tkXSCiP0MzTxjEY6ql2/MXdkqK0Ym92dYsRsgsyg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-arm64-musl": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.80.0.tgz", - "integrity": "sha512-j3tKausSXwHS/Ej6ct2dmKJtw0UIME2XJmj6QfPT6LyUSNTndj4yXRXuMSrCOrX9/0qH9GhmqeL9ouU27dQRFw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.80.0.tgz", - "integrity": "sha512-h+uPvyTcpTFd946fGPU57sZeec2qHPUYQRZeXHB2uuZjps+9pxQ5zIz0EBM/JgBtnwdtoR93RAu1YNAVbqY5Zw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-s390x-gnu": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.80.0.tgz", - "integrity": "sha512-+u74hV+WwCPL4UBNOJaIGRozTCfZ7pM5JCEe8zAlMkKexftUzbtvW02314bVD9bqoRAL3Gg6jcZrjNjwDX2FwQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-x64-gnu": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.80.0.tgz", - "integrity": "sha512-N9UGnWVWMlOJH+6550tqyBxd9qkMd0f4m+YRA0gly6efJTuLbPQpjkJm7pJbMu+GULcvSJ/Y0bkMAIQTtwP0vQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-linux-x64-musl": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.80.0.tgz", - "integrity": "sha512-l2N/GlFEri27QBMi0e53V/SlpQotIvHbz+rZZG/EO+vn58ZEr0eTG+PjJoOY/T8+TQb8nrCtRe4S/zNDpV6zSQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-wasm32-wasi": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.80.0.tgz", - "integrity": "sha512-5iEwQqMXU1HiRlWuD3f+8N2O3qWhS+nOFEAWgE3sjMUnTtILPJETYhaGBPqqPWg1iRO3+hE1lEBCdI91GS1CUQ==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.0.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-parser/binding-win32-arm64-msvc": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.80.0.tgz", - "integrity": "sha512-HedSH/Db7OFR2SugTbuawaV1vjgUjCXzxPquow/1FLtpRT2wASbMaRRbyD/h2n4DJ8V2zGqnV8Q+vic+VNvnKg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@oxc-parser/binding-win32-x64-msvc": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.80.0.tgz", - "integrity": "sha512-SSiM0m7jG5yxVf0ivy1rF8OuTJo8ITgp1ccp2aqPZG6Qyl5QiVpf8HI1X5AvPFxts2B4Bv8U3Dip+FobqBkwcw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@oxc-project/types": { "version": "0.80.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.80.0.tgz", @@ -3227,22 +1615,6 @@ "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@oxc-transform/binding-android-arm64": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-transform/binding-android-arm64/-/binding-android-arm64-0.80.0.tgz", - "integrity": "sha512-HAK6zIUOteptOsSRqoGu41cez7kj/OPJqBGdgdP6FFh2RFcRfh0vqefjgF69af7TjzsRxVF8itiWvFsJHrIFoA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@oxc-transform/binding-darwin-arm64": { "version": "0.80.0", "resolved": "https://registry.npmjs.org/@oxc-transform/binding-darwin-arm64/-/binding-darwin-arm64-0.80.0.tgz", @@ -3259,214 +1631,6 @@ "node": ">=14.0.0" } }, - "node_modules/@oxc-transform/binding-darwin-x64": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-transform/binding-darwin-x64/-/binding-darwin-x64-0.80.0.tgz", - "integrity": "sha512-MWmDTJszdO3X2LvbvIZocdfJnb/wjr3zhU99IlruwxsFfVNHbl03091bXi1ABsV5dyU+47V/A5jG3xOtg5X0vQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-transform/binding-freebsd-x64": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-transform/binding-freebsd-x64/-/binding-freebsd-x64-0.80.0.tgz", - "integrity": "sha512-fKuwj/iBfjfGePjcR9+j2TQ/7RlrUIT4ir/OAcHWYJ/kvxp4XY/juKYXo4lks/MW/dwe+UR1Lp6xiCQBuxpyIg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-transform/binding-linux-arm-gnueabihf": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.80.0.tgz", - "integrity": "sha512-R0QdfKiV+ZFiM28UnyylOEtTBFjAb4XuHvQltUSUpylXXIbGd+0Z1WF5lY3Z776Vy00HWhYj/Vo03rhvjdVDTA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-transform/binding-linux-arm-musleabihf": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.80.0.tgz", - "integrity": "sha512-hIfp4LwyQMRhsY9ptx4UleffoY9wZofTmnHFhZTMdb/hoE97Vuqw7Ub2cLcWMu0FYHIX8zXCMd1CJjs2MV1X3w==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-transform/binding-linux-arm64-gnu": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.80.0.tgz", - "integrity": "sha512-mOYGji1m55BD2vV5m1qnrXbdqyPp/AU9p1Rn+0hM2zkE3pVkETCPvLevSvt4rHQZBZFIWeRGo47QNsNQyaZBsg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-transform/binding-linux-arm64-musl": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.80.0.tgz", - "integrity": "sha512-kBBCQwr1GCkr/b0iXH+ijsg+CSPCAMSV2tu4LmG2PFaxBnZilMYfUyWHCAiskbbUADikecUfwX6hHIaQoMaixg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-transform/binding-linux-riscv64-gnu": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.80.0.tgz", - "integrity": "sha512-8CGJhHoD2Ttw8HtCNd/IWnGtL0Nsn448L2hZJtbDDGVUZUF4bbZFdXPnRt0QrEbupywoH6InN6q2imLous6xnw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-transform/binding-linux-s390x-gnu": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.80.0.tgz", - "integrity": "sha512-V/Lb6m5loWzvdB/qo6eYvVXidQku/PA706JbeE/PPCup8At+BwOXnZjktv7LDxrpuqnO32tZDHUUc9Y3bzOEBw==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-transform/binding-linux-x64-gnu": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.80.0.tgz", - "integrity": "sha512-03hHW04MQNb+ak27xo79nUkMjVu6146TNgeSapcDRATH4R0YMmXB2oPQK1K2nuBJzVZjBjH7Bus/I7tR3JasAg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-transform/binding-linux-x64-musl": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-transform/binding-linux-x64-musl/-/binding-linux-x64-musl-0.80.0.tgz", - "integrity": "sha512-BkXniuuHpo9cR2S3JDKIvmUrNvmm335owGW4rfp07HjVUsbq9e7bSnvOnyA3gXGdrPR2IgCWGi5nnXk2NN5Q0A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-transform/binding-wasm32-wasi": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-transform/binding-wasm32-wasi/-/binding-wasm32-wasi-0.80.0.tgz", - "integrity": "sha512-jfRRXLtfSgTeJXBHj6qb+HHUd6hmYcyUNMBcTY8/k+JVsx0ThfrmCIufNlSJTt1zB+ugnMVMuQGeB0oF+aa86w==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.0.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-transform/binding-win32-arm64-msvc": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-transform/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.80.0.tgz", - "integrity": "sha512-bofcVhlAV1AKzbE0TgDH+h813pbwWwwRhN6tv/hD4qEuWh/qEjv8Xb3Ar15xfBfyLI53FoJascuaJAFzX+IN9A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-transform/binding-win32-x64-msvc": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@oxc-transform/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.80.0.tgz", - "integrity": "sha512-MT6hQo9Kw/VuQUfX0fc0OpUdZesQruT0UNY9hxIcqcli7pbxMrvFBjkXo7oUb2151s/n+F4fyQOWvaR6zwxtDA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -3502,26 +1666,6 @@ "@parcel/watcher-win32-x64": "2.5.1" } }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@parcel/watcher-darwin-arm64": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", @@ -3542,166 +1686,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@parcel/watcher-wasm": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-wasm/-/watcher-wasm-2.5.1.tgz", @@ -3728,66 +1712,6 @@ "inBundle": true, "license": "MIT" }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4098,32 +2022,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz", - "integrity": "sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.49.0.tgz", - "integrity": "sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.49.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz", @@ -4137,227 +2035,6 @@ "darwin" ] }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.49.0.tgz", - "integrity": "sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.49.0.tgz", - "integrity": "sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.49.0.tgz", - "integrity": "sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.49.0.tgz", - "integrity": "sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.49.0.tgz", - "integrity": "sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.49.0.tgz", - "integrity": "sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.49.0.tgz", - "integrity": "sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.49.0.tgz", - "integrity": "sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.49.0.tgz", - "integrity": "sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.49.0.tgz", - "integrity": "sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.49.0.tgz", - "integrity": "sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.49.0.tgz", - "integrity": "sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.49.0.tgz", - "integrity": "sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.49.0.tgz", - "integrity": "sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.49.0.tgz", - "integrity": "sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.49.0.tgz", - "integrity": "sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.49.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.49.0.tgz", - "integrity": "sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@sindresorhus/is": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz", diff --git a/frontend_nuxt/package.json b/frontend_nuxt/package.json index 31f873a29..43c4f2b60 100644 --- a/frontend_nuxt/package.json +++ b/frontend_nuxt/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@icon-park/vue-next": "^1.4.2", - "@ffmpeg/ffmpeg": "^0.12.15", + "@ffmpeg/ffmpeg": "^0.12.10", "@ffmpeg/util": "^0.12.2", "@nuxt/image": "^1.11.0", "@stomp/stompjs": "^7.0.0", diff --git a/frontend_nuxt/plugins/ffmpeg.client.ts b/frontend_nuxt/plugins/ffmpeg.client.ts index 562a93aa2..b4d6eee6b 100644 --- a/frontend_nuxt/plugins/ffmpeg.client.ts +++ b/frontend_nuxt/plugins/ffmpeg.client.ts @@ -1,29 +1,16 @@ import { FFmpeg } from '@ffmpeg/ffmpeg' import { toBlobURL } from '@ffmpeg/util' -import { defineNuxtPlugin, useRuntimeConfig } from 'nuxt/app' +import { defineNuxtPlugin } from 'nuxt/app' let ffmpeg: FFmpeg | null = null export default defineNuxtPlugin(() => { - const { - public: { ffmpegVersion }, - } = useRuntimeConfig() - return { provide: { ffmpeg: async () => { if (ffmpeg) return ffmpeg - ffmpeg = new FFmpeg() - - const mtOk = - typeof crossOriginIsolated !== 'undefined' && - crossOriginIsolated && - typeof SharedArrayBuffer !== 'undefined' - - const pkg = mtOk ? '@ffmpeg/core-mt' : '@ffmpeg/core-st' - const base = `https://unpkg.com/${pkg}@${ffmpegVersion}/dist/umd` - + const base = `https://unpkg.com/@ffmpeg/core-mt@0.12.10/dist/umd` await ffmpeg.load({ coreURL: await toBlobURL(`${base}/ffmpeg-core.js`, 'text/javascript'), wasmURL: await toBlobURL(`${base}/ffmpeg-core.wasm`, 'application/wasm'), From ab04a8b6b1e310d136fd1dd00db5fa3d07745167 Mon Sep 17 00:00:00 2001 From: tim Date: Thu, 11 Sep 2025 19:10:14 +0800 Subject: [PATCH 06/12] =?UTF-8?q?fix:=20ffmpeg=20=E5=8E=8B=E7=BC=A9?= =?UTF-8?q?=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend_nuxt/nuxt.config.ts | 2 +- frontend_nuxt/package-lock.json | 2 +- frontend_nuxt/package.json | 2 +- frontend_nuxt/plugins/ffmpeg.client.ts | 5 +++-- frontend_nuxt/utils/vditor.js | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend_nuxt/nuxt.config.ts b/frontend_nuxt/nuxt.config.ts index 5f4264567..257566037 100644 --- a/frontend_nuxt/nuxt.config.ts +++ b/frontend_nuxt/nuxt.config.ts @@ -98,7 +98,7 @@ export default defineNuxtConfig({ }, vite: { optimizeDeps: { - include: ['@ffmpeg/ffmpeg', '@ffmpeg/util'], + exclude: ['@ffmpeg/ffmpeg', '@ffmpeg/util'], }, build: {}, }, diff --git a/frontend_nuxt/package-lock.json b/frontend_nuxt/package-lock.json index 8493a2590..06383280b 100644 --- a/frontend_nuxt/package-lock.json +++ b/frontend_nuxt/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "frontend_nuxt", "dependencies": { - "@ffmpeg/ffmpeg": "^0.12.10", + "@ffmpeg/ffmpeg": "^0.12.2", "@ffmpeg/util": "^0.12.2", "@icon-park/vue-next": "^1.4.2", "@nuxt/image": "^1.11.0", diff --git a/frontend_nuxt/package.json b/frontend_nuxt/package.json index 43c4f2b60..2756684f9 100644 --- a/frontend_nuxt/package.json +++ b/frontend_nuxt/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@icon-park/vue-next": "^1.4.2", - "@ffmpeg/ffmpeg": "^0.12.10", + "@ffmpeg/ffmpeg": "^0.12.2", "@ffmpeg/util": "^0.12.2", "@nuxt/image": "^1.11.0", "@stomp/stompjs": "^7.0.0", diff --git a/frontend_nuxt/plugins/ffmpeg.client.ts b/frontend_nuxt/plugins/ffmpeg.client.ts index b4d6eee6b..9ea62e4e4 100644 --- a/frontend_nuxt/plugins/ffmpeg.client.ts +++ b/frontend_nuxt/plugins/ffmpeg.client.ts @@ -10,11 +10,12 @@ export default defineNuxtPlugin(() => { ffmpeg: async () => { if (ffmpeg) return ffmpeg ffmpeg = new FFmpeg() - const base = `https://unpkg.com/@ffmpeg/core-mt@0.12.10/dist/umd` + const base = `https://unpkg.com/@ffmpeg/core@0.12.2/dist/esm` + const libBase = `https://unpkg.com/@ffmpeg/ffmpeg@0.12.2/dist/esm` await ffmpeg.load({ coreURL: await toBlobURL(`${base}/ffmpeg-core.js`, 'text/javascript'), wasmURL: await toBlobURL(`${base}/ffmpeg-core.wasm`, 'application/wasm'), - workerURL: await toBlobURL(`${base}/ffmpeg-core.worker.js`, 'text/javascript'), + workerURL: await toBlobURL(`${libBase}/worker.js`, 'text/javascript'), }) return ffmpeg diff --git a/frontend_nuxt/utils/vditor.js b/frontend_nuxt/utils/vditor.js index c2c6d7d9a..3603fe542 100644 --- a/frontend_nuxt/utils/vditor.js +++ b/frontend_nuxt/utils/vditor.js @@ -118,7 +118,7 @@ export function createVditor(editorId, options = {}) { // 如果是视频文件且需要压缩 if (isVideo && sizeCheck.needsCompression) { try { - vditor.tip('视频压缩中...', 0) + vditor.tip('开始部署ffmpeg环境... 请稍等', 0) vditor.disabled() // 使用 FFmpeg 压缩视频 From 37bef0b2d7376fbf4ab24dd7c29282d3f52fa2e1 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 12 Sep 2025 10:15:17 +0800 Subject: [PATCH 07/12] =?UTF-8?q?fix:=20remove=20=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend_nuxt/package-lock.json | 81 --------------------------------- 1 file changed, 81 deletions(-) diff --git a/frontend_nuxt/package-lock.json b/frontend_nuxt/package-lock.json index 06383280b..f6f32e430 100644 --- a/frontend_nuxt/package-lock.json +++ b/frontend_nuxt/package-lock.json @@ -509,58 +509,6 @@ "node": ">=18" } }, - "node_modules/@emnapi/core": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", - "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.4", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/core/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, - "node_modules/@emnapi/runtime": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", - "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", - "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", @@ -815,18 +763,6 @@ "node": ">=8" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.3.tgz", - "integrity": "sha512-rZxtMsLwjdXkMUGC3WwsPwLNVqVqnTJT6MNIB6e+5fhMcSCPP0AOsNWuMQ5mdCq6HNjs/ZeWAEchpqeprqBD2Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.5", - "@emnapi/runtime": "^1.4.5", - "@tybys/wasm-util": "^0.10.0" - } - }, "node_modules/@netlify/binary-info": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@netlify/binary-info/-/binary-info-1.0.0.tgz", @@ -2081,23 +2017,6 @@ "node": ">=10.13.0" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tybys/wasm-util/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true - }, "node_modules/@types/d3-scale": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", From 6497cb92afd661175ea54302b647a5c875632dec Mon Sep 17 00:00:00 2001 From: Tim <135014430+nagisa77@users.noreply.github.com> Date: Fri, 12 Sep 2025 10:41:48 +0800 Subject: [PATCH 08/12] chore: remove ffmpeg video compression --- frontend_nuxt/config/uploadConfig.js | 12 +- frontend_nuxt/nuxt.config.ts | 4 +- frontend_nuxt/package-lock.json | 32 -- frontend_nuxt/package.json | 2 - frontend_nuxt/plugins/ffmpeg.client.ts | 25 -- frontend_nuxt/utils/ffmpegVideoCompressor.js | 327 ------------------- frontend_nuxt/utils/vditor.js | 59 +--- frontend_nuxt/utils/videoCompressor.js | 51 +-- 8 files changed, 8 insertions(+), 504 deletions(-) delete mode 100644 frontend_nuxt/plugins/ffmpeg.client.ts delete mode 100644 frontend_nuxt/utils/ffmpegVideoCompressor.js diff --git a/frontend_nuxt/config/uploadConfig.js b/frontend_nuxt/config/uploadConfig.js index 0172682b0..3c252ecc0 100644 --- a/frontend_nuxt/config/uploadConfig.js +++ b/frontend_nuxt/config/uploadConfig.js @@ -1,21 +1,15 @@ /** - * 文件上传配置 - 简化版 - * 专注于 FFmpeg.wasm 视频压缩,支持 Chrome/Safari + * 文件上传配置 */ export const UPLOAD_CONFIG = { - // 视频文件配置 - 专为 FFmpeg.wasm 优化 + // 视频文件配置 VIDEO: { // 文件大小限制 (字节) MAX_SIZE: 20 * 1024 * 1024, - TARGET_SIZE: 5 * 1024 * 1024, // 5MB - // 支持的输入格式 (FFmpeg.wasm 支持更多格式) + // 支持的输入格式 SUPPORTED_FORMATS: ['mp4', 'webm', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'm4v', 'ogv'], - - // 输出格式 - MP4 (兼容性最好) - OUTPUT_FORMAT: 'mp4', - OUTPUT_CODEC: 'h264', }, // 图片文件配置 diff --git a/frontend_nuxt/nuxt.config.ts b/frontend_nuxt/nuxt.config.ts index 257566037..0c5bea8ac 100644 --- a/frontend_nuxt/nuxt.config.ts +++ b/frontend_nuxt/nuxt.config.ts @@ -97,9 +97,7 @@ export default defineNuxtConfig({ }, }, vite: { - optimizeDeps: { - exclude: ['@ffmpeg/ffmpeg', '@ffmpeg/util'], - }, + optimizeDeps: {}, build: {}, }, }) diff --git a/frontend_nuxt/package-lock.json b/frontend_nuxt/package-lock.json index f6f32e430..e1d3a692d 100644 --- a/frontend_nuxt/package-lock.json +++ b/frontend_nuxt/package-lock.json @@ -6,8 +6,6 @@ "": { "name": "frontend_nuxt", "dependencies": { - "@ffmpeg/ffmpeg": "^0.12.2", - "@ffmpeg/util": "^0.12.2", "@icon-park/vue-next": "^1.4.2", "@nuxt/image": "^1.11.0", "@stomp/stompjs": "^7.0.0", @@ -547,36 +545,6 @@ "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", "license": "MIT" }, - "node_modules/@ffmpeg/ffmpeg": { - "version": "0.12.15", - "resolved": "https://registry.npmmirror.com/@ffmpeg/ffmpeg/-/ffmpeg-0.12.15.tgz", - "integrity": "sha512-1C8Obr4GsN3xw+/1Ww6PFM84wSQAGsdoTuTWPOj2OizsRDLT4CXTaVjPhkw6ARyDus1B9X/L2LiXHqYYsGnRFw==", - "license": "MIT", - "dependencies": { - "@ffmpeg/types": "^0.12.4" - }, - "engines": { - "node": ">=18.x" - } - }, - "node_modules/@ffmpeg/types": { - "version": "0.12.4", - "resolved": "https://registry.npmmirror.com/@ffmpeg/types/-/types-0.12.4.tgz", - "integrity": "sha512-k9vJQNBGTxE5AhYDtOYR5rO5fKsspbg51gbcwtbkw2lCdoIILzklulcjJfIDwrtn7XhDeF2M+THwJ2FGrLeV6A==", - "license": "MIT", - "engines": { - "node": ">=16.x" - } - }, - "node_modules/@ffmpeg/util": { - "version": "0.12.2", - "resolved": "https://registry.npmmirror.com/@ffmpeg/util/-/util-0.12.2.tgz", - "integrity": "sha512-ouyoW+4JB7WxjeZ2y6KpRvB+dLp7Cp4ro8z0HIVpZVCM7AwFlHa0c4R8Y/a4M3wMqATpYKhC7lSFHQ0T11MEDw==", - "license": "MIT", - "engines": { - "node": ">=18.x" - } - }, "node_modules/@icon-park/vue-next": { "version": "1.4.2", "resolved": "https://registry.npmmirror.com/@icon-park/vue-next/-/vue-next-1.4.2.tgz", diff --git a/frontend_nuxt/package.json b/frontend_nuxt/package.json index 2756684f9..540a4f579 100644 --- a/frontend_nuxt/package.json +++ b/frontend_nuxt/package.json @@ -13,8 +13,6 @@ }, "dependencies": { "@icon-park/vue-next": "^1.4.2", - "@ffmpeg/ffmpeg": "^0.12.2", - "@ffmpeg/util": "^0.12.2", "@nuxt/image": "^1.11.0", "@stomp/stompjs": "^7.0.0", "cropperjs": "^1.6.2", diff --git a/frontend_nuxt/plugins/ffmpeg.client.ts b/frontend_nuxt/plugins/ffmpeg.client.ts deleted file mode 100644 index 9ea62e4e4..000000000 --- a/frontend_nuxt/plugins/ffmpeg.client.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { FFmpeg } from '@ffmpeg/ffmpeg' -import { toBlobURL } from '@ffmpeg/util' -import { defineNuxtPlugin } from 'nuxt/app' - -let ffmpeg: FFmpeg | null = null - -export default defineNuxtPlugin(() => { - return { - provide: { - ffmpeg: async () => { - if (ffmpeg) return ffmpeg - ffmpeg = new FFmpeg() - const base = `https://unpkg.com/@ffmpeg/core@0.12.2/dist/esm` - const libBase = `https://unpkg.com/@ffmpeg/ffmpeg@0.12.2/dist/esm` - await ffmpeg.load({ - coreURL: await toBlobURL(`${base}/ffmpeg-core.js`, 'text/javascript'), - wasmURL: await toBlobURL(`${base}/ffmpeg-core.wasm`, 'application/wasm'), - workerURL: await toBlobURL(`${libBase}/worker.js`, 'text/javascript'), - }) - - return ffmpeg - }, - }, - } -}) diff --git a/frontend_nuxt/utils/ffmpegVideoCompressor.js b/frontend_nuxt/utils/ffmpegVideoCompressor.js deleted file mode 100644 index 2aa155049..000000000 --- a/frontend_nuxt/utils/ffmpegVideoCompressor.js +++ /dev/null @@ -1,327 +0,0 @@ -/** - * FFmpeg.wasm 视频压缩器 - * - * 用法: - * const { $ffmpeg } = useNuxtApp() - * const ff = await $ffmpeg() // 插件里已完成 ffmpeg.load() - * const out = await compressVideoWithFFmpeg(ff, file, { onProgress, strictSize: false }) - * - * 设计要点: - * - 本文件不再负责加载/初始化,只负责转码逻辑;和 Nuxt 插件解耦。 - * - 针对【同一个 ffmpeg 实例】做串行队列,避免并发 exec 踩内存文件系统。 - * - 使用 nanoid 生成唯一文件名;日志环形缓冲;默认 CRF+VBV,可选 strictSize(two-pass)。 - * - 体积明显小于目标时直通返回,减少无谓重编码。 - */ - -import { fetchFile } from '@ffmpeg/util' -import { nanoid } from 'nanoid' -import { UPLOAD_CONFIG } from '../config/uploadConfig.js' - -/************************* - * 每实例一个串行队列 * - *************************/ -// WeakMapPromise)[], running: boolean, resolvers: {res,rej}[] }> -const queues = new WeakMap() - -function enqueueOn(instance, taskFn) { - return new Promise((res, rej) => { - let st = queues.get(instance) - if (!st) { - st = { q: [], running: false, resolvers: [] } - queues.set(instance, st) - } - st.q.push(taskFn) - st.resolvers.push({ res, rej }) - drain(instance) - }) -} - -async function drain(instance) { - const st = queues.get(instance) - if (!st || st.running) return - st.running = true - try { - while (st.q.length) { - const task = st.q.shift() - const rr = st.resolvers.shift() - try { - rr.res(await task()) - } catch (e) { - rr.rej(e) - } - } - } finally { - st.running = false - } -} - -/***************** - * 工具函数 * - *****************/ -function decideScale(widthHint) { - if (!widthHint) return { filter: null, width: null } - const evenW = widthHint % 2 === 0 ? widthHint : widthHint - 1 - return { filter: `scale=${evenW}:-2:flags=bicubic,setsar=1`, width: evenW } -} - -function calculateParamsByRatio(originalSize, targetSize) { - const ratio = Math.min(targetSize / originalSize, 1) - const crf = ratio < 0.35 ? 29 : ratio < 0.5 ? 27 : ratio < 0.7 ? 25 : 23 - const preset = ratio < 0.35 ? 'slow' : ratio < 0.5 ? 'medium' : 'veryfast' - const s = - ratio < 0.35 - ? decideScale(720) - : ratio < 0.6 - ? decideScale(960) - : ratio < 0.8 - ? decideScale(1280) - : { filter: null, width: null } - const audioBitrateK = ratio < 0.5 ? 96 : ratio < 0.7 ? 128 : 160 - const profile = s.width && s.width <= 1280 ? 'main' : 'high' - return { crf, preset, scaleFilter: s.filter, scaleWidth: s.width, audioBitrateK, profile } -} - -function makeRingLogger(capBytes = 4000) { - const buf = [] - let total = 0 - function push(s) { - if (!s) return - buf.push(s) - total += s.length - while (total > capBytes) total -= buf.shift().length - } - return { push, dump: () => buf.slice() } -} - -function parseDurationFromLogs(logs) { - // 避免正则:查找 Duration: 后的 00:00:00.xx - const text = logs.join(' ') - const idx = text.indexOf('Duration:') - if (idx === -1) return null - let i = idx + 'Duration:'.length - while (i < text.length && text[i] === ' ') i++ - function read2(start) { - const a = text.charCodeAt(start) - 48 - const b = text.charCodeAt(start + 1) - 48 - if (a < 0 || a > 9 || b < 0 || b > 9) return null - return a * 10 + b - } - const hh = read2(i) - if (hh === null) return null - i += 2 - if (text[i++] !== ':') return null - const mm = read2(i) - if (mm === null) return null - i += 2 - if (text[i++] !== ':') return null - const s1 = read2(i) - if (s1 === null) return null - i += 2 - if (text[i++] !== '.') return null - let j = i - while (j < text.length && text.charCodeAt(j) >= 48 && text.charCodeAt(j) <= 57) j++ - const frac = parseFloat('0.' + text.slice(i, j) || '0') - return hh * 3600 + mm * 60 + s1 + frac -} - -export function isFFmpegSupported() { - return typeof WebAssembly !== 'undefined' && typeof Worker !== 'undefined' -} - -/** - * 读取 ffmpeg 核心版本(通过 -version),会进入队列避免并发冲突 - */ -export async function getFFmpegInfo(ffmpegInstance) { - return enqueueOn(ffmpegInstance, async () => { - const logs = [] - const onLog = ({ type, message }) => { - if (type === 'info' || type === 'fferr') logs.push(message) - } - ffmpegInstance.on('log', onLog) - try { - await ffmpegInstance.exec(['-version']) - } finally { - ffmpegInstance.off('log', onLog) - } - const line = logs.find((l) => l.toLowerCase().includes('ffmpeg version')) || '' - const parts = line.trim().split(' ').filter(Boolean) - const version = parts.length > 2 ? parts[2] : parts[1] || null - return { version } - }) -} - -/** - * 压缩:接受一个已经 load() 完成的 ffmpeg 实例 - * @param {*} ffmpegInstance 已初始化的 FFmpeg 实例(来自 Nuxt 插件) - * @param {File|Blob} file 输入文件 - * @param {{ onProgress?:(p:{stage:string,progress:number})=>void, signal?:AbortSignal, strictSize?:boolean, targetSize?:number }} opts - */ -export async function compressVideoWithFFmpeg(ffmpegInstance, file, opts = {}) { - return enqueueOn(ffmpegInstance, () => doCompress(ffmpegInstance, file, opts)) -} - -async function doCompress(ffmpegInstance, file, opts) { - const onProgress = opts.onProgress || (() => {}) - const { signal, strictSize = false } = opts - - onProgress({ stage: 'preparing', progress: 10 }) - - const targetSize = opts.targetSize ?? UPLOAD_CONFIG?.VIDEO?.TARGET_SIZE ?? 12 * 1024 * 1024 - - // 小体积直通 - const sizeKnown = 'size' in file && typeof file.size === 'number' - if (sizeKnown && file.size <= targetSize * 0.9) { - onProgress({ stage: 'skipped', progress: 100 }) - return file - } - - const params = calculateParamsByRatio(sizeKnown ? file.size : targetSize * 2, targetSize) - const { crf, preset, scaleFilter, audioBitrateK, profile } = params - - const name = 'name' in file && typeof file.name === 'string' ? file.name : 'input.mp4' - const dot = name.lastIndexOf('.') - const outName = (dot > -1 ? name.slice(0, dot) : name) + '.mp4' - - const ext = dot > -1 ? name.slice(dot + 1).toLowerCase() : 'mp4' - const id = nanoid() - const inputName = `input-${id}.${ext}` - const outputName = `output-${id}.mp4` - const passlog = `ffpass-${id}` - - // 监听 - const ring = makeRingLogger() - const onFfmpegProgress = ({ progress: p }) => { - const adjusted = 20 + p * 70 - onProgress({ stage: 'compressing', progress: Math.min(90, adjusted) }) - } - const onFfmpegLog = ({ type, message }) => { - if (type === 'fferr' || type === 'info') ring.push(message) - } - ffmpegInstance.on('progress', onFfmpegProgress) - ffmpegInstance.on('log', onFfmpegLog) - - let aborted = false - const abortHandler = () => { - aborted = true - } - if (signal) signal.addEventListener('abort', abortHandler, { once: true }) - - try { - await ffmpegInstance.writeFile(inputName, await fetchFile(file)) - onProgress({ stage: 'analyzing', progress: 20 }) - - let durationSec = null - try { - await ffmpegInstance.exec(['-hide_banner', '-i', inputName, '-f', 'null', '-']) - durationSec = parseDurationFromLogs(ring.dump()) - } catch { - durationSec = durationSec ?? parseDurationFromLogs(ring.dump()) - } - - let videoBitrate = null - if (durationSec && sizeKnown && targetSize < file.size) { - const totalTargetBits = targetSize * 8 - const audioBits = audioBitrateK * 1000 * durationSec - const maxVideoBits = Math.max(totalTargetBits - audioBits, totalTargetBits * 0.7) - const bps = Math.max(180000, Math.floor(maxVideoBits / durationSec)) - videoBitrate = String(Math.min(bps, 5000000)) - } - - const baseArgs = [ - '-hide_banner', - '-i', - inputName, - '-c:v', - 'libx264', - '-pix_fmt', - 'yuv420p', - '-profile:v', - profile, - '-movflags', - '+faststart', - '-preset', - preset, - '-c:a', - 'aac', - '-b:a', - `${audioBitrateK}k`, - '-ac', - '2', - ] - if (scaleFilter) baseArgs.push('-vf', scaleFilter) - - const onePassArgs = [...baseArgs, '-crf', String(crf)] - if (videoBitrate) - onePassArgs.push('-maxrate', videoBitrate, '-bufsize', String(parseInt(videoBitrate, 10) * 2)) - - const twoPassFirst = [ - '-y', - '-hide_banner', - '-i', - inputName, - '-c:v', - 'libx264', - '-b:v', - `${videoBitrate || '1000000'}`, - '-pass', - '1', - '-passlogfile', - passlog, - '-an', - '-f', - 'mp4', - '/dev/null', - ] - const twoPassSecond = [ - ...baseArgs, - '-b:v', - `${videoBitrate || '1000000'}`, - '-pass', - '2', - '-passlogfile', - passlog, - outputName, - ] - - if (aborted) throw new DOMException('Aborted', 'AbortError') - - if (!strictSize) { - await ffmpegInstance.exec([...onePassArgs, outputName]) - } else { - if (!videoBitrate) videoBitrate = '1000000' - await ffmpegInstance.exec(twoPassFirst) - onProgress({ stage: 'second-pass', progress: 85 }) - await ffmpegInstance.exec(twoPassSecond) - } - - if (aborted) throw new DOMException('Aborted', 'AbortError') - - onProgress({ stage: 'finalizing', progress: 95 }) - const out = await ffmpegInstance.readFile(outputName) - - const mime = 'video/mp4' - const blob = new Blob([out], { type: mime }) - const hasFileCtor = typeof File === 'function' - const result = hasFileCtor ? new File([blob], outName, { type: mime }) : blob - - onProgress({ stage: 'completed', progress: 100 }) - return result - } finally { - try { - await ffmpegInstance.deleteFile(inputName) - } catch {} - try { - await ffmpegInstance.deleteFile(outputName) - } catch {} - try { - await ffmpegInstance.deleteFile(`${passlog}-0.log`) - } catch {} - try { - await ffmpegInstance.deleteFile(`${passlog}-0.log.mbtree`) - } catch {} - - ffmpegInstance.off('progress', onFfmpegProgress) - ffmpegInstance.off('log', onFfmpegLog) - if (signal) signal.removeEventListener('abort', abortHandler) - } -} diff --git a/frontend_nuxt/utils/vditor.js b/frontend_nuxt/utils/vditor.js index 3603fe542..bef1fdeee 100644 --- a/frontend_nuxt/utils/vditor.js +++ b/frontend_nuxt/utils/vditor.js @@ -3,8 +3,7 @@ import { getToken, authState } from './auth' import { searchUsers, fetchFollowings, fetchAdmins } from './user' import { tiebaEmoji } from './tiebaEmoji' import vditorPostCitation from './vditorPostCitation.js' -import { checkFileSize, formatFileSize, compressVideo, VIDEO_CONFIG } from './videoCompressor.js' -import { UPLOAD_CONFIG } from '../config/uploadConfig.js' +import { checkFileSize, formatFileSize } from './videoCompressor.js' export function getEditorTheme() { return document.documentElement.dataset.theme === 'dark' ? 'dark' : 'classic' @@ -95,7 +94,6 @@ export function createVditor(editorId, options = {}) { const file = files[0] const ext = file.name.split('.').pop().toLowerCase() const videoExts = ['mp4', 'webm', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'm4v', 'ogv'] - const isVideo = videoExts.includes(ext) // 检查文件大小 const sizeCheck = checkFileSize(file) @@ -113,61 +111,10 @@ export function createVditor(editorId, options = {}) { return '文件过大' } - let processedFile = file - - // 如果是视频文件且需要压缩 - if (isVideo && sizeCheck.needsCompression) { - try { - vditor.tip('开始部署ffmpeg环境... 请稍等', 0) - vditor.disabled() - - // 使用 FFmpeg 压缩视频 - processedFile = await compressVideo(file, (progress) => { - const messages = { - initializing: '初始化 FFmpeg', - preparing: '准备压缩', - analyzing: '分析视频', - compressing: '压缩中', - finalizing: '完成压缩', - completed: '压缩完成', - } - const message = messages[progress.stage] || progress.stage - vditor.tip(`${message} ${progress.progress}%`, 0) - }) - - const originalSize = formatFileSize(file.size) - const compressedSize = formatFileSize(processedFile.size) - const savings = Math.round((1 - processedFile.size / file.size) * 100) - - vditor.tip(`压缩完成!${originalSize} → ${compressedSize} (节省 ${savings}%)`, 2000) - // 压缩成功但仍然超过最大限制,则阻止上传 - if (processedFile.size > VIDEO_CONFIG.MAX_SIZE) { - vditor.tip( - `压缩后仍超过限制 (${formatFileSize(VIDEO_CONFIG.MAX_SIZE)}). 请降低分辨率或码率后再上传。`, - 4000, - ) - vditor.enable() - return '压缩后仍超过大小限制' - } - } catch (error) { - // 压缩失败时,如果原文件超过最大限制,则阻止上传 - if (file.size > VIDEO_CONFIG.MAX_SIZE) { - vditor.tip( - `视频压缩失败,且文件超过限制 (${formatFileSize(VIDEO_CONFIG.MAX_SIZE)}). 请先压缩后再上传。`, - 4000, - ) - vditor.enable() - return '视频压缩失败且文件过大' - } - vditor.tip('视频压缩失败,将尝试上传原文件', 3000) - processedFile = file - } - } - vditor.tip('文件上传中', 0) vditor.disabled() const res = await fetch( - `${API_BASE_URL}/api/upload/presign?filename=${encodeURIComponent(processedFile.name)}`, + `${API_BASE_URL}/api/upload/presign?filename=${encodeURIComponent(file.name)}`, { headers: { Authorization: `Bearer ${getToken()}` } }, ) if (!res.ok) { @@ -176,7 +123,7 @@ export function createVditor(editorId, options = {}) { return '获取上传地址失败' } const info = await res.json() - const put = await fetch(info.uploadUrl, { method: 'PUT', body: processedFile }) + const put = await fetch(info.uploadUrl, { method: 'PUT', body: file }) if (!put.ok) { vditor.enable() vditor.tip('上传失败') diff --git a/frontend_nuxt/utils/videoCompressor.js b/frontend_nuxt/utils/videoCompressor.js index 584d8a487..bffa6c453 100644 --- a/frontend_nuxt/utils/videoCompressor.js +++ b/frontend_nuxt/utils/videoCompressor.js @@ -1,11 +1,8 @@ /** - * 基于 FFmpeg.wasm 的视频压缩工具 - * 专为现代浏览器 (Chrome/Safari) 优化 + * 视频上传工具 */ import { UPLOAD_CONFIG } from '../config/uploadConfig.js' -import { compressVideoWithFFmpeg, isFFmpegSupported } from './ffmpegVideoCompressor.js' -import { useNuxtApp } from '#app' // 导出配置供外部使用 export const VIDEO_CONFIG = UPLOAD_CONFIG.VIDEO @@ -18,7 +15,6 @@ export function checkFileSize(file) { isValid: file.size <= VIDEO_CONFIG.MAX_SIZE, actualSize: file.size, maxSize: VIDEO_CONFIG.MAX_SIZE, - needsCompression: file.size > VIDEO_CONFIG.TARGET_SIZE, } } @@ -32,48 +28,3 @@ export function formatFileSize(bytes) { const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] } - -/** - * 压缩视频文件 - 使用 FFmpeg.wasm - */ -export async function compressVideo(file, onProgress = () => {}) { - // 检查是否需要压缩 - const sizeCheck = checkFileSize(file) - if (!sizeCheck.needsCompression) { - onProgress({ stage: 'completed', progress: 100 }) - return file - } - - // 检查 FFmpeg 支持 - if (!isFFmpegSupported()) { - throw new Error('当前浏览器不支持视频压缩功能,请使用 Chrome 或 Safari 浏览器') - } - - try { - const { $ffmpeg } = useNuxtApp() - const ff = await $ffmpeg() - return await compressVideoWithFFmpeg(ff, file, { onProgress }) - } catch (error) { - console.error('FFmpeg 压缩失败:', error) - throw new Error(`视频压缩失败: ${error.message}`) - } -} - -/** - * 预加载 FFmpeg(可选的性能优化) - */ -export async function preloadVideoCompressor() { - try { - // FFmpeg 初始化现在通过 Nuxt 插件处理 - // 这里只需要检查支持性 - if (!isFFmpegSupported()) { - throw new Error('当前浏览器不支持 FFmpeg') - } - const { $ffmpeg } = useNuxtApp() - await $ffmpeg() - return { success: true, message: 'FFmpeg 预加载成功' } - } catch (error) { - console.warn('FFmpeg 预加载失败:', error) - return { success: false, error: error.message } - } -} From d8534fb94d65f1659a76c7857dc1f83bda996741 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 12 Sep 2025 10:43:06 +0800 Subject: [PATCH 09/12] =?UTF-8?q?fix:=20=E8=AF=84=E8=AE=BA=E5=90=8E--?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E5=88=B7=E6=96=B0=E5=B8=96=E5=AD=90=E5=86=85?= =?UTF-8?q?=E5=AE=B9=20#939?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend_nuxt/assets/global.css | 6 +++--- frontend_nuxt/pages/posts/[id]/index.vue | 21 +++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/frontend_nuxt/assets/global.css b/frontend_nuxt/assets/global.css index f39535231..6105d1ab9 100644 --- a/frontend_nuxt/assets/global.css +++ b/frontend_nuxt/assets/global.css @@ -356,7 +356,7 @@ body { } .d2h-file-name { - font-size: 12px !important; + font-size: 14px !important; } .d2h-file-header { @@ -371,14 +371,14 @@ body { padding-left: 10px !important; } - .d2h-diff-table { + /* .d2h-diff-table { font-size: 6px !important; } .d2h-code-line ins { height: 100%; font-size: 13px !important; - } + } */ /* .d2h-code-line { height: 12px; diff --git a/frontend_nuxt/pages/posts/[id]/index.vue b/frontend_nuxt/pages/posts/[id]/index.vue index 9c491d71c..cad9525c8 100644 --- a/frontend_nuxt/pages/posts/[id]/index.vue +++ b/frontend_nuxt/pages/posts/[id]/index.vue @@ -445,7 +445,7 @@ const handleContentClick = (e) => { const onCommentDeleted = (id) => { removeCommentFromList(Number(id), comments.value) - fetchComments() + fetchTimeline() } const { @@ -557,7 +557,7 @@ const postComment = async (parentUserName, text, clear) => { if (res.ok) { const data = await res.json() console.debug('Post comment response data', data) - await fetchComments() + await fetchTimeline() clear() if (data.reward && data.reward > 0) { toast.success(`评论成功,获得 ${data.reward} 经验值`) @@ -612,7 +612,7 @@ const approvePost = async () => { status.value = 'PUBLISHED' toast.success('已通过审核') await refreshPost() - await fetchChangeLogs() + await fetchTimeline() } else { toast.error('操作失败') } @@ -628,7 +628,7 @@ const pinPost = async () => { if (res.ok) { toast.success('已置顶') await refreshPost() - await fetchChangeLogs() + await fetchTimeline() } else { toast.error('操作失败') } @@ -644,7 +644,7 @@ const unpinPost = async () => { if (res.ok) { toast.success('已取消置顶') await refreshPost() - await fetchChangeLogs() + await fetchTimeline() } else { toast.error('操作失败') } @@ -660,7 +660,7 @@ const excludeRss = async () => { if (res.ok) { rssExcluded.value = true toast.success('已标记为rss不推荐') - await fetchChangeLogs() + await fetchTimeline() } else { toast.error('操作失败') } @@ -676,7 +676,8 @@ const includeRss = async () => { if (res.ok) { rssExcluded.value = false toast.success('已标记为rss推荐') - await fetchChangeLogs() + await refreshPost() + await fetchTimeline() } else { toast.error('操作失败') } @@ -693,7 +694,7 @@ const closePost = async () => { closed.value = true toast.success('已关闭') await refreshPost() - await fetchChangeLogs() + await fetchTimeline() } else { toast.error('操作失败') } @@ -710,7 +711,7 @@ const reopenPost = async () => { closed.value = false toast.success('已重新打开') await refreshPost() - await fetchChangeLogs() + await fetchTimeline() } else { toast.error('操作失败') } @@ -755,7 +756,7 @@ const rejectPost = async () => { status.value = 'REJECTED' toast.success('已驳回') await refreshPost() - await fetchChangeLogs() + await fetchTimeline() } else { toast.error('操作失败') } From abbdb224e06f432ce3d93c78362e2e12656d47ca Mon Sep 17 00:00:00 2001 From: Tim <135014430+nagisa77@users.noreply.github.com> Date: Fri, 12 Sep 2025 10:50:15 +0800 Subject: [PATCH 10/12] fix: restrict image preview to markdown images --- frontend_nuxt/components/CommentItem.vue | 2 +- frontend_nuxt/pages/message-box/[id].vue | 6 +++++- frontend_nuxt/pages/posts/[id]/index.vue | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend_nuxt/components/CommentItem.vue b/frontend_nuxt/components/CommentItem.vue index 5147db26d..584f0a90b 100644 --- a/frontend_nuxt/components/CommentItem.vue +++ b/frontend_nuxt/components/CommentItem.vue @@ -342,7 +342,7 @@ const copyCommentLink = () => { const handleContentClick = (e) => { handleMarkdownClick(e) - if (e.target.tagName === 'IMG') { + if (e.target.tagName === 'IMG' && !e.target.classList.contains('emoji')) { const container = e.target.parentNode const imgs = [...container.querySelectorAll('img')].map((i) => i.src) lightboxImgs.value = imgs diff --git a/frontend_nuxt/pages/message-box/[id].vue b/frontend_nuxt/pages/message-box/[id].vue index 39265b66a..ddd581136 100644 --- a/frontend_nuxt/pages/message-box/[id].vue +++ b/frontend_nuxt/pages/message-box/[id].vue @@ -463,7 +463,11 @@ function minimize() { function handleContentClick(e) { handleMarkdownClick(e) - if (e.target.tagName === 'IMG') { + if ( + e.target.tagName === 'IMG' && + !e.target.classList.contains('emoji') && + !e.target.closest('.reactions-container') + ) { const container = e.target.parentNode const imgs = [...container.querySelectorAll('img')].map((i) => i.src) lightboxImgs.value = imgs diff --git a/frontend_nuxt/pages/posts/[id]/index.vue b/frontend_nuxt/pages/posts/[id]/index.vue index 9c491d71c..2a9d08173 100644 --- a/frontend_nuxt/pages/posts/[id]/index.vue +++ b/frontend_nuxt/pages/posts/[id]/index.vue @@ -434,7 +434,7 @@ const removeCommentFromList = (id, list) => { const handleContentClick = (e) => { handleMarkdownClick(e) - if (e.target.tagName === 'IMG') { + if (e.target.tagName === 'IMG' && !e.target.classList.contains('emoji')) { const container = e.target.parentNode const imgs = [...container.querySelectorAll('img')].map((i) => i.src) lightboxImgs.value = imgs From c3758cafe8dd29c3fbf8962bd2b817f2676cbff3 Mon Sep 17 00:00:00 2001 From: tim Date: Fri, 12 Sep 2025 13:42:03 +0800 Subject: [PATCH 11/12] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E7=BB=91=E5=AE=9A=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend_nuxt/pages/message-box/[id].vue | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend_nuxt/pages/message-box/[id].vue b/frontend_nuxt/pages/message-box/[id].vue index ddd581136..56fe7cefa 100644 --- a/frontend_nuxt/pages/message-box/[id].vue +++ b/frontend_nuxt/pages/message-box/[id].vue @@ -20,7 +20,7 @@ -
+
@@ -50,7 +50,11 @@
-
+
i.src) lightboxImgs.value = imgs From 134e3fc8662a10c3bacb18384704dc4972aae433 Mon Sep 17 00:00:00 2001 From: tim Date: Fri, 12 Sep 2025 14:27:01 +0800 Subject: [PATCH 12/12] fix: tags height --- frontend_nuxt/components/ArticleCategory.vue | 5 +++++ frontend_nuxt/components/ArticleTags.vue | 5 +++++ frontend_nuxt/pages/index.vue | 7 ------- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend_nuxt/components/ArticleCategory.vue b/frontend_nuxt/components/ArticleCategory.vue index f1b445a04..4e229c80b 100644 --- a/frontend_nuxt/components/ArticleCategory.vue +++ b/frontend_nuxt/components/ArticleCategory.vue @@ -35,6 +35,7 @@ const isImageIcon = (icon) => { display: flex; flex-direction: row; gap: 10px; + min-height: 25px; } .article-info-item { @@ -63,5 +64,9 @@ const isImageIcon = (icon) => { .article-info-item { font-size: 10px; } + + .article-category-container { + min-height: 20px; + } } diff --git a/frontend_nuxt/components/ArticleTags.vue b/frontend_nuxt/components/ArticleTags.vue index fd7620dd2..09ef3aad2 100644 --- a/frontend_nuxt/components/ArticleTags.vue +++ b/frontend_nuxt/components/ArticleTags.vue @@ -44,6 +44,7 @@ const isImageIcon = (icon) => { display: flex; flex-direction: row; gap: 10px; + min-height: 25px; } .article-info-item { @@ -72,5 +73,9 @@ const isImageIcon = (icon) => { .article-info-item { font-size: 10px; } + + .article-tags-container { + min-height: 20px; + } } diff --git a/frontend_nuxt/pages/index.vue b/frontend_nuxt/pages/index.vue index 04013333e..a3e7989d7 100644 --- a/frontend_nuxt/pages/index.vue +++ b/frontend_nuxt/pages/index.vue @@ -594,13 +594,6 @@ const sanitizeDescription = (text) => stripMarkdown(text) margin-bottom: 10px; } -.article-tags-container { - display: flex; - flex-direction: row; - align-items: center; - gap: 10px; -} - .article-tag-item { display: flex; flex-direction: row;