☰
安卓视频压缩实战:码率计算、MediaCodec硬编与音画同步
2026/11/4 5:07:27 网站建设 项目流程

手机弹出“存储空间不足”,点进去一看,视频占了 42GB——这两年我遇到太多次了。安卓用户的存储焦虑,八成不是照片造成的,而是视频。一段 4K 60 帧的随手拍,一分钟就能吃掉 400MB;游戏录屏、会议回放、孩子的成长记录,几个月下来轻松破百 G。视频压缩这件事说起来简单,就是“把文件变小”,但真正动手做过的人都知道,里面全是取舍:码率给低了画面糊成一团,给高了等于没压;分辨率降了字幕看不清,不降又省不了空间;音频砍狠了人声发闷。这篇我把折腾 VideoCompressor 这类安卓端压缩工具的完整过程写下来,从体积到底由什么决定、四条实现路径怎么选、参数怎么配、代码怎么写、坑怎么填,一直到怎么验收效果。适合两类人看:想给自己的应用加一个压缩能力的开发者,以及单纯想把手里的视频压小一点的安卓用户。看完你至少能做到两件事:自己算出该给多少码率,以及判断那些“一键瘦身”到底有没有偷偷牺牲你的画质。

1. 先从体积这件事算明白

1.1 存储告警的真实结构

很多人以为手机存储是被照片吃掉的,实际拆开看完全不是这么回事。我用文件管理器按类型统计过自己那台用了两年的中端机:照片 8.6GB,应用数据 22GB,视频 41GB。照片单张 3 到 5MB,堆到 8GB 需要两千多张;而视频一分钟就是几百兆,几十个文件轻松超过照片总量。原因很直接,照片是单帧压缩,视频是每秒 30 帧连续压缩,还得加上音轨,体积自然不在一个量级。

更麻烦的是,安卓上产生视频的入口太多了:系统相机、微信保存、抖音缓存、录屏、通话录音转视频、无人机导出、行车记录仪。这些来源的编码参数五花八门,有的用 H.264 高码率,有的用 HEVC,有的甚至是 MJPEG 这种古老格式,体积能大到离谱。也就是说,用户手机里的视频并不是均质的,真正值得压缩的往往是少数几个“体积异常”的大文件。搞清楚这一点很重要,它决定了压缩工具应该做成“批量全部处理”还是“按体积排序挑大的处理”,我后来选了后者,理由是用户的时间和电量都有限。

1.2 视频体积的计算公式

所有压缩参数的选择,底层都绕不开这一个公式:

文件大小(字节) = (视频码率 + 音频码率) × 时长(秒) / 8

注意单位:码率通常写作 bps(比特每秒),转换成字节要除以 8。以 Mbps 为单位的码率,算出来的结果是 MB。举个真实例子,一段 10 分钟的 1080p 视频,文件 1.2GB:

1200 MB × 8 = 9600 Mb 9600 Mb / 600 s = 16 Mbps

也就是说这段视频的总码率是 16Mbps,其中视频大概 15.7Mbps,音频 128kbps。这个数字偏高,正常 1080p30 的 H.264 内容 5Mbps 左右就足够好了,15Mbps 属于手机默认给了很大的余量。这就解释了为什么手机拍的视频特别“肥”:厂商为了保证任何场景都不糊,默认码率往往开得远超必要值。

反过来用这个公式做规划就更清楚了。想把 1.2GB 压到 180MB 左右,时长不变还是 600 秒:

180 MB × 8 = 1440 Mb 1440 Mb / 600 s = 2.4 Mbps

2.4Mbps 总码率,扣掉 128kbps 音频,视频码率给 2.27Mbps。这个值对 1080p 来说是“能看但经不起细看”的水平,对 720p 则相当宽裕。所以先算再调,比凭感觉拖进度条靠谱得多。

