feat: client-side video optimization - remove server transcoding for instant loading and zero CPU

This commit is contained in:
Khoa Vo 2025-12-20 14:57:01 +07:00
parent 13841f479e
commit aad352a80f
3 changed files with 91 additions and 79 deletions

View file

@ -190,18 +190,21 @@ async def proxy_video(
):
"""
Proxy video with LRU caching for mobile optimization.
Prefers H.264 codec for browser compatibility, re-encodes HEVC if needed.
OPTIMIZED: No server-side transcoding - client handles decoding.
This reduces server CPU to ~0% during video playback.
"""
import yt_dlp
import re
import subprocess
# Check cache first
cached_path = get_cached_path(url)
if cached_path:
print(f"CACHE HIT: {url[:50]}...")
response_headers = {}
response_headers = {
"Accept-Ranges": "bytes",
"Cache-Control": "public, max-age=3600",
}
if download:
video_id_match = re.search(r'/video/(\d+)', url)
video_id = video_id_match.group(1) if video_id_match else "tiktok_video"
@ -232,10 +235,10 @@ async def proxy_video(
cookie_file.close()
cookie_file_path = cookie_file.name
# Prefer H.264 (avc1) over HEVC (hvc1/hev1) for browser compatibility
# Format selection: prefer mp4 with h264, fallback to best mp4
# Download best quality - NO TRANSCODING (let client decode)
# Prefer H.264 when available, but accept any codec
ydl_opts = {
'format': 'best[ext=mp4][vcodec^=avc]/best[ext=mp4]/best', # Prefer H.264
'format': 'best[ext=mp4][vcodec^=avc]/best[ext=mp4]/best',
'outtmpl': output_template,
'quiet': True,
'no_warnings': True,
@ -249,7 +252,7 @@ async def proxy_video(
ydl_opts['cookiefile'] = cookie_file_path
video_path = None
video_codec = None
video_codec = "unknown"
try:
loop = asyncio.get_event_loop()
@ -258,8 +261,7 @@ async def proxy_video(
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=True)
ext = info.get('ext', 'mp4')
# Get codec info
vcodec = info.get('vcodec', '') or ''
vcodec = info.get('vcodec', 'unknown') or 'unknown'
return os.path.join(temp_dir, f"video.{ext}"), vcodec
video_path, video_codec = await loop.run_in_executor(None, download_video)
@ -267,70 +269,9 @@ async def proxy_video(
if not os.path.exists(video_path):
raise Exception("Video file not created")
print(f"Downloaded codec: {video_codec}")
print(f"Downloaded codec: {video_codec} (no transcoding - client will decode)")
# Check if we need to re-encode HEVC to H.264
# If codec unknown, probe the actual file
is_hevc = False
if video_codec:
is_hevc = any(x in video_codec.lower() for x in ['hevc', 'hev1', 'hvc1', 'h265', 'h.265'])
else:
# Try probing with yt-dlp's ffprobe
try:
probe_cmd = ['ffprobe', '-v', 'error', '-select_streams', 'v:0',
'-show_entries', 'stream=codec_name', '-of', 'csv=p=0', video_path]
probe_result = subprocess.run(probe_cmd, capture_output=True, timeout=10)
detected_codec = probe_result.stdout.decode().strip().lower()
is_hevc = 'hevc' in detected_codec or 'h265' in detected_codec
print(f"Probed codec: {detected_codec}")
except Exception as probe_err:
print(f"Probe failed: {probe_err}")
if is_hevc:
print(f"HEVC detected, re-encoding to H.264 for browser compatibility...")
h264_path = os.path.join(temp_dir, "video_h264.mp4")
def reencode_to_h264():
# Use ffmpeg to re-encode from HEVC to H.264
cmd = [
'ffmpeg', '-y',
'-i', video_path,
'-c:v', 'libx264', # H.264 codec
'-preset', 'fast', # Balance speed and quality
'-crf', '23', # Quality (lower = better, 18-28 is good)
'-c:a', 'aac', # AAC audio
'-movflags', '+faststart', # Web optimization
h264_path
]
try:
result = subprocess.run(cmd, capture_output=True, timeout=120)
if result.returncode != 0:
print(f"FFmpeg error: {result.stderr.decode()[:200]}")
return None
return h264_path
except FileNotFoundError:
print("FFmpeg not installed, trying yt-dlp postprocessor...")
# Fallback: re-download with recode postprocessor
recode_opts = ydl_opts.copy()
recode_opts['postprocessors'] = [{'key': 'FFmpegVideoConvertor', 'preferedformat': 'mp4'}]
recode_opts['postprocessor_args'] = ['-c:v', 'libx264', '-preset', 'fast', '-crf', '23']
recode_opts['outtmpl'] = h264_path.replace('.mp4', '.%(ext)s')
try:
with yt_dlp.YoutubeDL(recode_opts) as ydl:
ydl.download([url])
return h264_path if os.path.exists(h264_path) else None
except:
return None
reencoded_path = await loop.run_in_executor(None, reencode_to_h264)
if reencoded_path and os.path.exists(reencoded_path):
video_path = reencoded_path
print("Re-encoding successful!")
else:
print("Re-encoding failed, using original video")
# Save to cache for future requests
# Save to cache directly - NO TRANSCODING
cached_path = save_to_cache(url, video_path)
stats = get_cache_stats()
print(f"CACHED: {url[:50]}... ({stats['files']} files, {stats['size_mb']}MB total)")
@ -349,8 +290,12 @@ async def proxy_video(
os.unlink(cookie_file_path)
shutil.rmtree(temp_dir, ignore_errors=True)
# Return from cache
response_headers = {}
# Return from cache with codec info header
response_headers = {
"Accept-Ranges": "bytes",
"Cache-Control": "public, max-age=3600",
"X-Video-Codec": video_codec, # Let client know the codec
}
if download:
video_id_match = re.search(r'/video/(\d+)', url)
video_id = video_id_match.group(1) if video_id_match else "tiktok_video"
@ -363,6 +308,7 @@ async def proxy_video(
)
@router.get("/thin-proxy")
async def thin_proxy_video(
request: Request,

View file

@ -81,6 +81,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@ -1409,6 +1410,7 @@
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@ -1426,6 +1428,7 @@
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@ -1486,6 +1489,7 @@
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/types": "8.50.0",
@ -1737,6 +1741,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -1959,6 +1964,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@ -2364,6 +2370,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@ -3007,6 +3014,7 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@ -3507,6 +3515,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -3702,6 +3711,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@ -3714,6 +3724,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@ -4121,6 +4132,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -4186,6 +4198,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -4279,6 +4292,7 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",

View file

@ -1,9 +1,16 @@
import React, { useRef, useState, useEffect } from 'react';
import { Download, UserPlus, Check, Volume2, VolumeX } from 'lucide-react';
import { Download, UserPlus, Check, Volume2, VolumeX, AlertCircle } from 'lucide-react';
import type { Video } from '../types';
import { API_BASE_URL } from '../config';
import { videoCache } from '../utils/videoCache';
// Check if browser supports HEVC codec (Safari, Chrome 107+, Edge)
const supportsHEVC = (): boolean => {
if (typeof MediaSource === 'undefined') return false;
return MediaSource.isTypeSupported('video/mp4; codecs="hvc1"') ||
MediaSource.isTypeSupported('video/mp4; codecs="hev1"');
};
interface HeartParticle {
id: number;
x: number;
@ -42,9 +49,15 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
const [localMuted, setLocalMuted] = useState(true);
const isMuted = externalMuted !== undefined ? externalMuted : localMuted;
const [hearts, setHearts] = useState<HeartParticle[]>([]);
<<<<<<< HEAD
const [isLoading, setIsLoading] = useState(true);
const [cachedUrl, setCachedUrl] = useState<string | null>(null);
=======
const [isLoading, setIsLoading] = useState(true); // Show loading spinner until video is ready
const [codecError, setCodecError] = useState(false); // True if video codec not supported
>>>>>>> 6153739 (feat: client-side video optimization - remove server transcoding for instant loading and zero CPU)
const lastTapRef = useRef<number>(0);
const browserSupportsHEVC = useRef(supportsHEVC());
const fullProxyUrl = `${API_BASE_URL}/feed/proxy?url=${encodeURIComponent(video.url)}`;
const thinProxyUrl = video.cdn_url ? `${API_BASE_URL}/feed/thin-proxy?cdn_url=${encodeURIComponent(video.cdn_url)}` : null;
@ -106,6 +119,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
// Reset fallback and loading state when video changes
useEffect(() => {
setUseFallback(false);
<<<<<<< HEAD
setIsLoading(true);
setCachedUrl(null);
@ -118,6 +132,10 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
};
checkCache();
=======
setIsLoading(true); // Show loading for new video
setCodecError(false); // Reset codec error for new video
>>>>>>> 6153739 (feat: client-side video optimization - remove server transcoding for instant loading and zero CPU)
}, [video.id]);
// Progress tracking
@ -134,7 +152,22 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
};
// Fallback on error - if thin proxy fails, switch to full proxy
const handleError = () => {
// Also detect codec errors for graceful fallback UI
const handleError = (e: Event) => {
const videoEl = e.target as HTMLVideoElement;
const error = videoEl?.error;
// Check if this is a codec/decode error (MEDIA_ERR_DECODE = 3)
if (error?.code === 3 || error?.code === 4) {
console.log(`Codec error detected (code ${error.code}):`, error.message);
// Only show codec error if browser doesn't support HEVC
if (!browserSupportsHEVC.current) {
setCodecError(true);
setIsLoading(false);
return;
}
}
if (thinProxyUrl && !useFallback) {
console.log('Thin proxy failed, falling back to full proxy...');
setUseFallback(true);
@ -329,13 +362,13 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
onClick={handleVideoClick}
onTouchStart={handleTouchStart}
>
{/* Video Element */}
{/* Video Element - preload="metadata" for instant player readiness */}
<video
ref={videoRef}
src={proxyUrl}
loop
playsInline
preload="auto"
preload="metadata"
muted={isMuted}
className="w-full h-full"
style={{ objectFit }}
@ -345,7 +378,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
/>
{/* Loading Overlay - Subtle pulsing logo */}
{isLoading && (
{isLoading && !codecError && (
<div className="absolute inset-0 flex items-center justify-center bg-black/40 z-20">
<div className="w-16 h-16 bg-gradient-to-r from-cyan-400/80 to-pink-500/80 rounded-2xl flex items-center justify-center animate-pulse">
<svg className="w-8 h-8 text-white" viewBox="0 0 24 24" fill="currentColor">
@ -355,6 +388,25 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
</div>
)}
{/* Codec Error Fallback - Graceful UI for unsupported video codecs */}
{codecError && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/80 z-20 p-6 text-center">
<AlertCircle className="w-12 h-12 text-amber-400 mb-3" />
<h3 className="text-white font-semibold text-lg mb-2">Video Format Not Supported</h3>
<p className="text-white/60 text-sm mb-4 max-w-xs">
This video uses HEVC codec. Try Safari, Chrome 107+, or download to watch.
</p>
<a
href={downloadUrl}
download
className="px-4 py-2 bg-gradient-to-r from-cyan-500 to-pink-500 text-white text-sm font-medium rounded-full hover:opacity-90 transition-opacity"
onClick={(e) => e.stopPropagation()}
>
Download Video
</a>
</div>
)}
{/* Heart Animation Particles */}
{hearts.map(heart => (
<div