当前位置: 首页 > news >正文

Python视频压缩实战:从码率计算到自动化批量处理

前言

作为开发者,日常少不了和视频文件打交道——录了个 Bug 复现视频 200MB,想发给同事结果企业微信限制 50MB;游戏录屏 1GB,传 GitHub Release 要等到地老天荒。手动打开 FFmpeg 敲命令能解决,但每次都算码率、调 crf、试参数实在太低效了。最近我封装了一套 Python 脚本,把视频压缩流程自动化了,同时也发现了一些实用的在线工具。这篇文章把经验分享出来。

基础概念:视频文件大小到底由什么决定

在动手写代码之前,先搞清楚几个核心概念。

一个视频文件的大小主要由三个参数决定:

文件大小 ≈ 码率(bitrate) × 时长 + 音频大小

其中视频码率是压缩时最需要关注的变量,它又受以下因素影响:

参数作用典型值
分辨率画面像素尺寸1080p (1920×1080)、720p (1280×720)
帧率每秒画面数量30fps、60fps
编码格式压缩算法H.264 (兼容性最好)、H.265 (压缩率更高)、AV1 (最新)
CRF恒定质量因子,越低画质越好18(接近无损)、23(默认)、28(可接受)

码率反推公式

当我们需要「压缩到指定大小」时,核心是把目标文件大小反推为目标码率:

python

# 目标码率计算(kbps) # 目标大小单位 MB,时长单位秒 target_bitrate = (target_size_mb * 8192) / duration_seconds - audio_bitrate

举个例子:一段 5 分钟的视频要压缩到 8MB(Discord 限制),音频码率取 128kbps:

python

target_size_mb = 8 duration_seconds = 5 * 60 # 300秒 audio_bitrate = 128 target_bitrate = (8 * 8192) / 300 - 128 # 结果 ≈ 90 kbps print(f"目标视频码率: {target_bitrate:.0f} kbps")

这就是每次手动敲 FFmpeg 命令时心里要做的那道数学题。接下来把它自动化。

