Reduce media bandwidth: compress uploads, add long-lived cache headers, gzip API

- Resize/re-encode every uploaded image server-side (posts, avatars, stories,
  shop products/logo, chat messages) via a shared sharp-based optimizer —
  caps dimensions to what's actually displayed and re-compresses in place,
  cutting a typical raw camera photo by ~90% with no visible quality loss
- Cap uploaded video resolution/bitrate (re-encode only when above 1280px/
  ~2.5Mbps) alongside the existing faststart step; apply faststart+cap to
  story and chat videos too (previously stored completely raw)
- Add long-lived Cache-Control to all /storage static routes so clients stop
  re-downloading unchanged media on every request
- Add gzip/brotli compression for JSON API responses (feed/explore/chat)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
payacom
2026-09-15 17:55:09 +03:30
parent 26979057d7
commit e1abb40cc6
14 changed files with 316 additions and 27 deletions

View File

@@ -16,26 +16,73 @@ async function runFfmpeg(command) {
}
}
/** faststart بدون re-encode — شروع پخش سریع‌تر */
// موبایل/وب هیچ‌جا بیشتر از این نمایش داده نمی‌شه — ویدیوی خامِ دوربین (اغلب ۱۰۸۰p+ با
// بیت‌ریتِ چند برابرِ لازم) رو بدون افتِ کیفیتِ محسوس به این سقف می‌رسونیم
const VIDEO_MAX_DIMENSION = 1280;
const VIDEO_MAX_BITRATE_KBPS = 2500;
async function probeVideo(filePath) {
try {
const { stdout } = await execPromise(
`ffprobe -v error -select_streams v:0 -show_entries stream=width,height,bit_rate -show_entries format=bit_rate -of json "${filePath}"`
);
const data = JSON.parse(stdout);
const stream = data.streams?.[0] || {};
const width = Number(stream.width) || 0;
const height = Number(stream.height) || 0;
const bitRateBps = Number(stream.bit_rate) || Number(data.format?.bit_rate) || 0;
return { width, height, bitRateKbps: bitRateBps ? Math.round(bitRateBps / 1000) : 0 };
} catch (error) {
console.error("[postVideoProcessing] probe failed:", error.message);
return null;
}
}
/**
* فایل رو یا فقط faststart می‌کنه (بدون کیفیت‌کاهی، وقتی از قبل کوچیک/بهینه‌ست) یا
* وقتی رزولوشن/بیت‌ریت بیشتر از چیزیه که کاربر واقعاً می‌بینه، دوباره با سقفِ
* رزولوشن+بیت‌ریت انکود می‌کنه (که faststart هم توش هست، جداگانه لازم نیست)
*/
async function optimizeVideoFaststart(filePath) {
if (!(await fs.pathExists(filePath))) return;
const dir = path.dirname(filePath);
const base = path.basename(filePath);
const tempPath = path.join(dir, `faststart-${Date.now()}-${base}`);
const info = await probeVideo(filePath);
const ok = await runFfmpeg(
`ffmpeg -i "${filePath}" -c copy -movflags +faststart "${tempPath}" -y`
);
const needsReencode =
info && ((info.width > VIDEO_MAX_DIMENSION || info.height > VIDEO_MAX_DIMENSION) ||
(info.bitRateKbps > 0 && info.bitRateKbps > VIDEO_MAX_BITRATE_KBPS * 1.3));
const tempPath = path.join(dir, `optimize-${Date.now()}-${base}`);
const ok = needsReencode
? await runFfmpeg(
`ffmpeg -i "${filePath}" -vf "scale='min(${VIDEO_MAX_DIMENSION},iw)':'min(${VIDEO_MAX_DIMENSION},ih)':force_original_aspect_ratio=decrease" -c:v libx264 -preset veryfast -crf 25 -maxrate ${VIDEO_MAX_BITRATE_KBPS}k -bufsize ${VIDEO_MAX_BITRATE_KBPS * 2}k -c:a aac -b:a 128k -movflags +faststart "${tempPath}" -y`
)
: await runFfmpeg(
`ffmpeg -i "${filePath}" -c copy -movflags +faststart "${tempPath}" -y`
);
if (!ok || !(await fs.pathExists(tempPath))) return;
try {
const [originalStat, optimizedStat] = await Promise.all([
fs.stat(filePath),
fs.stat(tempPath),
]);
// اگه انکودِ جدید بزرگ‌تر شد (ویدیوی خیلی کوتاه/از قبل فشرده)، فایلِ اصلی نگه داشته می‌شه
if (needsReencode && optimizedStat.size >= originalStat.size) {
await fs.remove(tempPath).catch(() => {});
return;
}
await fs.rename(tempPath, filePath);
console.log(`[postVideoProcessing] faststart applied: ${base}`);
console.log(
`[postVideoProcessing] ${needsReencode ? "re-encoded" : "faststart applied"}: ${base} (${originalStat.size} -> ${optimizedStat.size} bytes)`
);
} catch (error) {
await fs.remove(tempPath).catch(() => {});
console.error("[postVideoProcessing] faststart rename failed:", error.message);
console.error("[postVideoProcessing] rename failed:", error.message);
}
}