目标体积时长 1 分钟时长 5 分钟时长 10 分钟时长 30 分钟
20MB2.67 Mbps0.53 Mbps0.27 Mbps0.09 Mbps
50MB6.67 Mbps1.33 Mbps0.67 Mbps0.22 Mbps
100MB13.3 Mbps2.67 Mbps1.33 Mbps0.44 Mbps
300MB40 Mbps8 Mbps4 Mbps1.33 Mbps

这张表可以直接当速查用。它暴露了一个残酷事实:短视频压起来很轻松,长视频想压到小体积就必须牺牲分辨率,因为码率是有物理下限的。一条 30 分钟的视频想塞进 50MB,平均码率只有 0.22Mbps,那是连 480p 都撑不住的水平。这时候正确的做法不是硬压,而是提示用户“要么接受更低分辨率,要么别压”。

1.3 压缩不是单纯变小,是三条曲线的交汇

真正做过才知道,压缩从来不是单变量问题。你手上其实握着四根旋钮:分辨率、帧率、码率、编码器效率,每调一根都会牵动画质、体积、耗时三条曲线。

分辨率是第一优先级。人眼对空间细节的敏感度远高于时间细节,把 4K 降到 1080p,体积直接砍到四分之一,观感上的损失在手机屏幕上几乎察觉不到;反过来把帧率从 60 降到 30,体积只省一部分,但运动画面会有明显顿感。所以我的默认策略永远是“先降分辨率,再考虑降帧率”。

编码器效率是“免费的午餐”。同样的画质,HEVC(H.265)比 H.264 省 30% 到 50% 的码率,代价是编码耗时翻倍、老设备可能解不动。我的判断标准很简单:目标设备是近五年的安卓机,就用 HEVC;要发给别人或者上传到不确定的平台,老老实实用 H.264,兼容性值那点体积。

还有一条容易被忽略的曲线是内容复杂度。同一个码率,拍静止的墙面和拍满屏飘落的树叶,效果天差地别。这就是为什么固定码率编码器(CBR)在复杂场景下会糊,恒定质量模式(CRF)会自适应地给复杂帧更多比特。理解了这一点,你就明白为什么有些工具压缩后“有的片段很清晰,有的糊成一片”——它用的是固定码率,简单但粗暴。

2. 四条实现路径,我为什么选这条

2.1 几种方案的横向对比

安卓上做视频压缩,能走的路其实就那么几条,我把它们摆在一起比过一轮:

方案速度功耗发热画质可控性兼容性包体积影响
系统 MediaCodec 硬编极快,接近实时低中等,参数受限依赖芯片,有坑几乎为零
软件编码(FFmpeg 集成)慢,0.2 到 0.5 倍速高,明显发热高,参数全开放极好增加 5 到 20MB
云端转码取决于网络无高依赖网络小
直接换封装(不重编码)极快无无(码率不变)好零

先说最后一个,很多人不知道还有这条路。视频文件其实是“容器 + 轨道”,MP4 只是个盒子,把盒子换成更省空间的格式或者去掉多余的元数据,能省一点,但码率没变,省得非常有限,通常只有 3% 到 8%。这招适合“实在懒得重编码但又想榨一点空间”的场景,不能当主力。

云端转码速度和质量都不错,但它绕不开一个根本问题:用户的私人视频要上传到别人的服务器。这个取舍不是技术问题而是信任问题,我个人的项目里直接排除了这条路径,本地能算完的事情不要在本地算完。

2.2 硬编为主、软编兜底的组合拳

最终的架构是这样的:默认走系统 MediaCodec 硬件编码,速度快、省电、包体积零增加;遇到硬编不支持的格式(比如某些老机器的 HEVC 编码器缺失)、或者用户明确要求“最高画质”,再降级到软件编码。

这个选择背后的逻辑值得说清楚。硬件编码器的本质是芯片里的一块专用电路,它只干编码这一件事,所以快且省电。但它也“笨”:参数调节粒度粗,很多高级选项(比如自定义量化矩阵、精细的码率控制)根本不开放;不同厂商的实现差异巨大,同一份配置在 A 机型上跑得好好的,B 机型可能直接抛异常。