环境准备

  • Python 3.8+
  • 系统需安装 FFmpeg 并在 PATH 中(https://ffmpeg.org/download.html)
  • 依赖库:Python 自带 subprocess,无需额外安装

验证安装:

bash

ffmpeg -version # 应输出版本信息 python -c "import subprocess; print('OK')"

实现步骤

Step 1: 用 ffprobe 提取视频信息

压缩的第一步是获取视频的元数据——分辨率、码率、时长、编码格式。可以用 FFmpeg 自带的 ffprobe 提取:

python

import subprocess import json from pathlib import Path def get_video_info(video_path: str) -> dict: """提取视频元数据 Args: video_path: 视频文件路径 Returns: 包含分辨率、码率、时长等信息的字典 """ cmd = [ "ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", str(video_path), ] result = subprocess.run(cmd, capture_output=True, text=True, check=True) data = json.loads(result.stdout) # 从 streams 中找视频流 video_stream = None audio_stream = None for stream in data.get("streams", []): if stream["codec_type"] == "video": video_stream = stream elif stream["codec_type"] == "audio": audio_stream = stream if not video_stream: raise ValueError("未找到视频流") info = { "path": str(video_path), "duration": float(data["format"]["duration"]), "file_size_mb": Path(video_path).stat().st_size / (1024 * 1024), "resolution": f"{video_stream['width']}x{video_stream['height']}", "fps": eval(video_stream.get("r_frame_rate", "0/1")), "video_codec": video_stream.get("codec_name", "unknown"), "video_bitrate_kbps": int(video_stream.get("bit_rate", 0)) // 1000 if video_stream.get("bit_rate") else 0, } if audio_stream: info["audio_codec"] = audio_stream.get("codec_name", "unknown") info["audio_bitrate_kbps"] = ( int(audio_stream.get("bit_rate", 0)) // 1000 if audio_stream.get("bit_rate") else 0 ) return info

Step 2: 根据目标大小计算压缩参数

有了视频信息后,下一步是根据目标文件大小,自动计算 FFmpeg 所需的压缩参数:

python

def calculate_compression_params( video_info: dict, target_size_mb: float, audio_bitrate_kbps: int = 64, codec: str = "hevc", ) -> dict: """根据目标文件大小计算 FFmpeg 压缩参数 Args: video_info: get_video_info() 返回的视频信息 target_size_mb: 目标文件大小(MB) audio_bitrate_kbps: 目标音频码率(kbps),默认 64 codec: 视频编码器,'hevc'=H.265, 'h264'=H.264 Returns: 包含码率、编码器等参数的字典 """ # 将目标大小转为 kbits target_size_kbits = target_size_mb * 8192 # 1 MB = 8192 kbits # 分配:音频占固定部分,剩余给视频 audio_kbits = audio_bitrate_kbps * video_info["duration"] video_kbits = target_size_kbits - audio_kbits # 反推视频码率 target_video_bitrate = video_kbits / video_info["duration"] # H.265 比 H.264 约节省 30%-50% 码率 codec_map = { "hevc": { "encoder": "libx265", "crf": 28, # H.265 默认 CRF "efficiency": 1.0, # 基准 }, "h264": { "encoder": "libx264", "crf": 23, # H.264 默认 CRF "efficiency": 0.6, # 同样画质需更多码率 }, } config = codec_map.get(codec, codec_map["hevc"]) # 根据压缩比决定是否降低分辨率 original_bitrate = video_info.get("video_bitrate_kbps", 0) or ( video_info["file_size_mb"] * 8192 / video_info["duration"] ) compression_ratio = original_bitrate / max(target_video_bitrate, 1) # 如果压缩比 > 5,建议降低分辨率 new_resolution = video_info["resolution"] if compression_ratio > 5 and "x" in new_resolution: w, h = map(int, new_resolution.split("x")) if h >= 1080: new_resolution = "1280x720" elif h >= 720: new_resolution = "960x540" return { "target_video_bitrate_kbps": int(target_video_bitrate), "target_audio_bitrate_kbps": audio_bitrate_kbps, "encoder": config["encoder"], "crf": config["crf"], "resolution": new_resolution, "compression_ratio": round(compression_ratio, 1), }

Step 3: 执行压缩

最后一步:拼装 FFmpeg 命令并执行:

python

def compress_video( input_path: str, output_path: str, params: dict, overwrite: bool = True, ) -> bool: """执行视频压缩 Args: input_path: 输入视频路径 output_path: 输出路径 params: calculate_compression_params() 返回的参数字典 overwrite: 是否覆盖已有输出文件 Returns: 压缩是否成功 """ cmd = [ "ffmpeg", "-i", str(input_path), "-c:v", params["encoder"], "-b:v", f"{params['target_video_bitrate_kbps']}k", "-maxrate", f"{params['target_video_bitrate_kbps']}k", "-bufsize", f"{params['target_video_bitrate_kbps'] * 2}k", "-c:a", "aac", "-b:a", f"{params['target_audio_bitrate_kbps']}k", "-preset", "medium", ] # 如果需要降分辨率 if params["resolution"] != get_video_info(input_path)["resolution"]: cmd.extend(["-vf", f"scale={params['resolution']}"]) if overwrite: cmd.append("-y") cmd.append(str(output_path)) result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: print(f"压缩失败: {result.stderr[-200:]}") return False output_size = Path(output_path).stat().st_size / (1024 * 1024) print(f"压缩完成: {output_size:.1f}MB") return True def batch_compress(input_dir: str, output_dir: str, target_size_mb: float = 20): """批量压缩目录下的所有视频""" input_path = Path(input_dir) output_path = Path(output_dir) output_path.mkdir(parents=True, exist_ok=True) video_extensions = {".mp4", ".mov", ".avi", ".mkv", ".webm"} videos = [f for f in input_path.iterdir() if f.suffix.lower() in video_extensions] print(f"找到 {len(videos)} 个视频文件,目标大小: {target_size_mb}MB\n") success = 0 for i, video in enumerate(videos, 1): print(f"[{i}/{len(videos)}] 处理: {video.name}") try: info = get_video_info(str(video)) params = calculate_compression_params(info, target_size_mb) out_file = output_path / f"{video.stem}_compressed.mp4" if compress_video(str(video), str(out_file), params): success += 1 except Exception as e: print(f" 错误: {e}") print(f"\n完成: {success}/{len(videos)} 个文件压缩成功")

完整代码

把以上三步整合到一个脚本中,保存为video_compressor.py

python

#!/usr/bin/env python3 """ 视频批量压缩脚本 用法: python video_compressor.py input.mp4 8 # 压缩单个视频到 8MB python video_compressor.py ./videos/ ./output/ 20 # 批量压缩目录到 20MB """ import sys import subprocess import json from pathlib import Path def get_video_info(video_path: str) -> dict: cmd = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", str(video_path)] result = subprocess.run(cmd, capture_output=True, text=True, check=True) data = json.loads(result.stdout) video_stream = next((s for s in data.get("streams", []) if s["codec_type"] == "video"), None) audio_stream = next((s for s in data.get("streams", []) if s["codec_type"] == "audio"), None) if not video_stream: raise ValueError("未找到视频流") info = { "duration": float(data["format"]["duration"]), "file_size_mb": Path(video_path).stat().st_size / 1048576, "resolution": f"{video_stream['width']}x{video_stream['height']}", "video_bitrate_kbps": int(video_stream.get("bit_rate", 0)) // 1000 if video_stream.get("bit_rate") else 0, } return info def calculate_params(info: dict, target_mb: float, codec: str = "hevc") -> dict: target_kbits = target_mb * 8192 audio_kbps = 64 target_video_bitrate = (target_kbits - audio_kbps * info["duration"] ) / info["duration"] codec_config = { "hevc": {"encoder": "libx265", "crf": 28}, "h264": {"encoder": "libx264", "crf": 23}, } config = codec_config.get(codec, codec_config["hevc"]) return { "encoder": config["encoder"], "bitrate": int(target_video_bitrate), "audio_bitrate": audio_kbps, "resolution": info["resolution"], } def compress(input_path: str, output_path: str, params: dict) -> bool: cmd = ["ffmpeg", "-y", "-i", str(input_path), "-c:v", params["encoder"], "-b:v", f"{params['bitrate']}k", "-c:a", "aac", "-b:a", f"{params['audio_bitrate']}k", str(output_path)] result = subprocess.run(cmd, capture_output=True, text=True) return result.returncode == 0 def main(): if len(sys.argv) < 3: print("用法: python video_compressor.py <输入> <目标MB>") print(" python video_compressor.py video.mp4 8") print(" python video_compressor.py ./input/ ./output/ 20") sys.exit(1) target_mb = float(sys.argv[-1]) source = Path(sys.argv[-3] if len(sys.argv) == 4 else sys.argv[-2]) if source.is_file(): info = get_video_info(str(source)) params = calculate_params(info, target_mb) out = source.parent / f"{source.stem}_compressed.mp4" if compress(str(source), str(out), params): output_size = out.stat().st_size / 1048576 print(f"完成: {source.name} → {output_size:.1f}MB (目标 {target_mb}MB)") elif source.is_dir(): output_dir = Path(sys.argv[-2]) if len(sys.argv) == 4 else Path("compressed") output_dir.mkdir(exist_ok=True) exts = {".mp4", ".mov", ".avi", ".mkv", ".webm"} videos = [f for f in source.iterdir() if f.suffix.lower() in exts] for v in videos: info = get_video_info(str(v)) params = calculate_params(info, target_mb) out = output_dir / f"{v.stem}_compressed.mp4" if compress(str(v), str(out), params): print(f"完成: {v.name}") if __name__ == "__main__": main()

运行效果

单文件压缩

bash

python video_compressor.py Screen_Recording_2024.mp4 8 # 输出: # 完成: Screen_Recording_2024.mp4 → 7.8MB (目标 8MB)

原始视频 327MB(1080p 30fps H.264),压缩到 7.8MB,画质虽然有轻微下降,但用于发给同事看 Bug 复现步骤完全够用。

批量压缩

bash

python video_compressor.py ./raw_videos/ ./compressed/ 20 # 输出: # 完成: Gameplay_Footage.mp4 # 完成: Meeting_Recording.mov # 完成: Demo_Walkthrough.avi

一键处理整个目录,每个文件都按目标大小自动计算最优码率。

什么时候用脚本,什么时候用在线工具

上面的自动化方案适合批量处理需要精确控制编码参数的场景。但还有一种常见情况——偶尔处理一两个视频,懒得打开终端敲命令。这种时候用浏览器端工具更省事。

VideoCompress 就是这类工具的代表:打开网页 → 拖入视频 → 输入目标大小(比如 8MB)→ 下载。不需要算码率、不需要装 FFmpeg。它本质上是把「码率反推 + 编码压缩」这个流程完全黑盒化了。

几个关键参数(实测所得):

维度数据
输入格式40+ 种(MP4 / MOV / AVI / MKV / WebM 等)
单文件限制最大 1GB
免费额度每月 30 积分
水印
注册要求无需强制注册
隐私上传加密,24 小时自动删除
画质有损压缩,目标「肉眼无差别」(visually lossless)

一句话总结选型逻辑

  • 批量处理、精确控制参数 → Python 脚本 + FFmpeg
  • 偶尔快速压缩一两个视频、不想折腾 → VideoCompress 直接浏览器搞定
  • 专业影视级编码、逐帧调色 → HandBrake / FFmpeg 命令行
http://www.cnnetsun.cn/news/3863970.html

相关文章:

  • 深度解析重庆商城网站建设:从底层架构到运营增长的完整指南
  • 2026上海橡塑展怎么挑选展台设计搭建公司?认准高品质搭建服务商
  • 基于LLM Agent的智能告警排查:从人肉运维到自动化根因定位
  • 告别面子工程,做有温度的政务服务:2024年电子政务网站建设的深度思考与落地指南
  • 迪康U盘:企业数据安全的“电子警察”
  • 从零做一个自己的 CLI
  • 微信小游戏性能优化实战:从启动加速到内存管理的全链路指南
  • C++期末复习与实战指南:从核心概念到高频考点解析
  • 深入解析商城网站建设哪家好:避坑指南与核心选型逻辑
  • Ozone变量波形显示:基于J-Link的嵌入式实时数据可视化调试指南
  • 2024年新乡企业如何选择靠谱的网站建设公司?揭秘避坑指南与实战策略
  • HTML5文档结构与CSS布局核心:从盒模型到响应式设计实战
  • PowerShell Universal Dashboard:无需前端技能,快速构建Web运维监控面板
  • LabVIEW面向对象编程:从数据流到类与对象的工程实践
  • AI漫剧二次元少女三视图提示词分享!
  • 深入理解Linux Locale环境变量:LANG、LC_CTYPE、LC_ALL配置与实战
  • 深入解析Dubbo缓存机制:从元数据管理到高性能调用的设计精髓
  • 2024年电商网站建设公司怎么选才能避坑?资深运营揭秘高质量获客背后的真相
  • Jmeter实现AES256加密参数测试的完整方案
  • 移动端Unity HUD性能优化实战:从Canvas到粒子特效的7个核心策略
  • 为什么越来越多的中山企业选择骏域进行高质量的网站建设以提升品牌竞争力?
  • 链式前向星:图论算法中的高效稀疏图存储方案
  • 东南亚物流PDA签收终端联网解决方案:多国通用免调试物联网卡
  • 大连金豆网站建设如何帮中小企业实现数字化逆袭并低成本获客
  • [AG-UI详解-08]AG-UI客户端工具 V.S. LangChain的Headless工具
  • 多应用场景平台架构实战:中台理念下的统一后端服务设计
  • Hi3519DV500嵌入式Wi-Fi驱动开发:内核配置、设备树与调试实战
  • 建站小白必看网站建设需要哪些软件全方位指南助你少走弯路
  • 交通控制基础理论:从交通流模型到信号配时优化实践
  • SpringBoot+Vue构建校园二手交易平台的技术实践