From 82d0e9afd690fa8e9fcfef377151c26480e23833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Mon, 20 Jul 2026 23:37:14 +0000 Subject: [PATCH] remove ffmpeg video transcode, back to native-only playback Drop the /transcode (and dead /transcode-audio) routes and the frontend mkv/avi transcode wiring. Only browser-native formats (mp4, webm, mov, m4v, ogv) are classified as video now; other containers fall through to the generic file view. Co-Authored-By: Claude Opus 4.8 --- src/servers/api/file-browser/router.ts | 112 +----------------- .../src/apps/FileViewer/FileViewerBody.tsx | 14 +-- .../src/apps/FileViewer/file-types.ts | 46 ++----- .../FileViewer/renderers/VideoRenderer.tsx | 53 +++------ 4 files changed, 33 insertions(+), 192 deletions(-) diff --git a/src/servers/api/file-browser/router.ts b/src/servers/api/file-browser/router.ts index 1233a603..e0ab8fcc 100644 --- a/src/servers/api/file-browser/router.ts +++ b/src/servers/api/file-browser/router.ts @@ -1,6 +1,6 @@ import { createRouter } from '@@/create-router'; import { resolve, dirname, join, parse as parsePath } from 'node:path'; -import { readdir, stat, mkdir, rm, rename, readFile, cp, unlink } from 'node:fs/promises'; +import { readdir, stat, mkdir, rm, rename, readFile, cp } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { getHomeDir, DATA_PATH } from '@@/data-path'; import * as errors from '@@/custom-errors'; @@ -261,116 +261,8 @@ router.get('/raw', async (ctx) => { }); }); -// Transcode video via ffmpeg with caching — outputs a seekable MP4 file -router.get('/transcode', async (ctx) => { - const user = ctx.get('user'); - const rootDir = getRootDir(user, ctx.req.query('root') ?? undefined); - const relPath = (ctx.req.query('path') || '').replace(/^\/+/, ''); - if (!relPath) throw errors.BAD_REQUEST('path is required'); - - const absPath = resolveUserPath(rootDir, relPath); - const s = await stat(absPath); - if (s.isDirectory()) throw errors.BAD_REQUEST('Cannot transcode a directory'); - - const userDataDir = getUserDataDir(user.email); - const { dir, name } = parsePath(relPath); - const cacheRel = dir ? `cache/video/${dir}/${name}.mp4` : `cache/video/${name}.mp4`; - const cacheAbs = resolve(userDataDir, cacheRel); - - if (!existsSync(cacheAbs)) { - await mkdir(dirname(cacheAbs), { recursive: true }); - const tmpPath = cacheAbs + '.tmp'; - - const proc = Bun.spawn( - [ - 'ffmpeg', - '-i', - absPath, - '-c:v', - 'libx264', - '-preset', - 'ultrafast', - '-crf', - '23', - '-c:a', - 'aac', - '-b:a', - '128k', - '-movflags', - '+faststart', - '-y', - tmpPath, - ], - { stdout: 'ignore', stderr: 'pipe' }, - ); - const exitCode = await proc.exited; - - if (exitCode !== 0) { - const stderr = await new Response(proc.stderr).text(); - await unlink(tmpPath).catch(() => {}); - throw errors.BAD_REQUEST(stderr.trim() || 'Video transcoding failed'); - } - - await rename(tmpPath, cacheAbs); - } - - const file = Bun.file(cacheAbs); - const total = file.size; - const rangeHeader = ctx.req.header('range'); - - if (rangeHeader) { - const match = rangeHeader.match(/bytes=(\d*)-(\d*)/); - if (match) { - const start = match[1] ? parseInt(match[1], 10) : 0; - const end = match[2] ? parseInt(match[2], 10) : total - 1; - const chunkSize = end - start + 1; - return new Response(file.slice(start, end + 1), { - status: 206, - headers: { - 'Content-Type': 'video/mp4', - 'Content-Range': `bytes ${start}-${end}/${total}`, - 'Content-Length': String(chunkSize), - 'Accept-Ranges': 'bytes', - }, - }); - } - } - - return new Response(file, { - headers: { - 'Content-Type': 'video/mp4', - 'Content-Length': String(total), - 'Accept-Ranges': 'bytes', - }, - }); -}); - -// Transcode audio via ffmpeg for universal playback (outputs MP3) -router.get('/transcode-audio', async (ctx) => { - const user = ctx.get('user'); - const rootDir = getRootDir(user, ctx.req.query('root') ?? undefined); - const relPath = (ctx.req.query('path') || '').replace(/^\/+/, ''); - if (!relPath) throw errors.BAD_REQUEST('path is required'); - - const absPath = resolveUserPath(rootDir, relPath); - const s = await stat(absPath); - if (s.isDirectory()) throw errors.BAD_REQUEST('Cannot transcode a directory'); - - const proc = Bun.spawn(['ffmpeg', '-i', absPath, '-c:a', 'libmp3lame', '-q:a', '2', '-f', 'mp3', 'pipe:1'], { - stdout: 'pipe', - stderr: 'ignore', - }); - - return new Response(proc.stdout as ReadableStream, { - headers: { - 'Content-Type': 'audio/mpeg', - 'Transfer-Encoding': 'chunked', - }, - }); -}); - // Save a cached result (ocr/tts/transcriptions/audio) next to the original file -const CACHE_PREFIXES = ['cache/ocr/', 'cache/tts/', 'cache/transcriptions/', 'cache/audio/', 'cache/video/']; +const CACHE_PREFIXES = ['cache/ocr/', 'cache/tts/', 'cache/transcriptions/', 'cache/audio/']; router.post('/save-result', async (ctx) => { const user = ctx.get('user'); diff --git a/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx b/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx index 44608c0f..e82624f1 100644 --- a/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx +++ b/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx @@ -1,6 +1,6 @@ import { useMemo, useRef } from 'react'; import { Loader2, FolderArchive } from 'lucide-react'; -import { getLang, getRawUrl, getTranscodeUrl, needsTranscode, getArchiveBaseName } from './file-types'; +import { getLang, getRawUrl, getArchiveBaseName } from './file-types'; import { useFileViewer } from './FileViewerContext'; import { PdfRenderer } from './renderers/PdfRenderer'; import { ImageRenderer } from './renderers/ImageRenderer'; @@ -14,11 +14,11 @@ import { EditorRenderer } from './renderers/EditorRenderer'; import { JsonEditorRenderer } from './renderers/JsonEditorRenderer'; export const FileViewerBody = () => { - const { filePath, fileName, root, fileType, content, loading, error, autoPlay, editing, setContent } = useFileViewer(); + const { filePath, fileName, root, fileType, content, loading, error, autoPlay, editing, setContent } = + useFileViewer(); const scrollRef = useRef(null); - const videoSrc = - fileType === 'video' ? (needsTranscode(fileName) ? getTranscodeUrl(filePath, root) : getRawUrl(filePath, root)) : ''; + const videoSrc = fileType === 'video' ? getRawUrl(filePath, root) : ''; const isJson = useMemo(() => { if (!content) return false; @@ -52,11 +52,7 @@ export const FileViewerBody = () => { ) : fileType === 'image' ? ( ) : fileType === 'video' ? ( - + ) : fileType === 'audio' ? ( ) : content !== null && editing && isJson ? ( diff --git a/src/workspaces/officerdev/src/apps/FileViewer/file-types.ts b/src/workspaces/officerdev/src/apps/FileViewer/file-types.ts index 088ff119..8a5730aa 100644 --- a/src/workspaces/officerdev/src/apps/FileViewer/file-types.ts +++ b/src/workspaces/officerdev/src/apps/FileViewer/file-types.ts @@ -7,24 +7,6 @@ export type FileType = 'markdown' | 'audio' | 'video' | 'image' | 'pdf' | 'code' export const ARCHIVE_EXTS = ['zip', 'tar', 'gz', 'tgz', 'bz2', 'tbz2', 'xz', 'txz', 'zst', '7z', 'rar']; export const AUDIO_EXTS = ['mp3', 'wav', 'flac', 'ogg', 'oga', 'opus', 'aac', 'm4a', 'wma']; export const NATIVE_VIDEO_EXTS = ['mp4', 'm4v', 'webm', 'ogv', 'mov']; -export const TRANSCODE_VIDEO_EXTS = [ - 'mkv', - 'avi', - 'wmv', - 'flv', - 'ts', - 'mts', - 'm2ts', - '3gp', - '3g2', - 'vob', - 'divx', - 'asf', - 'f4v', - 'rm', - 'rmvb', -]; -export const ALL_VIDEO_EXTS = [...NATIVE_VIDEO_EXTS, ...TRANSCODE_VIDEO_EXTS]; export const IMAGE_EXTS = [ 'jpg', 'jpeg', @@ -107,9 +89,15 @@ export function getFileType(name: string): FileType { if (ext === 'pdf') return 'pdf'; if (EXT_TO_LANG[ext]) return 'code'; if (AUDIO_EXTS.includes(ext)) return 'audio'; - if (ALL_VIDEO_EXTS.includes(ext)) return 'video'; + if (NATIVE_VIDEO_EXTS.includes(ext)) return 'video'; if (IMAGE_EXTS.includes(ext)) return 'image'; - if (baseName.endsWith('.tar.gz') || baseName.endsWith('.tar.bz2') || baseName.endsWith('.tar.xz') || baseName.endsWith('.tar.zst')) return 'archive'; + if ( + baseName.endsWith('.tar.gz') || + baseName.endsWith('.tar.bz2') || + baseName.endsWith('.tar.xz') || + baseName.endsWith('.tar.zst') + ) + return 'archive'; if (ARCHIVE_EXTS.includes(ext)) return 'archive'; if (baseName === 'dockerfile' || baseName === 'makefile') return 'code'; return 'text'; @@ -127,10 +115,6 @@ export function getExt(name: string): string { return name.split('.').pop()?.toLowerCase() ?? ''; } -export function needsTranscode(name: string): boolean { - return TRANSCODE_VIDEO_EXTS.includes(getExt(name)); -} - export function formatTime(s: number): string { if (!isFinite(s) || isNaN(s)) return '0:00'; const h = Math.floor(s / 3600); @@ -147,20 +131,6 @@ export function getRawUrl(filePath: string, root?: string): string { return `${API_URL}/file-browser/raw?path=${encodeURIComponent(filePath)}&token=${encodeURIComponent(token)}${rootParam}`; } -export function getTranscodeUrl(filePath: string, root?: string): string { - const headers = getHeaders(); - const token = headers['Authorization']?.replace('Bearer ', '') ?? ''; - const rootParam = root && root !== 'home' ? `&root=${encodeURIComponent(root)}` : ''; - return `${API_URL}/file-browser/transcode?path=${encodeURIComponent(filePath)}&token=${encodeURIComponent(token)}${rootParam}`; -} - -export function getTranscodeAudioUrl(filePath: string, root?: string): string { - const headers = getHeaders(); - const token = headers['Authorization']?.replace('Bearer ', '') ?? ''; - const rootParam = root && root !== 'home' ? `&root=${encodeURIComponent(root)}` : ''; - return `${API_URL}/file-browser/transcode-audio?path=${encodeURIComponent(filePath)}&token=${encodeURIComponent(token)}${rootParam}`; -} - export function getArchiveBaseName(name: string): string { const lower = name.toLowerCase(); for (const compound of ['.tar.gz', '.tar.bz2', '.tar.xz', '.tar.zst']) { diff --git a/src/workspaces/officerdev/src/apps/FileViewer/renderers/VideoRenderer.tsx b/src/workspaces/officerdev/src/apps/FileViewer/renderers/VideoRenderer.tsx index dffd4a29..d984eec5 100644 --- a/src/workspaces/officerdev/src/apps/FileViewer/renderers/VideoRenderer.tsx +++ b/src/workspaces/officerdev/src/apps/FileViewer/renderers/VideoRenderer.tsx @@ -6,10 +6,9 @@ import { useSeekBar, SeekBar } from './SeekBar'; type VideoRendererProps = { src: string; fileName: string; - fallbackSrc?: string; }; -export const VideoRenderer = ({ src, fileName, fallbackSrc }: VideoRendererProps) => { +export const VideoRenderer = ({ src, fileName }: VideoRendererProps) => { const videoRef = useRef(null); const containerRef = useRef(null); const hideTimer = useRef>(null); @@ -27,7 +26,6 @@ export const VideoRenderer = ({ src, fileName, fallbackSrc }: VideoRendererProps useEffect(() => { const v = videoRef.current; if (!v) return; - let blobUrl: string | null = null; const onLoaded = () => { setDuration(v.duration); setLoaded(true); @@ -36,34 +34,7 @@ export const VideoRenderer = ({ src, fileName, fallbackSrc }: VideoRendererProps const onPlay = () => setPlaying(true); const onPause = () => setPlaying(false); const onEnded = () => setPlaying(false); - let fetching = false; - let fetchDone = false; - const onError = () => { - if (fetching) return; - if (fetchDone) { - setError(true); - return; - } - fetching = true; - const fetchUrl = fallbackSrc || src; - fetch(fetchUrl) - .then((res) => { - if (!res.ok) throw new Error(); - return res.blob(); - }) - .then((blob) => { - fetching = false; - fetchDone = true; - blobUrl = URL.createObjectURL(blob); - v.src = blobUrl; - v.load(); - }) - .catch(() => { - fetching = false; - fetchDone = true; - setError(true); - }); - }; + const onError = () => setError(true); v.addEventListener('loadedmetadata', onLoaded); v.addEventListener('timeupdate', onTime); v.addEventListener('play', onPlay); @@ -77,7 +48,6 @@ export const VideoRenderer = ({ src, fileName, fallbackSrc }: VideoRendererProps v.removeEventListener('pause', onPause); v.removeEventListener('ended', onEnded); v.removeEventListener('error', onError); - if (blobUrl) URL.revokeObjectURL(blobUrl); }; }, []); @@ -147,7 +117,14 @@ export const VideoRenderer = ({ src, fileName, fallbackSrc }: VideoRendererProps if (playing) setShowControls(false); }} > -