我做过一次粗略的耗时对比:一段 5 分钟 1080p 视频,硬编大约 40 秒完成,机身温热;软编用中速预设跑了将近 4 分钟,机身明显发烫,电量掉了 6%。对普通用户来说,为了那一点点画质提升等四分钟是不划算的,所以默认值必须是硬编。但软编不能砍掉,它是兜底:用户愿意等,就给他更好的结果。

2.3 硬编码的兼容性雷区

这块是真正会让人栽跟头的地方,我列几个实测遇到的:

分辨率的对齐要求。不少编码器要求宽高是 16 的整数倍,你传 1080×1920 没问题,但传 1078×1918 就可能直接报错。解决办法是向下取整到 16 的倍数再编码,编码完成后如果需要保持原始宽高比,再在播放端做微调,或者干脆把画面缩放到标准尺寸。

Profile 和 Level 支持不全。同样是 H.264,有些芯片只支持 Baseline 和 Main,你配 High Profile 就失败。做法是先查MediaCodecInfo.CodecCapabilities里支持的 profileLevels,挑一个交集,别硬写死。

码率上限。有的编码器对单路码率设了硬上限(尤其是 4K 场景),你给 80Mbps 它默默截断到 40Mbps,这种“静默失败”最难查,只能压完看实际码率对不对。

还有一点,编码器实例数量有限。有些机型同时只能开一个硬件编码器,如果你的应用里还在后台录屏或者播放,压缩任务就会拿不到资源。这一点在应用里必须做优雅降级和排队。

3. 参数怎么配,给一份能直接抄的表

3.1 分辨率、帧率、码率的联动

这三个参数不是独立的,它们共同决定“每帧能分到多少比特”。业内有个经验值:H.264 编码下,每像素每帧大约给 0.05 到 0.1 比特就能得到不错的观感。按这个算 1920×1080×30fps:

1920 × 1080 × 30 × 0.07 ≈ 4.35 Mbps

和我前面说的“1080p30 给 5Mbps 就够好”基本吻合。同样的公式套 1280×720×30:

1280 × 720 × 30 × 0.07 ≈ 1.9 Mbps

套 640×360×30:

640 × 360 × 30 × 0.07 ≈ 0.48 Mbps

有了这个系数,你可以从任意分辨率帧率推出一个合理的起始码率,再根据实际效果上下浮动 30%。HEVC 的话把系数乘 0.6 左右。

帧率的选择我一般按内容类型来:普通拍摄用 30 够了,60 帧的意义主要在高速运动和游戏录屏;文档、屏幕录制、幻灯片这类内容,15 到 20 帧就完全够用,压出来体积能再省三分之一;而如果是慢动作视频,降帧率会毁掉整个效果,宁可不压。

3.2 CRF 与固定码率,两套思路分别适合谁

恒定质量模式(CRF)的思路是“我要这个画质,你看着给码率”,复杂场景自动多给比特,简单场景自动少给,最终体积是不可预测的。固定码率(或目标码率 + 峰值上限)的思路是“我就要这个体积”,画面质量随内容浮动。

我的用法是:用户显式输入“压到 200MB”时用目标码率模式,因为体积是可承诺的;用户选择“画质优先/均衡/极限压缩”时用 CRF,因为质量是可承诺的。这两个承诺不能同时给,非要说“又小又清晰”那就是骗人。

H.264 的 CRF 取值参考:18 到 20 视觉上基本无损,普通人不做对比看不出;23 是默认值,画质和体积平衡点;26 到 28 开始能看出细节丢失,但手机屏上看还行;30 以上明显发糊,只适合“能看清内容就行”的场合。还有个关键规律要记住:CRF 每加 6,码率大约减半。这个规律在做参数预设时特别好用,你想让体积减半,直接把 CRF 加 6 试试。

3.3 四套场景配方

这些是我反复调试后固定下来的预设,可以直接拿去用:

