diff --git a/.github/min.node-version b/.github/min.node-version index db49bb14d78..1dd37d53743 100644 --- a/.github/min.node-version +++ b/.github/min.node-version @@ -1 +1 @@ -22.22.2 +24.17.0 diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 7992d5d83bf..e4ff228145c 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -56,9 +56,6 @@ jobs: submodules: true - name: Setup pnpm uses: pnpm/action-setup@v6.0.9 - - name: Install FFmpeg - run: | - sudo apt install -y ffmpeg - name: Use Node.js uses: actions/setup-node@v7.0.0 with: diff --git a/.github/workflows/test-federation.yml b/.github/workflows/test-federation.yml index d91dbaa07b7..3d40aa7ec7e 100644 --- a/.github/workflows/test-federation.yml +++ b/.github/workflows/test-federation.yml @@ -31,9 +31,6 @@ jobs: submodules: true - name: Setup pnpm uses: pnpm/action-setup@v6.0.9 - - name: Install FFmpeg - run: | - sudo apt install -y ffmpeg - name: Use Node.js uses: actions/setup-node@v7.0.0 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index d9fdaef1fe9..65ce4c3c5f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 2026.8.0 +### Note +- Node.js v22のサポートを終了しました。Node.js v22では動作しません。Node.js v24, v26をご利用ください。 + ### General - @@ -8,6 +11,7 @@ - Fix: 画像の表示時にBlurhashが描画されない場合があるのを修正 ### Server +- Enhance: 動画処理のパフォーマンスを改善 - Fix: 既にミュートしているスレッドに対して再度スレッドミュートを作成しようとするとサーバーエラーになる問題を修正 diff --git a/Dockerfile b/Dockerfile index 2823c2cd393..804bd59906b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -78,7 +78,7 @@ ENV PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN=false RUN apt-get update \ && apt-get install -y --no-install-recommends \ - ffmpeg tini curl libjemalloc-dev libjemalloc2 \ + tini curl libjemalloc-dev libjemalloc2 \ && ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so \ && groupadd -g "${GID}" misskey \ && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \ diff --git a/packages/backend/lib/backend-dev-server.ts b/packages/backend/lib/backend-dev-server.ts new file mode 100644 index 00000000000..ca77190022e --- /dev/null +++ b/packages/backend/lib/backend-dev-server.ts @@ -0,0 +1,74 @@ +import { execa, execaNode } from 'execa'; +import fkill from 'fkill'; +import type { ResultPromise } from 'execa'; +import type { Plugin } from 'rolldown'; + +/** + * Watchモード時にバックエンドの起動・停止制御を行うプラグイン + */ +export function backendDevServerPlugin(): Plugin { + let backendProcess: ResultPromise | null = null; + let backendShutdownPromise: Promise | null = null; + + async function runBuildAssets() { + await execa('pnpm', ['run', 'build-assets'], { + cwd: '../../', + stdout: process.stdout, + stderr: process.stderr, + }); + } + + async function killBackendProcess() { + if (backendShutdownPromise) return backendShutdownPromise; + if (!backendProcess) return; + + const processToKill = backendProcess; + backendProcess = null; + processToKill.catch(() => {}); // プロセスの終了によって発生する例外を無視するためにcatch()を呼び出す + + backendShutdownPromise = (async () => { + if (process.platform === 'win32' && processToKill.pid != null) { + await fkill(processToKill.pid, { + force: true, + tree: true, + silent: true, + waitForExit: 5000, + }); + } else { + processToKill.kill(); + } + + await processToKill.catch(() => {}); + })().finally(() => { + backendShutdownPromise = null; + }); + + return backendShutdownPromise; + } + + return { + name: 'backend-dev-server', + async closeBundle() { + await runBuildAssets(); + if (backendProcess) { + await killBackendProcess(); + } + backendProcess = execaNode('./built/entry.js', [], { + stdout: process.stdout, + stderr: process.stderr, + env: { + NODE_ENV: 'development', + }, + }); + }, + async watchChange() { + if (backendProcess) { + await killBackendProcess(); + await runBuildAssets(); + } + }, + async closeWatcher() { + await killBackendProcess(); + }, + }; +} diff --git a/packages/backend/lib/esm-shim.ts b/packages/backend/lib/esm-shim.ts new file mode 100644 index 00000000000..59eca7f8e6e --- /dev/null +++ b/packages/backend/lib/esm-shim.ts @@ -0,0 +1,75 @@ +import { RolldownMagicString } from 'rolldown'; +import type { Plugin } from 'rolldown'; + +const cjsSyntaxRegex = /__filename|__dirname|require\(|require\.resolve\(/; + +const shimComment = '// -- ESM Shim --'; + +export function esmShimPlugin(): Plugin { + return { + name: 'esm-shim', + renderChunk(code, _, opts, meta) { + if (opts.format === 'es') { + if (code.includes(shimComment) || !cjsSyntaxRegex.test(code)) { + return null; + } + + const ast = this.parse(code, { + sourceType: 'module', + }); + + let lastImportIndex = -1; + let isFilenameShimmed = false; + let isDirnameShimmed = false; + let isRequireShimmed = false; + + for (const [index, node] of ast.body.entries()) { + switch (node.type) { + case 'ImportDeclaration': + lastImportIndex = index; + break; + case 'VariableDeclaration': + for (const decl of node.declarations) { + if (decl.id.type === 'Identifier') { + if (decl.id.name === '__filename') { + isFilenameShimmed = true; + } else if (decl.id.name === '__dirname') { + isDirnameShimmed = true; + } else if (decl.id.name === 'require') { + isRequireShimmed = true; + } + } + } + break; + } + } + + const shimLines: string[] = []; + if (!isFilenameShimmed) { + shimLines.push('const __filename = import.meta.filename;'); + } + if (!isDirnameShimmed) { + shimLines.push('const __dirname = import.meta.dirname;'); + } + if (!isRequireShimmed) { + shimLines.push('import { createRequire as cjsShimCreateRequire } from \'node:module\';'); + shimLines.push('const require = cjsShimCreateRequire(import.meta.url);'); + } + + if (shimLines.length > 0) { + const magicString = meta.magicString ?? new RolldownMagicString(code); + const shimCode = `${shimComment}\n${shimLines.join('\n')}\n`; + if (lastImportIndex >= 0) { + const lastImportNode = ast.body[lastImportIndex]; + magicString.appendLeft(lastImportNode.end, `\n${shimCode}`); + } else { + magicString.prepend(`${shimCode}\n`); + } + return magicString; + } + } + + return null; + }, + }; +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 8a8d3c7f5ac..36dc4b61890 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "engines": { - "node": "^22.22.2 || ^24.17.0 || ^26.4.0" + "node": "^24.17.0 || ^26.4.0" }, "scripts": { "start": "pnpm compile-config && node ./built/entry.js", @@ -34,6 +34,14 @@ "generate-api-json": "pnpm compile-config && node ./scripts/generate_api_json.js" }, "optionalDependencies": { + "@seydx/node-av-darwin-arm64": "6.1.1", + "@seydx/node-av-darwin-x64": "6.1.1", + "@seydx/node-av-linux-arm64": "6.1.1", + "@seydx/node-av-linux-x64": "6.1.1", + "@seydx/node-av-win32-arm64-mingw": "6.1.1", + "@seydx/node-av-win32-arm64-msvc": "6.1.1", + "@seydx/node-av-win32-x64-mingw": "6.1.1", + "@seydx/node-av-win32-x64-msvc": "6.1.1", "bufferutil": "4.1.0", "slacc-android-arm-eabi": "0.1.5", "slacc-android-arm64": "0.1.5", @@ -82,7 +90,6 @@ "cacheable-lookup": "7.0.0", "chalk": "6.0.0", "chalk-template": "1.1.2", - "chokidar": "5.0.0", "color-convert": "3.1.3", "content-disposition": "2.0.1", "date-fns": "4.4.0", @@ -91,7 +98,6 @@ "fastify-raw-body": "6.0.1", "feed": "6.0.0", "file-type": "22.0.1", - "fluent-ffmpeg": "2.1.3", "got": "15.1.0", "hpagent": "1.2.0", "http-link-header": "1.1.4", @@ -111,6 +117,7 @@ "ms": "3.0.0-canary.202508261828", "nanoid": "6.0.1", "nested-property": "4.0.0", + "node-av": "6.1.1", "node-fetch": "3.3.2", "node-html-parser": "9.0.1", "nodemailer": "9.0.5", @@ -148,12 +155,10 @@ "devDependencies": { "@kitajs/ts-html-plugin": "4.1.4", "@nestjs/platform-express": "11.1.29", - "@rollup/plugin-esm-shim": "0.1.8", "@sentry/vue": "10.70.0", "@sinonjs/fake-timers": "15.4.0", "@types/accepts": "1.3.7", "@types/archiver": "8.0.0", - "@types/fluent-ffmpeg": "2.1.28", "@types/http-link-header": "1.0.7", "@types/jsonld": "1.5.15", "@types/mime-types": "3.0.1", diff --git a/packages/backend/rolldown.config.ts b/packages/backend/rolldown.config.ts index 76bfa35421a..4d936c668a2 100644 --- a/packages/backend/rolldown.config.ts +++ b/packages/backend/rolldown.config.ts @@ -1,80 +1,8 @@ import { defineConfig } from 'rolldown'; import { version as summalyVersion } from '@misskey-dev/summaly'; -import type { Plugin, ExternalOption } from 'rolldown'; -import { execa, execaNode } from 'execa'; -import type { ResultPromise } from 'execa'; -import fkill from 'fkill'; -import esmShim from '@rollup/plugin-esm-shim'; - -/** - * Watchモード時にバックエンドの起動・停止制御を行うプラグイン - */ -function backendDevServerPlugin(): Plugin { - let backendProcess: ResultPromise | null = null; - let backendShutdownPromise: Promise | null = null; - - async function runBuildAssets() { - await execa('pnpm', ['run', 'build-assets'], { - cwd: '../../', - stdout: process.stdout, - stderr: process.stderr, - }); - } - - async function killBackendProcess() { - if (backendShutdownPromise) return backendShutdownPromise; - if (!backendProcess) return; - - const processToKill = backendProcess; - backendProcess = null; - processToKill.catch(() => {}); // プロセスの終了によって発生する例外を無視するためにcatch()を呼び出す - - backendShutdownPromise = (async () => { - if (process.platform === 'win32' && processToKill.pid != null) { - await fkill(processToKill.pid, { - force: true, - tree: true, - silent: true, - waitForExit: 5000, - }); - } else { - processToKill.kill(); - } - - await processToKill.catch(() => {}); - })().finally(() => { - backendShutdownPromise = null; - }); - - return backendShutdownPromise; - } - - return { - name: 'backend-dev-server', - async closeBundle() { - await runBuildAssets(); - if (backendProcess) { - await killBackendProcess(); - } - backendProcess = execaNode('./built/entry.js', [], { - stdout: process.stdout, - stderr: process.stderr, - env: { - NODE_ENV: 'development', - }, - }); - }, - async watchChange() { - if (backendProcess) { - await killBackendProcess(); - await runBuildAssets(); - } - }, - async closeWatcher() { - await killBackendProcess(); - }, - }; -} +import type { ExternalOption } from 'rolldown'; +import { backendDevServerPlugin } from './lib/backend-dev-server.ts'; +import { esmShimPlugin } from './lib/esm-shim.ts'; export default defineConfig((args) => { const isWatchMode = args.watch != null && args.watch !== 'false'; @@ -97,6 +25,7 @@ export default defineConfig((args) => { 'sharp', 'jsdom', 're2', + /^@seydx\/node-av-.*/, 'ipaddr.js', 'file-type', // バンドルするとSentryの自動計装が正しく行われなくなるため外しておく @@ -114,7 +43,7 @@ export default defineConfig((args) => { platform: 'node', tsconfig: './test-server/tsconfig.json', plugins: [ - esmShim(), + esmShimPlugin(), ], transform: { define, @@ -126,6 +55,9 @@ export default defineConfig((args) => { cleanDir: true, format: 'esm', }, + experimental: { + nativeMagicString: true, + }, external: externalModules, }; } else { @@ -140,7 +72,7 @@ export default defineConfig((args) => { platform: 'node', tsconfig: true, plugins: [ - esmShim(), + esmShimPlugin(), (isWatchMode ? backendDevServerPlugin() : undefined), ], transform: { @@ -158,6 +90,9 @@ export default defineConfig((args) => { include: ['src/**/*.{ts,js,mjs,cjs,tsx,json}'], clearScreen: false, }, + experimental: { + nativeMagicString: true, + }, // ビルドの高速化のために、watchモードのときは外部モジュールは全てバンドルしないようにする external: isWatchMode ? /^(?!@\/|\0)[^.\/](?!:[\/\\])/ : externalModules, }; diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index f0befc2a855..312556901b2 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -5,23 +5,22 @@ import * as fs from 'node:fs'; import * as crypto from 'node:crypto'; -import { join } from 'node:path'; import * as stream from 'node:stream/promises'; import { Injectable } from '@nestjs/common'; -import { FSWatcher } from 'chokidar'; import * as fileType from 'file-type'; -import FFmpeg from 'fluent-ffmpeg'; import isSvg from 'is-svg'; import probeImageSize from 'probe-image-size'; import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; import * as blurhash from 'blurhash'; -import { createTempDir } from '@/misc/create-temp.js'; +import { Decoder, Demuxer, FilterAPI, Scaler, probe } from 'node-av/api'; +import { AV_PICTURE_TYPE_I } from 'node-av/constants'; import { SensitiveMediaDetectionService } from '@/core/SensitiveMediaDetectionService.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; import type { Prediction } from '@/core/SensitiveMediaDetectionService.js'; +import '@/misc/node-av-log.js'; export type FileInfo = { size: number; @@ -49,6 +48,26 @@ const TYPE_SVG = { ext: 'svg', }; +/** 映像トラックの有無を調べる probe の制限時間 */ +const PROBE_TIMEOUT_MS = 10 * 1000; + +/** + * センシティブ判定用のフレーム抽出全体の制限時間。 + * 長尺・高解像度の動画でデコードが延々と続くのを防ぐ。 + * 時間切れになった場合は、そこまでに集まったフレームだけで判定する。 + */ +const FRAME_EXTRACTION_TIMEOUT_MS = 60 * 1000; + +/** 判定対象から除外する暗部の割合 (これ以上暗いフレームは誤検知を招くため使わない) */ +const MAX_BLACK_PERCENTAGE = 50; + +/** 判定サービス側のデコーダが内部で使う画像サイズ */ +const DETECTION_IMAGE_SIZE = 299; + +function isAbortError(err: unknown): boolean { + return err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError'); +} + @Injectable() export class FileInfoService { private logger: Logger; @@ -206,77 +225,16 @@ export class FileInfoService { } if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) { - const [outDir, disposeOutDir] = await createTempDir(); - try { - const command = FFmpeg() - .input(source) - .inputOptions([ - '-skip_frame', 'nokey', // 可能ならキーフレームのみを取得してほしいとする(そうなるとは限らない) - '-lowres', '3', // 元の画質でデコードする必要はないので 1/8 画質でデコードしてもよいとする(そうなるとは限らない) - ]) - .noAudio() - .videoFilters([ - { - filter: 'select', // フレームのフィルタリング - options: { - e: 'eq(pict_type,PICT_TYPE_I)', // I-Frame のみをフィルタする(VP9 とかはデコードしてみないとわからないっぽい) - }, - }, - { - filter: 'blackframe', // 暗いフレームの検出 - options: { - amount: '0', // 暗さに関わらず全てのフレームで測定値を取る - }, - }, - { - filter: 'metadata', - options: { - mode: 'select', // フレーム選択モード - key: 'lavfi.blackframe.pblack', // フレームにおける暗部の百分率(前のフィルタからのメタデータを参照する) - value: '50', - function: 'less', // 50% 未満のフレームを選択する(50% 以上暗部があるフレームだと誤検知を招くかもしれないので) - }, - }, - { - filter: 'scale', - options: { - w: 299, - h: 299, - }, - }, - ]) - .format('image2') - .output(join(outDir, '%d.png')) - .outputOptions(['-vsync', '0']); // 可変フレームレートにすることで穴埋めをさせない - // 判定対象フレームを選定して正規化済みバッファとして集め、外部サービスへまとめて送る。 - const frameBuffers: Buffer[] = []; - let frameIndex = 0; - let targetIndex = 0; - let nextIndex = 1; - for await (const path of this.asyncIterateFrames(outDir, command)) { - try { - const index = frameIndex++; - if (index !== targetIndex) { - continue; - } - targetIndex = nextIndex; - nextIndex += index; // fibonacci sequence によってフレーム数制限を掛ける - frameBuffers.push(await fs.promises.readFile(path)); - } finally { - fs.promises.unlink(path); - } - } - const predictions = await this.sensitiveMediaDetectionService.detectSensitiveMany(frameBuffers); - const results = predictions.filter((x): x is Prediction[] => x != null).map(x => judgePrediction(x)); - // 判定に成功したフレームが 0 件のとき(接続先未設定・通信失敗等)は、 - // Math.ceil(0) との比較が 0 >= 0 で真になり全動画がセンシティブ扱いになってしまうため、 - // 1 件以上判定できたときのみ集約する(失敗時は非センシティブ扱い: misskey-dev/misskey#16804)。 - if (results.length > 0) { - sensitive = results.filter(x => x[0]).length >= Math.ceil(results.length * sensitiveThreshold); - porn = results.filter(x => x[1]).length >= Math.ceil(results.length * sensitiveThresholdForPorn); - } - } finally { - disposeOutDir(); + // 判定対象フレームを選定して正規化済みバッファとして集め、外部サービスへまとめて送る。 + const frameBuffers = await this.extractFramesForDetection(source); + const predictions = await this.sensitiveMediaDetectionService.detectSensitiveMany(frameBuffers); + const results = predictions.filter((x): x is Prediction[] => x != null).map(x => judgePrediction(x)); + // 判定に成功したフレームが 0 件のとき(接続先未設定・通信失敗等)は、 + // Math.ceil(0) との比較が 0 >= 0 で真になり全動画がセンシティブ扱いになってしまうため、 + // 1 件以上判定できたときのみ集約する(失敗時は非センシティブ扱い: misskey-dev/misskey#16804)。 + if (results.length > 0) { + sensitive = results.filter(x => x[0]).length >= Math.ceil(results.length * sensitiveThreshold); + porn = results.filter(x => x[1]).length >= Math.ceil(results.length * sensitiveThresholdForPorn); } } else if (isMimeImage(mime, 'sharp-convertible-image-with-bmp')) { /* @@ -300,47 +258,71 @@ export class FileInfoService { return [sensitive, porn]; } - private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator { - const watcher = new FSWatcher({ - cwd, - }); - let finished = false; - command.once('end', () => { - finished = true; - watcher.close(); - }); - command.run(); - for (let i = 1; true; i++) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition - const current = `${i}.png`; - const next = `${i + 1}.png`; - const framePath = join(cwd, current); - if (await this.exists(join(cwd, next))) { - yield framePath; - } else if (!finished) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition - watcher.add(next); - await new Promise((resolve, reject) => { - watcher.on('add', function onAdd(path) { - if (path === next) { // 次フレームの書き出しが始まっているなら、現在フレームの書き出しは終わっている - watcher.unwatch(current); - watcher.off('add', onAdd); - resolve(); - } - }); - command.once('end', resolve); // 全てのフレームを処理し終わったなら、最終フレームである現在フレームの書き出しは終わっている - command.once('error', reject); - }); - yield framePath; - } else if (await this.exists(framePath)) { - yield framePath; - } else { - return; + /** + * センシティブ判定に掛けるフレームを、正規化済みの PNG バッファとして抽出する。 + * + * 制限時間を超えた場合はそこまでに集まった分を返す。フレームが 1 枚も取れなくても + * 呼び出し元の「判定成功 0 件なら非センシティブ」の扱いに落ちるため、例外にはしない。 + * + * @param source ファイルパス + * @returns 299x299 に正規化された PNG バッファの配列 + */ + @bindThis + private async extractFramesForDetection(source: string): Promise { + const signal = AbortSignal.timeout(FRAME_EXTRACTION_TIMEOUT_MS); + const frameBuffers: Buffer[] = []; + + try { + await using demuxer = await Demuxer.open(source, { signal }); + + const videoStream = demuxer.video(); + if (videoStream == null) return frameBuffers; + + using decoder = await Decoder.create(videoStream, { + signal, + options: { + 'skip_frame': 'nokey', // 可能ならキーフレームのみを取得してほしいとする(そうなるとは限らない) + 'lowres': '3', // 元の画質でデコードする必要はないので 1/8 画質でデコードしてもよいとする(そうなるとは限らない) + }, + }); + // 暗さに関わらず全てのフレームで測定値を取り、lavfi.blackframe.pblack として受け取る + using filter = FilterAPI.create('blackframe=amount=0', { signal }); + using scaler = new Scaler(); + + let frameIndex = 0; + let targetIndex = 0; + let nextIndex = 1; + + for await (using frame of decoder.frames(demuxer.packets(videoStream.index))) { + // I-Frame のみを対象にする(VP9 とかはデコードしてみないとわからないっぽい) + if (frame == null || frame.pictType !== AV_PICTURE_TYPE_I) continue; + + for await (using measured of filter.frames(frame)) { + if (measured == null) continue; + + // フレームにおける暗部の百分率。取得できなかったフレームは判定に使わない + // (Number(null) が 0 になり、真っ暗なフレームを通してしまうため明示的に弾く) + const pblack = measured.getMetadata().get('lavfi.blackframe.pblack'); + if (pblack == null || !(Number(pblack) < MAX_BLACK_PERCENTAGE)) continue; + + const index = frameIndex++; + if (index !== targetIndex) continue; + const following = targetIndex + nextIndex; + targetIndex = nextIndex; + nextIndex = Math.max(following, targetIndex + 1); + + frameBuffers.push(await scaler.toPng(measured, { + resize: { width: DETECTION_IMAGE_SIZE, height: DETECTION_IMAGE_SIZE }, + })); + } } + } catch (err) { + // 時間切れは想定内。集まった分だけで判定を続ける + if (!isAbortError(err)) throw err; + this.logger.warn(`Frame extraction timed out after ${FRAME_EXTRACTION_TIMEOUT_MS}ms, using ${frameBuffers.length} frame(s). File path: ${source}`); } - } - @bindThis - private exists(path: string): Promise { - return fs.promises.access(path).then(() => true, () => false); + return frameBuffers; } @bindThis @@ -364,24 +346,17 @@ export class FileInfoService { * @returns ビデオトラックがあるかどうか(エラー発生時は常に`true`を返す) */ @bindThis - private hasVideoTrackOnVideoFile(path: string): Promise { - const sublogger = this.logger.createSubLogger('ffprobe'); + private async hasVideoTrackOnVideoFile(path: string): Promise { + const sublogger = this.logger.createSubLogger('probe'); sublogger.info(`Checking the video file. File path: ${path}`); - return new Promise((resolve) => { - try { - FFmpeg.ffprobe(path, (err, metadata) => { - if (err) { - sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err); - resolve(true); - return; - } - resolve(metadata.streams.some((stream) => stream.codec_type === 'video')); - }); - } catch (err) { - sublogger.warn(`Could not check the video file. Returns true. File path: ${path}`, err as Error); - resolve(true); - } - }); + try { + const result = await probe(path, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) }); + return result.streams.some((stream) => stream.type === 'video'); + } catch (err) { + const reason = isAbortError(err) ? `Timed out after ${PROBE_TIMEOUT_MS}ms` : 'Could not check the video file'; + sublogger.warn(`${reason}. Returns true. File path: ${path}`, err as Error); + return true; + } } /** diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index 747fe4fc7ee..ff33d720c51 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -4,14 +4,25 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import FFmpeg from 'fluent-ffmpeg'; +import sharp from 'sharp'; +import { Decoder, Demuxer, Scaler } from 'node-av/api'; +import { AVSEEK_FLAG_BACKWARD } from 'node-av/constants'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { ImageProcessingService } from '@/core/ImageProcessingService.js'; import type { IImage } from '@/core/ImageProcessingService.js'; -import { createTempDir } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; import { appendQuery, query } from '@/misc/prelude/url.js'; +import '@/misc/node-av-log.js'; + +/** + * サムネイル生成全体の制限時間。 + * 壊れたファイルや極端に大きなファイルでデコードが張り付くのを防ぐ。 + */ +const THUMBNAIL_TIMEOUT_MS = 30 * 1000; + +/** サムネイルとして切り出す位置 (動画全体に対する割合) */ +const THUMBNAIL_POSITION_RATIO = 0.05; @Injectable() export class VideoProcessingService { @@ -25,27 +36,34 @@ export class VideoProcessingService { @bindThis public async generateVideoThumbnail(source: string): Promise { - const [dir, cleanup] = await createTempDir(); - - try { - await new Promise((res, rej) => { - FFmpeg({ - source, - }) - .on('end', res) - .on('error', rej) - .screenshot({ - folder: dir, - filename: 'out.png', // must have .png extension - count: 1, - timestamps: ['5%'], - }); - }); - - return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 422); - } finally { - cleanup(); + const signal = AbortSignal.timeout(THUMBNAIL_TIMEOUT_MS); + + await using demuxer = await Demuxer.open(source, { signal }); + + const videoStream = demuxer.video(); + if (videoStream == null) { + throw new Error(`No video stream found: ${source}`); + } + + // 冒頭は暗転やロゴであることが多いので、少し進んだ位置のキーフレームを狙う。 + // 尺が取得できないコンテナもあるため、その場合は先頭から読む。 + if (demuxer.duration > 0) { + // streamIndex を指定すると timestamp がそのストリームの time base として解釈されてしまうため、 + // 秒で指定できる全体シーク (-1) を使う + await demuxer.seek(demuxer.duration * THUMBNAIL_POSITION_RATIO, -1, AVSEEK_FLAG_BACKWARD); + } + + using decoder = await Decoder.create(videoStream, { signal }); + using scaler = new Scaler(); + + for await (using frame of decoder.frames(demuxer.packets(videoStream.index))) { + if (frame == null) continue; + + const png = await scaler.toPng(frame); + return await this.imageProcessingService.convertSharpToWebp(sharp(png), 498, 422); } + + throw new Error(`Could not decode any frame: ${source}`); } @bindThis diff --git a/packages/backend/src/misc/node-av-log.ts b/packages/backend/src/misc/node-av-log.ts new file mode 100644 index 00000000000..2b99cfab8a8 --- /dev/null +++ b/packages/backend/src/misc/node-av-log.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Log } from 'node-av/lib'; +import { AV_LOG_ERROR } from 'node-av/constants'; + +// libav 自体のログ出力を抑制する +// node-av を使うモジュールから副作用 import する必要がある +Log.setLevel(AV_LOG_ERROR); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c98f3c8207a..7d1890f961d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: '@aiscript-dev/aiscript-languageserver': '-' + node-av>werift: '-' chokidar: 5.0.0 lodash: 4.18.1 @@ -253,9 +254,6 @@ importers: chalk-template: specifier: 1.1.2 version: 1.1.2 - chokidar: - specifier: 5.0.0 - version: 5.0.0 color-convert: specifier: 3.1.3 version: 3.1.3 @@ -280,9 +278,6 @@ importers: file-type: specifier: 22.0.1 version: 22.0.1(supports-color@10.2.2) - fluent-ffmpeg: - specifier: 2.1.3 - version: 2.1.3 got: specifier: 15.1.0 version: 15.1.0 @@ -340,6 +335,9 @@ importers: nested-property: specifier: 4.0.0 version: 4.0.0 + node-av: + specifier: 6.1.1 + version: 6.1.1 node-fetch: specifier: 3.3.2 version: 3.3.2 @@ -446,9 +444,6 @@ importers: '@nestjs/platform-express': specifier: 11.1.29 version: 11.1.29(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.29)(supports-color@10.2.2) - '@rollup/plugin-esm-shim': - specifier: 0.1.8 - version: 0.1.8(rollup@4.62.4) '@sentry/vue': specifier: 10.70.0 version: 10.70.0(vue@3.5.41(@typescript/typescript6@6.0.2)) @@ -461,9 +456,6 @@ importers: '@types/archiver': specifier: 8.0.0 version: 8.0.0 - '@types/fluent-ffmpeg': - specifier: 2.1.28 - version: 2.1.28 '@types/http-link-header': specifier: 1.0.7 version: 1.0.7 @@ -573,6 +565,30 @@ importers: specifier: 5.1.1 version: 5.1.1(@typescript/typescript6@6.0.2)(vitest@4.1.10) optionalDependencies: + '@seydx/node-av-darwin-arm64': + specifier: 6.1.1 + version: 6.1.1 + '@seydx/node-av-darwin-x64': + specifier: 6.1.1 + version: 6.1.1 + '@seydx/node-av-linux-arm64': + specifier: 6.1.1 + version: 6.1.1 + '@seydx/node-av-linux-x64': + specifier: 6.1.1 + version: 6.1.1 + '@seydx/node-av-win32-arm64-mingw': + specifier: 6.1.1 + version: 6.1.1 + '@seydx/node-av-win32-arm64-msvc': + specifier: 6.1.1 + version: 6.1.1 + '@seydx/node-av-win32-x64-mingw': + specifier: 6.1.1 + version: 6.1.1 + '@seydx/node-av-win32-x64-msvc': + specifier: 6.1.1 + version: 6.1.1 bufferutil: specifier: 4.1.0 version: 4.1.0 @@ -3167,15 +3183,6 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - '@rollup/plugin-esm-shim@0.1.8': - resolution: {integrity: sha512-xEU0b/BShgDDSPjidhJd4R74J9xZ9jLVtFWNGtsUXyEsdwwwB1a3XOAwwGaNIyUHD6EhxPO21JMfUmJWoMn7SA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - '@rollup/plugin-json@6.1.0': resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} engines: {node: '>=14.0.0'} @@ -3455,6 +3462,54 @@ packages: pinia: optional: true + '@seydx/node-av-darwin-arm64@6.1.1': + resolution: {integrity: sha512-hJ07HDFwz96MI/EY1V89/jVWXMfn/xb/32vocNhAeqnvBRMZio+hDz8lQloGynqv8iow5y9mPb7fOuzt5cW1+w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@seydx/node-av-darwin-x64@6.1.1': + resolution: {integrity: sha512-7sfIEA/KkEMPMbfLlU9pGy34vE5iOpWryBFK87jDXeqLo4hB5p5qx2IDKcKfD6WBzEMk5FJScZz4rlT3J546Og==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@seydx/node-av-linux-arm64@6.1.1': + resolution: {integrity: sha512-E5LopQ+LaTGdjmnk3KYsrKF1qRQHT22xVGijUdAcvbScfLTJz2RuJoevKT53cuNupR1gdmRmvnmq+kac4TkDRA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@seydx/node-av-linux-x64@6.1.1': + resolution: {integrity: sha512-2HkBkMUSmA+1sP0PN7eFSkNJhbejgJDCXx0w0ZMRlTLqF+snz5ptf/gl/tQye9yER6I4OGW/7WVL5Y2PVneNCA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@seydx/node-av-win32-arm64-mingw@6.1.1': + resolution: {integrity: sha512-inhihhVGetTgpYaUQHfeTbG/PdBzYHqBRz79U/83C/64FHw7+sKQF125B7Hfle7tnTX3w7LoJQ+RxsoAP2RWgw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@seydx/node-av-win32-arm64-msvc@6.1.1': + resolution: {integrity: sha512-gESbRZhCkldciGV+e08PhgJ3Do2PnL1ZY8ZIIDTjutCZdw+lgZigvZGq8jQ+TnsV6Djf1pdcKx3Z2VGnIGId5Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@seydx/node-av-win32-x64-mingw@6.1.1': + resolution: {integrity: sha512-DqdNMsGTMcmg/y+DwHu+slFdP5VluCFNrefhUoq7mobu2kKk0i3LBN9xz2NdeG15jKB+DCeqFmt0r5iMGymOKA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@seydx/node-av-win32-x64-msvc@6.1.1': + resolution: {integrity: sha512-GLjjoOYTqh1rvTzawgBMVKgY+8OIXMyrgD1eIqKLy3L6rWJhGvsbY+kUy15XbatGTFwStTlHK9ovTi2Xj/+9lQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@shikijs/core@4.4.3': resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==} engines: {node: '>=20'} @@ -3872,9 +3927,6 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/fluent-ffmpeg@2.1.28': - resolution: {integrity: sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw==} - '@types/hammerjs@2.0.46': resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} @@ -4574,9 +4626,6 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} - async@0.2.10: - resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} - async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -4673,6 +4722,9 @@ packages: resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} hasBin: true + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + blurhash@2.0.5: resolution: {integrity: sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==} @@ -5017,9 +5069,6 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -5324,6 +5373,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -5764,11 +5816,6 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - fluent-ffmpeg@2.1.3: - resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} - engines: {node: '>=18'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} @@ -5802,6 +5849,10 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-extra@11.3.1: + resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} + engines: {node: '>=14.14'} + fs-extra@11.3.5: resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} engines: {node: '>=14.14'} @@ -6957,9 +7008,6 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - mock-socket@9.3.1: resolution: {integrity: sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==} engines: {node: '>= 8'} @@ -7069,6 +7117,9 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-av@6.1.1: + resolution: {integrity: sha512-nuj2cpAelVZQ8hWJB26i9wYIPI/l+GzIAw8zbZuepFpq52khjYQ8s5S+Qi2cG1D+zjb7TYx7QqKpiZH32tqo2A==} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -7098,6 +7149,9 @@ packages: node-html-parser@9.0.1: resolution: {integrity: sha512-QrdiYYm1NnLRXsMXThUgVcF/syWfWIgHFmy8hylWMGFbHFtnRuXLBxxGQvIm3xaIJMUDJ0ayOmT/FGJYT3pZIw==} + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + nodemailer@9.0.5: resolution: {integrity: sha512-wvjiKvjczmsN7U/8006JOdXubgBk2XFAbioDMbT+sM7cPs0QrhJTa6KBRX7P5REGGkDcLUz/EarWidb8G8C1jQ==} engines: {node: '>=6.0.0'} @@ -7443,9 +7497,6 @@ packages: resolution: {integrity: sha512-KrEAOa2aGlEyV+gN2jMXwSQhZ5gUR4WwivtPjSktYzh7srb70pYvVGV0uLfmoCx75fUQtqTairuoxayHLtvo9w==} engines: {node: '>=16.20.0'} - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - playwright-core@1.62.1: resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} engines: {node: '>=20'} @@ -8854,9 +8905,6 @@ packages: engines: {node: '>=16.20.0'} hasBin: true - ufo@1.6.4: - resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} - uid@2.0.2: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} @@ -8935,6 +8983,9 @@ packages: until-async@3.0.2: resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + unzipper@0.12.5: + resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==} + uri-js-replace@1.0.1: resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} @@ -9255,10 +9306,6 @@ packages: resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} - which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -11073,13 +11120,6 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} - '@rollup/plugin-esm-shim@0.1.8(rollup@4.62.4)': - dependencies: - magic-string: 0.30.21 - mlly: 1.8.2 - optionalDependencies: - rollup: 4.62.4 - '@rollup/plugin-json@6.1.0(rollup@4.62.4)': dependencies: '@rollup/pluginutils': 5.4.0(rollup@4.62.4) @@ -11321,6 +11361,30 @@ snapshots: '@sentry/core': 10.70.0 vue: 3.5.41(typescript@6.0.3) + '@seydx/node-av-darwin-arm64@6.1.1': + optional: true + + '@seydx/node-av-darwin-x64@6.1.1': + optional: true + + '@seydx/node-av-linux-arm64@6.1.1': + optional: true + + '@seydx/node-av-linux-x64@6.1.1': + optional: true + + '@seydx/node-av-win32-arm64-mingw@6.1.1': + optional: true + + '@seydx/node-av-win32-arm64-msvc@6.1.1': + optional: true + + '@seydx/node-av-win32-x64-mingw@6.1.1': + optional: true + + '@seydx/node-av-win32-x64-msvc@6.1.1': + optional: true + '@shikijs/core@4.4.3': dependencies: '@shikijs/primitive': 4.4.3 @@ -11849,10 +11913,6 @@ snapshots: '@types/estree@1.0.9': {} - '@types/fluent-ffmpeg@2.1.28': - dependencies: - '@types/node': 26.2.0 - '@types/hammerjs@2.0.46': {} '@types/hast@3.0.5': @@ -12871,8 +12931,6 @@ snapshots: async-function@1.0.0: {} - async@0.2.10: {} - async@3.2.6: {} asynckit@0.4.0: {} @@ -12956,6 +13014,8 @@ snapshots: bcryptjs@3.0.3: {} + bluebird@3.7.2: {} + blurhash@2.0.5: {} bn.js@4.12.3: {} @@ -13291,8 +13351,6 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 - confbox@0.1.8: {} - config-chain@1.1.13: dependencies: ini: 1.3.8 @@ -13592,6 +13650,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + eastasianwidth@0.2.0: {} ecc-jsbn@0.1.2: @@ -14411,11 +14473,6 @@ snapshots: flatted@3.4.2: {} - fluent-ffmpeg@2.1.3: - dependencies: - async: 0.2.10 - which: 1.3.1 - follow-redirects@1.16.0(debug@4.4.3(supports-color@10.2.2)): optionalDependencies: debug: 4.4.3(supports-color@10.2.2) @@ -14445,6 +14502,12 @@ snapshots: fresh@2.0.0: {} + fs-extra@11.3.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fs-extra@11.3.5: dependencies: graceful-fs: 4.2.11 @@ -15785,13 +15848,6 @@ snapshots: dependencies: minipass: 7.1.3 - mlly@1.8.2: - dependencies: - acorn: 8.17.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.4 - mock-socket@9.3.1: {} module-details-from-path@1.0.4: {} @@ -15955,6 +16011,19 @@ snapshots: node-addon-api@7.1.1: optional: true + node-av@6.1.1: + dependencies: + unzipper: 0.12.5 + optionalDependencies: + '@seydx/node-av-darwin-arm64': 6.1.1 + '@seydx/node-av-darwin-x64': 6.1.1 + '@seydx/node-av-linux-arm64': 6.1.1 + '@seydx/node-av-linux-x64': 6.1.1 + '@seydx/node-av-win32-arm64-mingw': 6.1.1 + '@seydx/node-av-win32-arm64-msvc': 6.1.1 + '@seydx/node-av-win32-x64-mingw': 6.1.1 + '@seydx/node-av-win32-x64-msvc': 6.1.1 + node-domexception@1.0.0: {} node-exports-info@1.6.0: @@ -15996,6 +16065,8 @@ snapshots: css-select: 5.2.2 entities: 8.0.0 + node-int64@0.4.0: {} + nodemailer@9.0.5: {} nodemon@3.1.14: @@ -16403,12 +16474,6 @@ snapshots: pkce-challenge@6.0.0: {} - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 - playwright-core@1.62.1: {} playwright@1.62.1: @@ -17932,8 +17997,6 @@ snapshots: '@typescript/typescript-win32-arm64': 7.0.2 '@typescript/typescript-win32-x64': 7.0.2 - ufo@1.6.4: {} - uid@2.0.2: dependencies: '@lukeed/csprng': 1.1.0 @@ -18014,6 +18077,14 @@ snapshots: until-async@3.0.2: {} + unzipper@0.12.5: + dependencies: + bluebird: 3.7.2 + duplexer2: 0.1.4 + fs-extra: 11.3.1 + graceful-fs: 4.2.11 + node-int64: 0.4.0 + uri-js-replace@1.0.1: {} uri-js@4.4.1: @@ -18400,10 +18471,6 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 - which@1.3.1: - dependencies: - isexe: 2.0.0 - which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c099bc7a33e..c21f3fefb86 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -31,6 +31,7 @@ allowBuilds: v-code-diff: true vue-demi: true exifreader: false + node-av: false # napiバイナリで足りるため不要 protobufjs: false minimumReleaseAge: 10080 # delay 7days to mitigate supply-chain attack minimumReleaseAgeExclude: @@ -56,6 +57,7 @@ minimumReleaseAgeExclude: - vitest-mock-extended@5.1.1 # そのうち消す overrides: '@aiscript-dev/aiscript-languageserver': '-' + 'node-av>werift': '-' # node-av の WebRTC でしか使われない optional dependency で使用しないためインストール抑止。今後のバージョンで公式に optional peer dependency に移行するため、タイミングを見て消す chokidar: 5.0.0 lodash: 4.18.1 engineStrict: true