
1. 需求拆解为什么要给视频做 HLS 加密分片先说个实际场景。我之前接过一个视频站点的改造需求原始视频都是 MP4直接放在服务器上搞了个播放器页面就上线了。结果没到一个月就被别人拿着播放地址批量下载整站视频打包搬走连水印都沒去掉。这种时候你才会意识到裸奔的 MP4 就是放在货架上的商品任何人拿着工具就能搬走。HLS 加密分片就是针对这个问题的标准解法。HLS 本身是苹果提出的流媒体协议核心思路是把一个完整视频切成若干个小片段一般叫 TS 分片再用一个 m3u8 索引文件去描述这些分片的播放顺序。加密则是在切分的基础上对每个分片做 AES-128 加密播放器必须先拿到密钥才能解开分片内容。为什么要强调“切分 加密”一起做而不是只加密这里有个关键点如果只是把整个文件加密后再播放播放器必须下载完整个文件才能解密用户点开视频要等半天这体验基本没法用。切成小分片之后播放器可以边下边播第一个分片下载完就能开始渲染起播时间能拉到 1 到 3 秒左右。加密分片合在一起既保证了内容安全又不牺牲播放体验。ffmpeg 是这个方案里最核心的工具没有之一。它原生支持 HLS 封装和 AES-128 加密一条命令就能完成切片、加密、生成索引文件三个动作。更妙的是它还支持批量处理配合脚本可以一次性把一个目录下的所有视频全部转成加密分片适合中等规模的内容库。我写这篇文章就是想把这些折腾过的东西整理出来。如果你手头有一批视频要做 HLS 加密分片或者正在研究怎么做批量转换这篇文章应该能帮你少走不少弯路。我会从最基本的单条命令讲起再到批量脚本的完整实现最后梳理常见坑点。整个过程不需要你懂太高深的流媒体原理跟着操作就行。2. 方案选型为什么是 ffmpeg 的 HLS 加密分片2.1 加密方案的横向对比市面上给视频做保护的手段不少简单分个类方案实现成本安全性兼容性适用场景直接隐藏文件地址极低几乎为零不限防君子不防小人HLS AES-128 加密低中等所有主流播放器常规视频网站、课程平台HLS 结合自定义鉴权中中高需配合后端会员制视频站商业 DRM如 Widevine较高高有版权费电影、剧集等版权内容从实际项目角度看DRM 虽然安全性最强但要对接版权体系还要付授权费个人开发者和中小团队根本玩不动。HLS AES-128 加密则刚好卡在成本和安全的平衡点上。加密逻辑内置在 ffmpeg 里不需要额外写代码播放端天然支持H5 播放器、原生播放器、第三方播放器都能直接播。2.2 HLS 加密分片的工作流程要理解 ffmpeg 具体做了什么你先得知道 HLS 加密分片涉及几个关键产物密钥文件key file16 字节的随机数AES-128 的密钥。分片文件.ts视频被切成若干小段每段都经过加密。索引文件.m3u8描述分片顺序、时长以及密钥获取地址。整个流程可以类比成寄快递原始视频是你要寄的东西切片相当于把大包裹拆成多个小包裹加密就是给每个小包裹上锁m3u8 文件则是随包裹附带的清单写着每个锁对应的钥匙在哪。ffmpeg 做这件事的核心逻辑是读取原始视频按照指定时长切片每生成一个分片就用密钥加密一次最后把所有分片信息写到 m3u8 文件里。这里有一个细节值得注意ffmpeg 用了hls_key_info_file这个参数来指定密钥配置文件。这个配置文件不是密钥本身而是告诉 ffmpeg 密钥 URI 怎么写、密钥文件在哪、要不要用 IV。2.3 为什么优先选择保留原编码而非重编码很多第一次做加密分片的人最容易犯的错是直接拿 ffmpeg 默认参数去转码结果一个 1GB 的视频被重新编码CPU 跑满就算了输出文件体积还变大画质也受损。实际上对于已经压缩好的 MP4 文件切成 TS 分片时完全可以做到不重新编码。ffmpeg 的-c copy参数就是这个用途。它直接把原始视频流和音频流拷贝到 TS 分片里不做任何重新压缩。这样做有两个好处第一速度快拷贝过程几乎是线性的一个 2 小时的电影大概一分钟左右就能切完第二画质零损失因为根本没经过解码编码流程。不过这里有个前提原始文件本身得是 H.264/H.265 编码的 MP4。如果是其他编码格式比如 MKV 里的某些特殊编码TS 封装可能不支持那就得老老实实重编码了。2.4 分片时长的选择逻辑分片时长是个很值得琢磨的参数它不是随便拍的。-hls_time 10表示每个分片 10 秒左右这个值直接影响到起播速度和分片总数量。我实际测试下来10 秒是一个比较通用的值。分片太短比如 2 秒起播确实快但分片数量会膨胀请求数暴增服务器压力大而且 m3u8 文件会很长。分片太长比如 30 秒起播会明显变慢用户等半天第一帧画面才出来而且拖动进度条时缓冲时间也会变长。从 CDN 分发角度看分片数量直接影响缓存效率。分片太小CDN 回源次数增多分片太大单次回源时间长。综合下来 4 到 10 秒是业界比较认可的范围。如果你做的是短视频建议用 4 秒长视频、电视剧建议 6 到 10 秒这个结论我用不同片源验证过。3. 单条命令实战手写一个 HLS 加密分片命令3.1 环境准备与基础命令在动手之前先确认你的 ffmpeg 版本支持 HLS 加密。一般来说 4.0 以上的版本都支持我建议直接装最新稳定版。Windows 用户可以去官网下载编译好的二进制文件Linux 用户用包管理器安装或者自己编译都行。假设我有一个视频文件demo.mp4要把它加密分片输出命令是这样的openssl rand 16 video.key cat key_info EOF video.key video.key EOF ffmpeg -i demo.mp4 \ -c:v copy \ -c:a copy \ -hls_time 10 \ -hls_key_info_file key_info \ -hls_segment_filename output_%03d.ts \ output.m3u8先用openssl rand 16生成 16 字节随机数当作密钥文件然后写一个key_info配置文件。key_info文件的格式很固定一共三行第一行密钥的 URI也就是 m3u8 里写的密钥获取路径第二行密钥文件的本地路径第三行IV 参数如果留空则自动生成上面这个例子里我用了video.key作为 URI 和本地路径这在实际部署时多半要改成服务器的 URL 地址后面会细说。3.2 参数逐个拆解-hls_time 10上面已经讲过了下面重点说其他几个容易出错的参数。-hls_key_info_file是加密的关键参数它指向 key_info 配置文件。ffmpeg 在切片过程中会不断读取这个文件里的信息给每一个 TS 分片加上 AES-128 加密。这个参数只要指定了ffmpeg 就会启用 HLS 加密不用额外加别的开关。-hls_segment_filename是用来控制分片文件名的。如果不加这个参数ffmpeg 会按output_0.ts、output_1.ts这种格式生成但无法自定义文件名模式。我习惯写成output_%03d.ts这样会生成output_000.ts、output_001.ts排序更清晰。-hls_playlist_type vod这个参数值得加上。它表示生成的是一个点播类型的 m3u8而不是直播类型。两者区别在于直播类型 m3u8 会不断更新而点播类型是固定不变的更适合视频文件的分发场景。还有一个参数叫-hls_list_size默认值是 5表示 m3u8 文件里只保留最近 5 个分片条目这个默认值是为直播设计的。做点播必须把这个值设成 0否则 m3u8 里只会有最后几个分片视频根本播不完。我见过很多新手在这个参数上翻车明明加密分片都生成了播放器却只能播最后十几秒。3.3 补全可用的完整命令把上面的参数整合一下得到一个可以直接用的完整命令openssl rand 16 video.key cat key_info EOF https://your-server.com/hls/video.key video.key EOF ffmpeg -i demo.mp4 \ -c:v copy \ -c:a copy \ -hls_time 10 \ -hls_list_size 0 \ -hls_playlist_type vod \ -hls_key_info_file key_info \ -hls_segment_filename hls/output_%03d.ts \ hls/output.m3u8这里我把密钥 URI 换成了服务器的绝对地址保证播放器在解析 m3u8 时能直接通过 URL 获取到密钥文件。为什么会这样写原因在于 m3u8 文件里的#EXT-X-KEY字段会原样使用 key_info 第一行的内容。如果写成相对路径播放器会基于 m3u8 所在的目录去拼接路径一深就容易出错。另外我建议把输出文件放到hls/子目录里这样跟原始视频文件做个区分后续批量处理和部署也更清爽。4. 批量处理写一个通用脚本一次处理整个目录4.1 等等先想清楚目录结构真正进入批量之前先想明白一个问题输出目录到底怎么组织。不同视频的 TS 分片如果混在一个目录里文件名会相互覆盖m3u8 文件也会乱成一团。我在项目里用的目录结构是这样的media/ video001.mp4 video002.mp4 video003.mp4 output/ video001/ video001.m3u8 video001.key video001_000.ts video001_001.ts video002/ ...每个视频一个独立子目录目录名跟视频文件名保持一致密钥文件也放在这个目录里。这样后续做鉴权、清理缓存、单独重转某个视频都方便。4.2 Linux/macOS 下的批量脚本我先把 Linux 环境的脚本贴出来这是我在服务器上用过很多轮的方案#!/bin/bash INPUT_DIRmedia OUTPUT_DIRoutput FAILED_LOGfailed_videos.txt : $FAILED_LOG for video in $INPUT_DIR/*.mp4; do filename$(basename $video .mp4) output_path$OUTPUT_DIR/$filename mkdir -p $output_path openssl rand 16 $output_path/$filename.key cat $output_path/key_info EOF $filename.key $output_path/$filename.key EOF echo 正在处理: $video ffmpeg -i $video \ -c:v copy \ -c:a copy \ -hls_time 6 \ -hls_list_size 0 \ -hls_playlist_type vod \ -hls_key_info_file $output_path/key_info \ -hls_segment_filename $output_path/${filename}_%03d.ts \ $output_path/$filename.m3u8 2 /tmp/ffmpeg_$filename.log if [ $? -eq 0 ]; then echo 处理完成: $filename rm -f $output_path/key_info else echo 处理失败: $filename echo $video $FAILED_LOG fi done echo 全部处理结束失败的视频列表见 $FAILED_LOG脚本里做了几件值得注意的事每个视频独立生成密钥文件互不影响。输出目录按视频名隔离避免分片相互覆盖。处理失败的视频记录到独立日志里方便事后排查。成功之后自动删除临时 key_info 文件减少目录垃圾。4.3 Windows 下的批处理方案Windows 服务器或者本地 Windows 环境用批处理也能实现同样的效果。我提供一个 PowerShell 版本的思路$inputDir media $outputDir output Get-ChildItem -Path $inputDir -Filter *.mp4 | ForEach-Object { $filename $_.BaseName $outputPath Join-Path $outputDir $filename New-Item -ItemType Directory -Force -Path $outputPath | Out-Null $keyFile Join-Path $outputPath $filename.key openssl rand 16 | Set-Content -Path $keyFile -AsByteStream $keyInfo $outputPath\key_info $filename.keyn$keyFile | Set-Content -Path $keyInfo ffmpeg -i $_.FullName -c:v copy -c:a copy -hls_time 6 -hls_list_size 0 -hls_playlist_type vod -hls_key_info_file $keyInfo -hls_segment_filename $outputPath\${filename}_%03d.ts (Join-Path $outputPath $filename.m3u8) if ($LASTEXITCODE -eq 0) { Write-Host 处理完成: $filename Remove-Item $keyInfo } else { Write-Host 处理失败: $filename -ForegroundColor Red } }PowerShell 的Set-Content -AsByteStream在较新版本里才支持如果你的环境版本旧可以用[System.IO.File]::WriteAllBytes()替代。Windows 环境跑批量脚本的坑主要是路径分隔符和编码问题建议脚本文件存成 UTF-8 格式避免中文路径乱码。4.4 如何应对超大视频和异常文件实际批量处理中最怕的是 CPU 跑半天结果文件损坏或者某个视频异常导致整个脚本中断。我在脚本里加了几个保护手段亲测有效第一单视频超时控制。有些视频可能编码异常ffmpeg 会卡住不动。用timeout 600 ffmpeg ...给每个视频加一个 10 分钟超时限制超了就杀掉进程记录失败不拖累后续任务。第二磁盘空间预检。TS 分片的体积和原始视频差距不大但如果是重编码的话会膨胀。脚本开头用df -h检查磁盘剩余空间低于设定阈值就停住别等磁盘写满了才发现问题。第三只处理有效视频。可以用ffprobe先检测文件是否完整ffprobe -v error -select_streams v:0 -show_entries streamcodec_name -of csvp0 $video如果这个命令返回空说明文件打不开或者没有视频流直接跳过并记录原因。4.5 批量处理后的产物交付处理完一批视频之后输出目录的交付内容一般长这样output/ video001/ video001.m3u8 video001.key video001_000.ts video001_001.ts ...部署到服务器时你需要保证这些 m3u8 里引用的密钥 URI 指向实际可访问的地址。这里有一个常见坑如果密钥文件跟 m3u8 同目录URI 可以写成相对路径video001.key但前提是播放器必须在相同域名下才能访问。如果视频要嵌入到别的站点或者有跨域播放的需求最好把 URI 改成完整的 HTTPS 地址并且给密钥文件所在目录配置正确的跨域响应头。nginx 下可以这样配置location /hls/ { add_header Access-Control-Allow-Origin *; }这一行配置就是为了解决播放器在另一个域名下请求密钥时被浏览器拦截的问题。很多 H5 播放器项目里明明视频能加载密钥却拿不到播放黑屏十有八九就是跨域头没加。5. 常见问题排查加密分片过程中的高频坑5.1 报错信息速查表我梳理了一个排查表都是实际操作中遇到的真实问题现象可能原因解决办法m3u8 能访问但播放黑屏密钥文件跨域无法加载给密钥所在目录加 CORS 头播放卡在某个分片后不动该分片文件损坏或缺失重新生成整个视频的分片ffmpeg 报Invalid data原始文件编码不兼容 TS 封装改用重编码参数解密后花屏、绿屏key_info 里 URI 与密钥不匹配核对密钥文件是否被覆盖只有最后几个分片能播-hls_list_size没设成 0改成-hls_list_size 0密钥文件 404URI 写的是相对路径但目录结构变了改成绝对 URL 或修正相对路径音频不同步TS 封装时时间戳异常加-copyts参数重新处理5.2 密钥泄露与轮换策略HLS AES-128 加密的安全性说句实话属于“防君子不防高手”级别。TS 分片本身是标准加密但要解密只需要拿到密钥就行而密钥文件就公开放在服务器上。播放器要解密就必须能访问密钥这等于钥匙就挂在门旁边。所以在实际项目中密钥文件的管理策略比加密本身更重要。我常用两个办法来提升安全性一是鉴权 URL。密钥文件不直接对外开放而是通过后端接口动态生成带时效的下载链接。播放器解析 m3u8 后请求密钥后端验证会话有效后返回密钥内容。超过有效期就拒绝这样即使有人拿到了密钥地址过一会儿也就失效了。二是防盗链配置。在服务器层面只允许特定的 Referer 或 User-Agent 访问密钥文件其他请求一律 403。这个方法简单粗暴但有效能挡掉大部分随手转发链接的人。5.3 关于 IV 参数的一个隐藏细节key_info文件的第三行是 IV 参数。如果不写ffmpeg 会自动生成一个序列号作为 IV。这看起来省事但在某些播放器上会引起问题——不同分片用了不同的 IV部分播放器实现得不好解密就乱了。我的建议是固定一个 IV。IV 可以随便定只要 32 位十六进制数就行比如https://your-server.com/hls/video.key video.key 000102030405060708090a0b0c0d0e0f固定 IV 之后所有分片都用同一个 IV 解密兼容性稳定得多。注意 IV 在 m3u8 里是用#EXT-X-KEY的IV属性标出的如果写成十进制数或者位数不对播放器直接不认。5.4 单独处理无音频的视频流有些视频本身就是无声源比如监控片段、录屏素材。这种视频丢给 ffmpeg 处理时如果命令里带-c:a copy而视频流里没有音频ffmpeg 会警告甚至报错。解决办法是判断一下是否有音频流然后用条件参数。ffmpeg 的 map 参数可以解决ffmpeg -i $video \ -map 0:v:0 -map 0:a? \ -c:v copy \ -c:a copy \ ...-map 0:a?里的问号表示音频流可选有就映射处理没有就自动跳过不会报错。这个写法对混合目录批量处理特别有用不用提前区分哪些是无声视频。5.5 日志与失败重试机制批量处理跑得时间长了难免有几个视频反复失败。我在脚本里加了FAILED_LOG就是为了这个。处理后捞一眼失败列表逐个看日志大部分失败原因集中在原始文件编码异常、文件路径有特殊字符、磁盘空间不足这三类。重试的时候有个技巧不用整个视频重新来如果 ffmpeg 已经生成了部分分片可以用-hls_start_number_source datetime或者手动指定-start_number来避免分片序号冲突。不过更省事的做法还是删掉该目录重新转一次反正批量脚本设计上就是可重复执行的。这里再补充一个细节日志要去掉噪音。ffmpeg 默认会向 stderr 输出大量进度信息和警告用-v error参数可以只输出错误信息日志会清爽很多。但注意不要把-v error用在排查 bug 时那会儿需要详细日志辅助定位。6. 按需扩展从纯加密分片到完整分发链路批量脚本跑通之后你会发现这只是整个视频分发体系的第一步。后面还能叠加不少东西比如转码预设、CDN 预热、防盗链配置、播放器对接、实时监控等。我个人的经验是在做完加密分片这一步之后要先花一点时间把播放端串通验证一遍而不是急着把整批视频全部转换完毕再去接播放器。找个开发环境拿一个视频转了分片用本地播放器和 H5 播放器分别测试播放、拖动、倍速这些基础功能确认没问题再批量铺开这样避免返工。密钥管理同样可以扩展。现在脚本里每视频一个密钥文件如果视频量到几千上万个密钥文件的保存和备份就成了新问题。可以考虑把密钥存到数据库里分发时后端动态读取并返回前端不用关心密钥文件在哪个目录。防盗链也不是一劳永逸的。HLS 加密分片能挡住普通用户直接下载原视频但录屏、录播这种行为谁都防不住。所以加密分片的定位应该是“提高获取门槛”而不是“绝对不可破解”。做完这一步整个链路的保护等级已经从裸奔上升到一个可接受的范围。如果你是第一次在自己的项目里落地这套方案建议先拿三五条不同格式、不同时长、不同分辨率的视频试试把脚本参数调到符合自己片源的实际情况再扩大到全量数据。加密分片这件事看起来就是一条命令的事真正做顺了还是要在背后把细节打磨好。