场景分辨率帧率视频码率/CRF音频编码器预估压缩比
聊天软件发送720p30CRF 28AAC 64kbps 单声道H.2648 到 15 倍
本地归档保存1080p30CRF 23AAC 128kbpsH.2653 到 6 倍
屏幕录制/文档720p(保原宽高比)15CRF 30AAC 64kbpsH.26410 到 20 倍
游戏录屏分享1080p60CRF 25AAC 128kbpsH.2654 到 8 倍

“聊天软件发送”那一档要特别说明:绝大多数即时通讯软件收到视频后还会再压一次,你压得太狠就是二次伤害,压得太轻又传不动。720p + CRF 28 是个甜点,传过去对方看着清楚,你自己这边体积也降下来了。另外这一档我建议加faststart(把索引挪到文件头),否则对方点开要等半天才开始播。

3.4 一个完整的取舍手算

拿一段真实素材走一遍,你就明白参数怎么定了。假设有一段 4K 60fps、时长 3 分钟、体积 1.5GB 的旅行视频,目标是压到 150MB 以内并且要发朋友圈。

先算原码率:1500 × 8 / 180 = 66.7 Mbps。确实很肥。

目标码率:150 × 8 / 180 = 6.67 Mbps,减去 128kbps 音频,视频给 6.5Mbps。

现在问自己:6.5Mbps 能撑住 4K 吗?按系数反推,4K30 需要 3840×2160×30×0.07 ≈ 17.4 Mbps,明显不够。1080p30 需要 4.35Mbps,6.5Mbps 有富余。所以答案是:降到 1080p,还能把帧率保留在 30。

最终配置:1080p、30fps、目标码率 6.5Mbps(或 CRF 21 左右),H.265,音频 AAC 128kbps。预计耗时 1 到 2 分钟,画质在手机屏上基本无损。如果换成 H.264,同样画质需要 9Mbps 左右,那就得提到 CRF 24 并接受一定损失。

这套推理过程比背参数表有用得多,因为它能推广到任何素材。

4. 动手实现,把流程跑通

4.1 工程准备与数据结构

核心思路是“解码 → 编码 → 封装”三段式,中间不落地成图片序列。这是最关键的设计决定:把视频拆成 PNG 帧再编码,中间要写几百 GB 的临时文件,速度和存储都受不了。正确做法是让解码器的输出直接送到编码器的输入,全程在内存/显存里流转。

安卓上最省事的连接方式是 Surface 直连:创建编码器的输入 Surface,把它作为解码器的输出目标。数据流在图形子系统里直接传递,连 CPU 拷贝都省了,实测比走 ByteBuffer 中转快 2 到 3 倍。

权限方面,读取视频要READ_MEDIA_VIDEO(新版本)或旧的存储权限,写入到应用私有目录不需要额外权限。压缩这种耗时任务建议放在前台服务里跑,否则用户切到后台系统可能把进程回收,压到一半的视频直接废掉。

4.2 解封装与轨道选择

拿到文件先解封装,把视频轨和音频轨分离出来:

val extractor = MediaExtractor() extractor.setDataSource(inputPath) var videoTrack = -1 var audioTrack = -1 for (i in 0 until extractor.trackCount) { val format = extractor.getTrackFormat(i) val mime = format.getString(MediaFormat.KEY_MIME) ?: continue when { mime.startsWith("video/") && videoTrack < 0 -> videoTrack = i mime.startsWith("audio/") && audioTrack < 0 -> audioTrack = i } }

这里有个细节值得说:不要假设轨道顺序。有些文件音频在前、视频在后,硬编码索引 0 取视频就会拿到音频轨,然后报一堆莫名其妙的错。我第一次踩这个坑花了大半天才定位到,因为异常信息完全指向别处。

读轨道格式的时候顺便把KEY_ROTATION、KEY_WIDTH、KEY_HEIGHT、KEY_DURATION都取出来。旋转角度特别容易丢,后面会专门讲。

4.3 编码循环的核心骨架

这是整个流程的心脏部分:

