我又做了一个视频压缩站,这次反而要上传文件

前一个叫 videocompress.dev,全程在浏览器里跑,一个字节都不上传。
这个叫 vidsmaller.com,正好相反:文件得先传上去。

写这篇一半是解释为什么要走回头路,一半是记一笔我算了两天才算平的账。

浏览器端有一条画不过去的线

纯客户端那条路我挺满意,但确实有一类文件它做不了:

  • 46 分钟、702 MB 的录屏。ffmpeg.wasm 单线程要跑一个多小时;WebCodecs 快得多,
    但整条解码—编码管线都在标签页里,内存曲线不好看。
  • iPhone 上的 Safari。WebCodecs 在移动端的支持跟桌面不是一回事。
  • H.265。能解不等于能编。

而想让 ffmpeg.wasm 多线程就得要 SharedArrayBuffer,就得跨源隔离,就得发 COOP / COEP。
COEP: require-corp 会打断没有主动 opt-in 的第三方嵌入——对一个靠广告的站,
这不是技术权衡,是收入决策。上一篇写过,这条我没换过立场。

所以开了第二条路:编码交给服务器。代价是文件要上传,换回来的是大小和时长基本不设上限,
手机上也能用。两个站做的是同一件事,取舍点不同,我就都留着。

它是怎么跑的

浏览器 → 我自己的 R2 桶 → 上游编码器 → 另一个 R2 桶 → 下载。

中间那一跳(先进 R2)是后加的。同一个 593 MB 的文件实测:

路径 上游拿到文件用了多久
浏览器直传给上游 127.8s
浏览器 → R2,上游从边缘拉 18.3s(32 MB/s)

编码早开始一分半钟,而 R2 的 egress 免费,所以这一跳不花钱。
顺带还去掉一个旧兜底流程:以前是「建任务 → 直传失败 → 再建一个任务」,
白烧一遍上游的 operations 配额。

一个 46 分钟的文件,我收了用户 5 个 credit,上游收了我 10 分钟

上游按墙上时间计费:编码任务跑了多少秒,向上取整到分钟。
所以我得在任务开始之前估出「这个文件大概要编多久」,才能标价。

这条曲线后来被我搬到了前台,就是那个
压缩体积计算器
传文件之前先算一下这个视频能压到多小、过不过得去平台的阀。

拟合过一条曲线,1080p / libx264 / medium:

compress_seconds ≈ 6.1 + 0.1191 × source_seconds

看着挺像回事。上线之后一个 702 MB / 46 分钟的真实任务把它打穿了:
按模型扣了 5 个 credit,上游那边实际计了 10 分钟。

第一反应是调参。然后我拿同一个文件、同一组设置,连跑了四遍:

compress: 16.5s / 19.4s / 27.3s / 44.3s

参数一个字没改,2.7 倍的离散。上游会把任务分到快慢明显不同的机器上,
这不是调系数能修的东西。

更麻烦的是这个误差是单边的:落到慢机器上那次我全赔,
落到快机器上那次用户也不会退钱给我。一边是亏,另一边不是赚,是零。

顺手还打掉了两条我以为对的:

  • 曲线是拿 testsrc2 合成素材拟的,真实内容一律更贵,中位数 1.33 倍。
    合成图案好编,真实画面不好编。

  • 我按码率反推分辨率系数,把低码率 720p 判成 1080p 的 0.44 倍。实测是 0.92

    (61.68 − 6.1) / (0.1191 × 507.6) = 0.92
    

    低估两倍,而且恰好低估在用户最常上传的那一类文件上(已经压过一遍的 720p 录屏)。
    上游的流水线不是纯编码瓶颈,按码率猜分辨率这条路本身就是错的。

修法是不再试图估准

改成「预扣 + 结算」:

  • 建任务时按保守值预扣,压在离散区间的上沿;
  • 任务进终态时,拿上游自己报的每个 task 的秒数重算一遍,多退少补;
  • 补扣以余额为上限,而且永远不因此让任务失败——文件已经编完了,
    上游那一分钟已经花掉了,这时候再把用户卡在那儿一点意义都没有。

结算挂在 provider_billed_minutes 由 null 变成数字的那一次 UPDATE 上,
谁抢到行谁结算,所以轮询和 webhook 并发也只会结一次。

拿一天里 12 个真实任务回放:预扣合计 99 分钟,实际计费 77 分钟,
结算之后用户付的正好是 77——其中 10 个预扣够用(退差额),2 个不够(补扣)。

同一件事的另一面:进度条

上游在整个编码过程里只给一个 status: "processing"
没有百分比、没有帧数、没有已写字节。所以进度条不可能是真的。

旧算法按三个阶段等权重分配,结果是整段编码显示成一个不动的 50%——
90 分钟的片子就是十分钟纹丝不动的 50%。

现在按时钟插值:服务端下发当前阶段和它的 startedAt,加上用上面那条曲线预测的时长;
浏览器每秒用同一条曲线重算,所以两次轮询之间进度条也在动,ETA 在倒数。
曲线在预测时间点走到该阶段的 88%,之后渐近爬升但永远到不了 100%——
超时的任务会变慢,不会撒谎说已经完成;提前完成就直接跳到 100%。

顺手扒了竞品的前端 bundle,他们也是纯前端造的曲线:

SOFT_CAP = 97, ASYMPTOTE = 82, CRAWL_RATE = 0.004, MAX_JITTER = 0.8
tau = min(90, 15 + 复杂度 × 0.5)

同一类解法,还加了随机抖动装活。区别只是我的时间常数来自实测,并且报 ETA。

顺便说个白捡的

既然按墙上时间计费,那省时间就是省钱。同一个 720p / 507.6s 的文件跑三个编码 preset:

speed 编码耗时 输出字节 本地 VMAF
medium 61.68s 25,703,118 96.06
fast 43.23s 25,702,451 96.05
faster 27.33s 25,701,649 95.96

三个输出体积差 0.006%。按百分比压缩时,preset 换来的是墙上时间,不是体积;
代价只有画质,而 0.1 个 VMAF 点远在可见阈值以下(JND 大约 1–2 分)。

默认就从 medium 换成了 faster:同样的文件大小,编码时间减半,上游账单也减半。
medium / slow 还留在高级选项里。

最后

vidsmaller.com 不注册就能压,账现在跟上游的表对得上了。
被问得最多的两个场景各有单独的页面:压到 Discord 能发的大小
压到 10MB
不想传文件先估一下的,用计算器

有压不动的文件欢迎丢给我,我最想收的还是那种「看起来成功了、结果不对」的样本。

posted @ 2026-09-20 18:56  mengyuxuan  阅读(21)  评论(0)    收藏  举报