尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

HLS与M3U8实战:直播点播、TS切片与AES加密全解析

HLS与M3U8实战:直播点播、TS切片与AES加密全解析 开头部分我用一个真正的业务故障切入之前负责的一个直播项目上线当晚用户反馈“首页进去黑屏”后来发现是选型时没考虑HLS的兼容性。从这个实战角度引出HLS/M3U8这套体系说明这篇文章要讲什么、适合谁看。1. 为什么老司机都在谈HLS一次直播选型复盘1.1 从“首页黑屏”说起前两年我负责一个电视直播类的Web项目技术选型的时候团队一开始走的是RTMP推流HTTP-FLV播放。这套方案在PC端Chrome里跑得很欢延迟能做到3秒以内当时大家都觉得“稳了”。上线那天晚上运营反馈说苹果手机用户打开页面黑屏Safari直接不支持RTMP和FLV只能靠浏览器插件或者压根不播这才开始认真评估HLS。后来我们把直播链路整个切到HLS服务端推流后转封装成TS分片通过M3U8索引对外分发客户端用原生video标签加hls.js播放问题才算解决。那次踩坑给我最大的教训是流媒体协议选型第一优先不是延迟而是终端兼容性。HLS基于HTTP协议iOS Safari、Android WebView、Chrome、Firefox全都能覆盖不需要额外装插件天然过防火墙CDN分发也方便。这篇文章就把HLS这套体系掰开讲清楚M3U8索引到底怎么组织视频切片为什么是TS格式AES-128加密怎么接多码流自适应是怎么让播放器“自动选档”的。如果你是做直播点播的后端、前端或者正在调研播放方案的运维这篇文章可以帮你少走弯路。1.2 HLS的三件套切片、索引、传输HLS全称HTTP Live Streaming苹果公司提出。它和RTMP最大的区别是RTMP是长连接持续推流HLS是“把视频切碎分批发货”。一段10秒的视频会被切成多个小文件每个小文件叫一个TS分片Transport Stream时长通常2到10秒不等。然后生成一个M3U8索引文件把分片的播放顺序、时长、加密信息都记在里面。播放器拿到M3U8后逐个请求TS分片边下载边播放。这套设计最聪明的地方在于所有分发都退化成“HTTP GET一个文件”而不是维护一条常连接。CDN节点缓存的就是一堆静态TS文件流量再大也能横向扩展源站压力很小。这也是为什么HLS能成为目前OTT、移动端直播点播事实标准的原因。注意新标准里HLS也可以封装成fMP4CMAF分片但这里我们讨论最主流的TS分片方案80%以上的线上系统还在用它排查问题的思路也更通用。2. M3U8索引文件播放器的“总调度”2.1 点播M3U8逐行拆解M3U8本质上是一个UTF-8编码的文本索引播放器靠它才知道“先播谁、后播谁”。我拿一个真实的点播M3U8文件来逐行拆解#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:6.006, segment_000.ts #EXTINF:6.006, segment_001.ts #EXTINF:4.004, segment_002.ts #EXT-X-ENDLIST#EXTM3U固定头表示这是一个M3U8文件。#EXT-X-VERSION:3协议版本3是兼容性最好的版本。#EXT-X-TARGETDURATION:6所有分片的最大时长播放器用它来预分配缓冲。#EXT-X-MEDIA-SEQUENCE:0第一个分片的序号直播场景里这个值的意义很大后面细说。#EXT-X-PLAYLIST-TYPE:VOD告诉播放器这是一个点播列表不允许动态追加内容。#EXTINF:6.006,紧跟的分片时长单位秒后面那个逗号可以跟标题信息通常为空。segment_000.ts分片文件名可以是相对路径也可以是完整URL。#EXT-X-ENDLIST列表结束标记点播文件必须有直播场景通常没有。这里有个细节值得留意#EXTINF里的时长是十进制浮点数实际生产环境里TS分片的帧结构决定了它不一定是整6秒写6.006、4.004这种值是正常的。播放器做进度条和时间切换时依据的是这些EXTINF累积出来的总时长不是你以为的“文件名序号”。2.2 直播M3U8索引是怎么“滚动”的直播和点播最大的区别在于直播的M3U8是不停更新的。服务端持续生成新的TS分片同时把过期的分片从索引里移除播放器每隔几秒重新拉取一次M3U8拿到新增的分片地址继续播。实际生产环境里直播M3U8长这样#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-MEDIA-SEQUENCE:5682 #EXTINF:6.006, live_segment_5682.ts #EXTINF:6.006, live_segment_5683.ts #EXTINF:6.006, live_segment_5684.ts注意这里没有#EXT-X-ENDLIST只有越来越大的#EXT-X-MEDIA-SEQUENCE序号。播放器看到序号从5682开始就知道前面5680、5681已经过期了不会再请求。这也是为什么直播HLS延迟比RTMP高——播放器总是从当前索引的某个位置开始播而不是从推流端实时渲染。HLS直播的延迟通常在15到30秒左右切片时长6秒的情况下。如果你把切片调短比如设成2秒延迟能降到8秒左右但CDN回源次数会增加索引文件也会更频繁刷新容易触发CDN缓存穿透。做直播方案时要在这两个目标之间找平衡不能一味追求低延迟。2.3 播放器如何把分片拼回完整视频播放器拿到M3U8后做的事情可以理解成“流水线组装”下载分片、解析分片、解码分片、送入渲染队列。TS分片和MP4不一样MP4有一个叫moov的索引盒子通常放在文件头部一旦文件不完整就播不了而TS分片是一段连续的小型传输流每个分片自带音视频数据包PES解码器可以从任意分片开始解码。这带来两个优势。第一直播场景里播放器丢几个分片不会全盘崩溃最多画面跳一下第二点播中用户拖进度条时播放器能精准定位到对应的TS分片只下载那一段而不是从头缓冲。分片内部结构也值得一提。一个TS分片通常以0x47同步字节开头内部包含PAT节目关联表、PMT节目映射表和PES封装后的音视频数据。ffprobe可以看到这些细节ffprobe -show_packets -select_streams v live_segment_5682.ts看输出里pts_time和dts_time的差值就能判断关键帧间隔是否合理。GOP关键帧组通常要和切片边界对齐这直接影响多码流自适应切换的流畅度后面第4节展开说。3. AES-128加密给TS分片加一道锁3.1 为什么加密的是分片不是整个文件很多人第一次接触HLS加密都会问为什么不直接对整个视频加密原因有两个。第一HLS是“边下边播”的播放器需要能解密单个分片并立即播放而不是等整个文件下载完再解密。AES-128加密模式下每个TS分片都是独立加密的播放器拿到钥匙后可以逐片解密播放完全符合流式体验。第二不加密的话视频CDN分发出去任何人拿到M3U8和TS文件都能直接拼成完整视频防盗链形同虚设。即便加了HLS加密也要明白它防的是“普通用户抓包下载”防不了有技术能力的攻击者因为播放器要播放就必须拿到密钥密钥一旦下发就存在泄露风险。更高安全诉求必须上商业DRM如Widevine、FairPlay那是另一个话题。AES-128加密逻辑上并不复杂服务端生成一个16字节128位的密钥对每个TS分片做AES-CBC加密。加密后的分片照常发布密钥放在另一个文件里由M3U8通过#EXT-X-KEY标签引用。3.2 实操用ffmpeg跑通AES-128加密HLS我直接给一整套可复现的命令。假设你有一个input.mp4要把它切成加密的点播HLS。第一步生成16字节密钥openssl rand 16 enc.key这个enc.key就是对称密钥文件只有256比特不要用文本编辑器手工乱敲。第二步创建密钥信息文件enc.keyinfo内容格式固定共三行https://your.cdn.com/keys/enc.key /path/to/local/enc.key 00000000000000000000000000000001第一行是M3U8里EXT-X-KEY标签引用的URI播放器会拿着这个地址去请求密钥第二行是ffmpeg加密时读取密钥的本地路径第三行是初始向量IV必须是32位十六进制字符串16字节。如果第三行留空ffmpeg会用分片序号自动生成IV。第三步执行切片加密ffmpeg -i input.mp4 -c:v libx264 -c:a aac \ -hls_time 6 \ -hls_playlist_type vod \ -hls_key_info_file enc.keyinfo \ -hls_segment_filename enc_seg_%03d.ts \ enc_index.m3u8生成的enc_index.m3u8里会出现关键行#EXT-X-KEY:METHODAES-128,URIhttps://your.cdn.com/keys/enc.key,IV0x00000000000000000000000000000001播放器看到这个标签会先请求密钥文件再解密后续分片。3.3 密钥文件与IV管理的细节实际操作中密钥管理才是最容易踩坑的地方我列几个心得密钥文件不要放在和TS分片同一目录的静态目录里。生产环境应该由鉴权服务动态返回比如/api/hls/key?idxxxtokenyyy服务端校验会话后输出16字节二进制密钥。对应到一些业务里的说法加密用的“盐”放后端而不是写死在播放器端就是这个道理。密钥下发必须走HTTPS否则密钥在传输过程中被人抓包加密等于白做。每个视频最好用不同的密钥甚至每24小时轮换一次密钥。密钥文件被泄露后至少影响范围可控。IV不一定要固定如果不填ffmpeg会用分片序号来做IV初始化。只要密钥不泄露用序号做IV是安全的但如果你同一把密钥加密了多个视频文件建议IV也换一换避免相同明文块在不同视频中产生相同密文块。注意改密钥或IV后旧的M3U8如果还在CDN缓存里播放器可能会拿到旧索引和旧密钥导致解密失败。密钥轮换时要同时刷新M3U8缓存或者使用不同的URL版本。4. 多码流自适应让每个观众都看到“不卡”的画面4.1 自适应到底在解决什么问题同样是1080P视频有的人用千兆宽带有的人在地铁里用4G网络同一路码率怎么可能都流畅不搞自适应的方案通常是要么定一个低码率画质被所有人骂要么定一个高码率弱网用户疯狂卡顿。HLS多码流自适应的思路很简单服务端同时准备多档码率的视频分片主M3U8里把这些档位全部列出来播放器根据实时测速自动选择最合适的那一档。网络变好就切高码率网络变差就切低码率全程不用用户干预。苹果官方推荐的码率阶梯大概是1080P约4000kbps、720P约2000kbps、540P约1500kbps、360P约800kbps。你可以根据自己内容的类型调整比如纯画面变化的体育赛事需要更高码率画面相对静止的访谈节目可以压低码率。4.2 主索引与子索引的层级结构多码流HLS是一个“一级索引套二级索引”的结构。主M3U8长这样#EXTM3U #EXT-X-STREAM-INF:BANDWIDTH800000,RESOLUTION640x360,CODECSavc1.4d401e,mp4a.40.2 360p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH2000000,RESOLUTION1280x720,CODECSavc1.4d401f,mp4a.40.2 720p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH4000000,RESOLUTION1920x1080,CODECSavc1.640028,mp4a.40.2 1080p.m3u8BANDWIDTH属性是峰值码率单位是bit/s这里800000代表800kbps。建议在这个数值上再上浮20%把TCP/IP头部开销和下载抖动余量算进去否则播放器容易误判带宽。RESOLUTION是画面分辨率CODECS是编码信息Android端的一些播放器会拿它做硬解兼容性判断。每个子M3U8就是普通的分片索引内容格式和上面第2节点播/直播M3U8一致。播放器先下载主M3U8解析出所有档位再根据当前网络状况选择下载某个子M3U8。4.3 播放器端的选择策略与踩坑点播放器选档逻辑大致分两类。一种是“带宽探测型”下载一个分片后统计下载耗时和分片大小估算出当前带宽再决定下一档切换。另一种是“自定义策略型”比如hls.js里可以设置startLevel起始档位和capLevelToPlayerSize按播放器窗口大小限制码率开发者可以直接干预。实际项目里我在用hls.js时调过这些参数const hls new Hls({ startLevel: -1, // -1表示自动选择 abrEwmaDefaultBandwidth: 1000000, abrEwmaFastLive: 3.0, abrEwmaSlowLive: 9.0, capLevelToPlayerSize: true });startLevel: -1让播放器从最合适的档位开始而不是固定最低档避免画质“先糊后清晰”的体验capLevelToPlayerSize在手机横竖屏切换时自动降低码率省流量。多码流自适应最容易踩的坑是GOP对齐。所谓GOP对齐就是所有档位的分片边界必须落在同一批关键帧位置上。如果360P的切片边界在0秒、6秒、12秒而1080P的边界在1秒、7秒、13秒播放器从1080P切到360P时目标分片可能不是一个关键帧开头画面会出现花屏或卡顿。解决方法是编码时统一设置GOP大小比如-g 6060帧一个关键帧对应帧率30fps时正好2秒并且保证切片时长是GOP时长的整数倍。实操时可以用一条ffmpeg命令批量生成多档分片ffmpeg -i input.mp4 \ -vf scale640:360 -c:v libx264 -b:v 800k -g 60 -sc_threshold 0 \ -c:a aac -b:a 96k -hls_time 6 -hls_playlist_type vod -hls_segment_filename 360p_%03d.ts 360p.m3u8 \ -vf scale1280:720 -c:v libx264 -b:v 2000k -g 60 -sc_threshold 0 \ -c:a aac -b:a 128k -hls_time 6 -hls_playlist_type vod -hls_segment_filename 720p_%03d.ts 720p.m3u8注意-sc_threshold 0这个参数它用来关闭ffmpeg的场景自动切换关键帧保证每个分片严格按设定的GOP边界切分。省略它编码器会在画面变化剧烈时提前插入关键帧导致分片边界不一致。5. 常见问题与排查技巧实录5.1 M3U8转MP4失败的几个高频原因网上流传一句话“M3U8转MP4失败”我从实际接触到的案例总结出四个高频原因。第一个是#EXT-X-ENDLIST缺失。直播型M3U8是滚动更新的没有结束标记ffmpeg会一直等待后续分片命令卡住不动。处理方法是确认这个M3U8到底是不是点播文件直播内容想转MP4得先完整录制得到所有分片并生成带ENDLIST的索引。第二个是分片文件请求失败。M3U8正常但里面的TS分片返回403或404。这种情况十有八九是防盗链分片请求需要带Referer或User-Agent才能访问。用ffmpeg下载时可以通过-headers传入ffmpeg -headers Referer: https://example.com/ -i input.m3u8 -c copy -bsf:a aac_adtstoasc output.mp4第三个是加密内容密钥缺失。加密M3U8里EXT-X-KEY指向的密钥文件不可访问ffmpeg就解不了密。可以先用curl测试密钥URL是否能正常返回16字节内容如果密钥有鉴权先在浏览器里登录把密钥文件下载下来再手动修改M3U8里的URI指向本地路径。第四个是音频编码问题。TS分片里的AAC音频转成MP4时需要处理ADTS头到MP4容器的转换命令里必须带-bsf:a aac_adtstoasc否则转出来的MP4要么没声音要么播放器报错。5.2 网页播放M3U8hls.js与原生支持的取舍在Web端播放M3U8先搞清楚一个事实Safari和iOS内置浏览器原生支持HLS直接把M3U8地址丢给video标签就能播Chrome、Firefox、Edge都不原生支持需要借助JavaScript播放器。hls.js是目前用的最多的库它通过Media Source ExtensionsMSE把TS分片转成浏览器能直接消费的fMP4流喂给video标签。Vue项目里的用法我贴一段简化代码import Hls from hls.js; export default { props: { src: String }, mounted() { const video this.$refs.video; if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(this.src); hls.attachMedia(video); hls.on(Hls.Events.ERROR, (event, data) { if (data.fatal) { // 处理致命错误比如网络切换、媒体错误 hls.recoverMediaError(); } }); this.hls hls; } else if (video.canPlayType(application/vnd.apple.mpegurl)) { video.src this.src; // iOS原生播放 } }, beforeDestroy() { this.hls this.hls.destroy(); } };这里特别强调beforeDestroy里要调hls.destroy()。我见过不少项目因为组件卸载时没有销毁Hls实例导致页面反复进入时视频黑屏、音频和画面不同步内存和网络请求都异常。你可以在浏览器Network面板里看到多个Hls实例在重复请求M3U8。5.3 直播卡顿、花屏从分片序号找线索直播HLS出问题先看M3U8的#EXT-X-MEDIA-SEQUENCE。正常直播中这个序号是单调递增的每次刷新索引都在前一次基础上加几个。如果序号跳变幅度很大说明源站切片服务出现过中断播放器要么追不上进度要么跳帧播放。分片请求出现404时要区分两种情况一种是真的过期了播放器请求的是“上一版M3U8里的旧分片”源站已经把文件删了这是正常的播放器重新拉取索引即可恢复另一种是分片还没生成完播放器就请求了常见于源站切片速度跟不上分发请求CDN每回源一次就多等几秒。排查方式是在CDN日志里看404响应码的回源时间和源站处理时长。花屏问题先看是不是所有用户都花屏。如果是个别网络下的用户花屏大概率是弱网丢包导致分片下载不完整播放器没做分片完整性校验。如果是所有用户都在某一个时间点花屏去对比那个时间点各码率分片的编码参数重点看width、height、profile_idc是否一致。我曾经遇到过一次花屏原因是后台配置的1080P档位编码器丢了一个-profile:v high参数导致关键帧尺寸异常切到该档位就花屏。用ffprobe检查分片编码参数ffprobe -show_streams -select_streams v 1080p_002.ts重点关注codec_name、profile、level、width、height、avg_frame_rate这些字段。多码流所有档位的H264编码等级不需要完全一致但解码器兼容性和关键帧间隔最好保持一致。5.4 关于M3U8地址与转换的几点提醒平时会看到一些公开的M3U8地址比如网络收音机、电视直播源之类的。这类地址大部分属于非官方渠道稳定性没有保证经常遇到404、403或者突然不能访问。问题往往不在播放器而在地址本身随时会失效。做技术测试时可以在本地用ffmpeg自建一路HLS流来练手ffmpeg -re -i local_video.mp4 -c:v libx264 -tune zerolatency -c:a aac -f hls -hls_time 4 -hls_list_size 8 -hls_flags delete_segments live_test.m3u8-hls_list_size 8表示直播索引只保留最近8个分片-hls_flags delete_segments会自动删除过期分片模拟真实的直播滚动效果。至于从在线平台抓取M3U8并转存视频这类操作很容易涉及版权问题。从技术上讲只要M3U8可访问、密钥可获取转MP4确实可行但这不代表你有权这么做。我自己的态度是技术研究可以别把抓取别人平台的付费内容当日常操作尊重版权别给自己惹麻烦。还有一个小经验想分享遇到M3U8播放问题不要一上来就改代码。先用curl把M3U8下载下来看一眼内容确认有没有EXT-X-KEY、#EXT-X-ENDLIST、#EXT-X-STREAM-INF这三种标签直接决定问题方向。真要说值钱的经验我觉得是把HLS理解成“一堆HTTP小文件加一个索引”后面所有问题都好排查了。无论是切片、加密还是自适应码率本质上都是在对这套基于HTTP的文件体系做文章。
返回列表