val outFormat = MediaFormat.createVideoFormat( MediaFormat.MIMETYPE_VIDEO_AVC, targetWidth, targetHeight ).apply { setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface) setInteger(MediaFormat.KEY_BIT_RATE, targetBitrate) setInteger(MediaFormat.KEY_FRAME_RATE, targetFps) setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2) setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR) } val encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC) encoder.configure(outFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) val inputSurface = encoder.createInputSurface() encoder.start() val decoder = MediaCodec.createDecoderByType(srcMime) decoder.configure(srcFormat, inputSurface, null, 0) decoder.start()

KEY_I_FRAME_INTERVAL设成 2 表示每 2 秒一个关键帧(GOP 长度 = 帧率 × 2)。这个值的取舍是:GOP 短,随机拖动定位快、抗丢包强,但体积大;GOP 长,体积小,但拖动和剪辑时定位精度差。存档用 2 到 5 秒,网络传输用 1 到 2 秒,够了。

主循环的逻辑是:给解码器喂输入、取解码输出、把输出渲染到 Surface、从编码器取编码输出、写进封装器。四个动作交替进行,用一个循环驱动。这里必须用非阻塞模式(dequeueInputBuffer(0)加超时),别用无限等待的阻塞调用,否则用户在界面上点取消,你得等它跑完才能响应。

进度计算用解码器输出的 presentationTimeUs 除以总时长,比按帧数估算准,因为可变帧率的素材按帧数算会跑偏。

4.4 封装与时间戳对齐

封装器本身不难用,难在时间戳:

val muxer = MediaMuxer(outPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) // 编码器输出的 format changed 事件里拿到真实格式后加轨 val videoIndex = muxer.addTrack(encoderOutputFormat) val audioIndex = muxer.addTrack(audioOutputFormat) muxer.start() // 写视频样本 buffer.position(0) buffer.size = info.size muxer.writeSampleData(videoIndex, buffer, info)

三个坑必须提前处理。

第一,时间戳要归零。原始视频的第一帧时间戳不一定是 0,可能是几千微秒甚至几十万微秒,直接写进去会导致文件开头有一段“空白”。做法是记下第一个样本的 pts,之后每个都减掉它。

第二,编码器输出的BUFFER_FLAG_CODEC_CONFIG帧(就是 SPS/PPS 参数集)不要写进封装器。MediaMuxer在addTrack时已经通过 format 拿到这些信息了,你再写一遍会导致部分播放器解析出错。判断方式是检查 flag 位。

第三,音视频两条轨的时间戳必须独立归零、独立计算,不能混用一个基准。如果音频从 0 开始、视频从 0.03 秒开始,播放器会以为视频晚开始,结果就是声音快半拍。

4.5 命令行兜底方案

用户选了“最高画质”或者硬编失败时,走软件编码。如果集成了命令行工具,参数模板可以直接用:

ffmpeg -i input.mp4 \ -c:v libx264 -preset medium -crf 23 \ -vf "scale=-2:1080,fps=30" \ -c:a aac -b:a 128k -ac 2 \ -movflags +faststart \ -y output.mp4

几个参数解释一下:scale=-2:1080里的 -2 表示宽度自动按比例算并保持偶数(编码器要求偶数);-movflags +faststart把 moov 索引移到文件头部,边下边播靠它;-preset从 ultrafast 到 veryslow 一共九档,手机上建议用 medium 到 slow,再慢就真的等不起了。

如果想让命令行也走硬件加速(速度会快很多但兼容性差一些):

ffmpeg -i input.mp4 -c:v h264_mediacodec -b:v 4M \ -c:a copy -y output.mp4

注意-c:a copy是“音频直接复制不重编码”,能省一截时间,而且音质不会二次损失。前提是音频格式目标容器能接受,AAC 进 MP4 是可以的,别的格式要小心。

4.6 进度回调与取消机制

