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 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 23:37:14 +00:00
co-authored by Claude Opus 4.8
parent 510254cbd4
commit 82d0e9afd6
4 changed files with 33 additions and 192 deletions
@@ -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<HTMLDivElement>(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' ? (
<ImageRenderer src={getRawUrl(filePath, root)} fileName={fileName} />
) : fileType === 'video' ? (
<VideoRenderer
src={videoSrc}
fileName={fileName}
fallbackSrc={needsTranscode(fileName) ? undefined : getTranscodeUrl(filePath, root)}
/>
<VideoRenderer src={videoSrc} fileName={fileName} />
) : fileType === 'audio' ? (
<AudioRenderer src={getRawUrl(filePath, root)} fileName={fileName} autoPlay={autoPlay} />
) : content !== null && editing && isJson ? (
@@ -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']) {
@@ -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<HTMLVideoElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const hideTimer = useRef<ReturnType<typeof setTimeout>>(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);
}}
>
<video ref={videoRef} src={src} preload="metadata" playsInline className="max-w-full max-h-full" onClick={togglePlay} />
<video
ref={videoRef}
src={src}
preload="metadata"
playsInline
className="max-w-full max-h-full"
onClick={togglePlay}
/>
{loaded && !playing && (
<button onClick={togglePlay} className="absolute inset-0 flex items-center justify-center cursor-pointer">
@@ -183,7 +160,10 @@ export const VideoRenderer = ({ src, fileName, fallbackSrc }: VideoRendererProps
</button>
<div className="flex items-center gap-1.5">
<button onClick={toggleMute} className="p-1 text-white/70 hover:text-white transition-colors cursor-pointer">
<button
onClick={toggleMute}
className="p-1 text-white/70 hover:text-white transition-colors cursor-pointer"
>
{muted ? <VolumeX className="h-4 w-4" /> : <Volume2 className="h-4 w-4" />}
</button>
<input
@@ -205,7 +185,10 @@ export const VideoRenderer = ({ src, fileName, fallbackSrc }: VideoRendererProps
<span className="text-[9px] font-mono text-white/30 uppercase tracking-wider">{getExt(fileName)}</span>
<button onClick={toggleFullscreen} className="p-1 text-white/70 hover:text-white transition-colors cursor-pointer">
<button
onClick={toggleFullscreen}
className="p-1 text-white/70 hover:text-white transition-colors cursor-pointer"
>
{isFullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
</button>
</div>