压缩是个长任务,用户必须能看到进度、必须能取消。进度用回调往外抛,别直接更新界面,用消息机制或者协程切回来。取消的实现要点是设置一个 volatile 标志位,主循环每轮检查一次,为真就停止喂数据、发 EOS、清理资源。清理顺序有讲究:先stop()再release(),先停编码器再停解码器,先关 muxer 再删临时文件。顺序错了轻则抛异常,重则留下一个零字节的坏文件被用户看到。

我还会在写文件时先写到一个.tmp后缀的临时文件,全部成功后再重命名。这样即使中途崩了,用户目录里也不会出现一批“看起来正常但打不开”的视频。

5. 踩过的坑,值得一条条记下来

5.1 音画不同步的三个成因

这是最高频的问题,我遇到过的原因有三类。

第一类是音频直接复制没有重编码,但原音频的时间戳基准和视频不同,封装后整体偏移。解决办法是要么重编码音频统一基准,要么在写音频样本时手动减去它自己的第一个 pts。

第二类是编码器缓冲导致视频某一帧延迟输出,而音频是实时的,累计下来差了几十毫秒。这种情况肉眼可见“口型对不上”。做法是记录编码器实际输出的第一帧 pts 作为视频基准,而不是用解码器的第一帧 pts。

第三类是可变帧率素材。有些手机录制的是可变帧率(VFR),视频里每帧的实际间隔不固定,你如果按固定帧率重算时间戳,整条时间轴就会漂移。正确做法是原样保留每帧的 pts 相对间隔,只做整体平移。

5.2 旋转角度丢失与色彩异常

手机拍的竖屏视频,实际存储的可能是横向的 1920×1080 加一个 90 度的旋转元数据。你如果只读宽高不读旋转,压出来的视频会“躺着播”。处理方式有两种:把旋转角度透传给输出文件(简单,但部分播放器识别不了),或者在编码时先把画面旋转过来再写入(稳妥,但要重设宽高)。

色彩异常主要出现在硬件编码路径上,表现为画面偏绿、偏紫或者上下颠倒。成因通常是颜色格式不匹配,比如编码器要 NV12,你给了 I420。用 Surface 直连一般不会遇到,走 ByteBuffer 中转就很容易踩。另外有些设备在 4K 分辨率下颜色格式支持和 1080p 不一样,同一个配置在低分辨率下正常,切到高分辨率就出问题。

还有一种情况是 HDR 视频被当成 SDR 压,画面会发灰。这类素材建议直接检测 HDR 元数据然后跳过压缩,或者提示用户“该视频为 HDR,压缩会损失效果”。

5.3 内存、发热与分片策略

大文件处理最怕两件事:OOM 和烫手。一个 4K 视频如果一次性把解码数据全读进内存,几百兆起步,中低端机直接崩。正确做法是流式处理,每次只持有一个缓冲区,输入一点、输出一点。缓冲区大小从编码器的 format 里读KEY_MAX_INPUT_SIZE,别自己拍脑袋设个大数字。

发热方面,连续压 10 分钟以上机身温度能到 40 度以上,系统可能触发降频,速度越来越慢。我的处理是把长视频切成 30 秒一段分别编码,中间插入短暂的让出时间,最后再拼接。听起来麻烦,但实测下来总耗时反而更短,因为避免了持续降频。

后台任务一定要加锁,避免用户连点两次触发两个并行的压缩任务。

5.4 常见问题速查

现象可能原因排查与解决
压缩后文件 0 字节muxer 未 start 或提前 stop检查 addTrack 后是否调用 start,异常分支里保留日志
有画面无声音音频轨未添加或音轨编码失败单独打印音频轨的 addTrack 返回值
播放到一半卡住时间戳非单调递增写样本前校验 pts 必须大于上一条
画面比例变形只改了宽未按比例改高用原宽高比重新计算,强制偶数值
压缩比远低于预期码率被硬件静默截断压完读实际文件码率对比目标值
部分设备直接崩溃profile/level 不支持遍历 codec 支持的 profileLevels 取交集
进度条卡在 99%未处理 EOS 后的收尾输出EOS 后继续循环直到输出 flag 带 EOS
视频开头黑屏一秒首帧时间戳未归零记录首帧 pts 并在写入时全部减去

6. 压完了怎么验收,别只看体积

6.1 客观指标:三个数字就够

压完不看质量就是耍流氓。我一般看三个数。

实际码率。用算出来的文件大小和时长反算,和你设定的目标值对比。偏差超过 20% 就说明码率控制没生效,可能是被硬件截断,也可能是 CRF 模式吃了太多比特。

关键帧间隔。如果设了 2 秒,实际却出现 10 秒一个关键帧,说明I_FRAME_INTERVAL没被采纳,某些编码器只认自己的默认值。这会影响拖动定位体验。

音频参数。确认采样率、声道数、码率符合预期。曾经遇到过音频被强制降成 32kHz 单声道,人声听起来像隔了一层布,查下来是编码器不支持 48kHz 双声道,静默降级了。

至于 PSNR 和 SSIM 这类画质相似度指标,说实话在手机场景里参考价值有限,它们和主观观感的关联度不高,算起来还慢。我一般只在前面对比不同 CRF 值时用一次,做个横向排序,日常不跑。

6.2 主观验收清单

真正决定“能不能用”的还是眼睛。我固定看这几个地方:

人脸和字幕。人脸是最容易被压糊的区域,眼睛和嘴唇的细节丢失最明显;字幕边缘如果有明显的锯齿和振铃,说明码率不够。这两处过关,基本就过关。

暗部噪点。低码率下暗部容易出现块状色斑,尤其是夜景视频。全屏看一遍,如果干净就说明编码器分配比特比较合理。

快速运动段落。找一个镜头快速平移的片段,如果画面出现明显的“拖影”和块状,是码率不够或者 GOP 太长。

声音。听 20 秒人声,有没有变闷、有没有忽大忽小。忽大忽小通常是音频重采样出了问题。

这套验收流程走下来 2 分钟,比什么指标都靠谱。

6.3 批量处理的省事写法

单个文件处理完,接下来就是批量。我的做法是扫描目录、按体积倒序排列、只处理超过阈值的文件、处理完再校验:

# 找出体积超过 200MB 的视频,逐个压缩 find /sdcard/DCIM -type f -name "*.mp4" -size +200M | while read f; do out="${f%.mp4}_min.mp4" ffmpeg -i "$f" -c:v libx265 -crf 24 -preset medium \ -vf "scale=-2:1080" -c:a aac -b:a 128k \ -movflags +faststart -y "$out" # 只有输出确实更小才替换,避免越压越大 if [ -s "$out" ] && [ $(stat -c%s "$out") -lt $(stat -c%s "$f") ]; then mv "$out" "$f" else rm -f "$out" fi done

那个“只有更小才替换”的判断很重要。我遇到过原视频是低码率低分辨率、压完反而更大的情况,因为没有这个判断,用户的文件被“优化”得更占地方了。

批量处理还要考虑排队。同时跑三个压缩任务,三个都变慢,用户体验反而更差。串行跑,一次一个,进度明确,发热也可控。

提示:压缩前一定先确认目标文件是完整的。如果原始视频本身就有损坏,解码器读到一半会报错,这时不要强行输出,直接标记失败并跳过,否则会生成一个时长不对的残缺文件。

最后再分享两个实操里的心得。一是压缩参数别做成“专业模式”让用户自己填,绝大多数人不懂码率是什么,给了也是一顿乱调然后怪你效果差。做成三档预设加一个“目标体积”输入框就够了,前者覆盖 90% 的需求,后者覆盖那些有明确体积要求的场合。二是永远保留原文件,或者至少在一次压缩后提供“恢复原始”的入口。我在自己项目里见过不止一次用户后悔压得太狠,原文件又没了的窘境——那种情况下除了道歉没有任何补救办法。压缩这事本质上是不可逆的信息丢失,工具能做的就是把选择权交清楚,并且给一条退路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询