[{"data":1,"prerenderedAt":1004},["ShallowReactive",2],{"blog-zh-Hans-hls-m3u8-deep-dive-encryption-multitrack":3,"blog-zh-Hans-hls-m3u8-deep-dive-encryption-multitrack-surround":991},{"id":4,"title":5,"author":6,"body":7,"category":972,"date":973,"description":974,"extension":975,"image":976,"lastModified":973,"meta":977,"navigation":978,"path":979,"readingTime":980,"seo":981,"stem":982,"tags":983,"__hash__":990},"blog_zh_Hans\u002Fzh-Hans\u002Fblog\u002Fhls-m3u8-deep-dive-encryption-multitrack.md","HLS 深度：加密、多音轨、值得记住的 EXT-X 标签","FlowPick 团队",{"type":8,"value":9,"toc":956},"minimark",[10,28,31,36,39,49,52,58,65,70,76,104,126,130,133,139,146,373,379,394,418,424,428,442,445,471,480,495,503,506,512,515,523,526,545,549,627,638,642,648,671,674,697,700,704,707,727,737,740,762,767,773,784,802,808,811,849,852,875,894,897,900,952],[11,12,13,18,19,23,24,27],"p",{},[14,15,17],"a",{"href":16},"\u002Fzh-Hans\u002Fblog\u002Fgetting-started-with-hls-m3u8-download","HLS 入门指南"," 讲了\"什么是 M3U8、怎么下载\"。这篇是另一篇——给那些试着下过一个流、拿到一个 200KB 的 ",[20,21,22],"code",{},".m3u8","、里面引用了 8 条音轨、4 种字幕、还有一个让 VLC 吐垃圾的 ",[20,25,26],{},"#EXT-X-KEY"," 标签的人。",[11,29,30],{},"你打开过 Netflix、Disney+ 或付费课程平台的真实 HLS 清单，纳闷为什么你的下载器下出来的是无声视频或损坏文件，原因就在这里。",[32,33,35],"h2",{"id":34},"真实-hls-清单长什么样","真实 HLS 清单长什么样",[11,37,38],{},"多数教程用这个当例子：",[40,41,46],"pre",{"className":42,"code":44,"language":45},[43],"language-text","#EXTM3U\n#EXT-X-TARGETDURATION:6\n#EXT-X-VERSION:3\n#EXTINF:6.0,\nsegment0.ts\n#EXTINF:6.0,\nsegment1.ts\n#EXTINF:4.2,\nsegment2.ts\n#EXT-X-ENDLIST\n","text",[20,47,44],{"__ignoreMap":48},"",[11,50,51],{},"可爱。真实清单不长这样。这是一个主流平台的真实 master playlist，精简过但能代表全貌：",[40,53,56],{"className":54,"code":55,"language":45},[43],"#EXTM3U\n#EXT-X-VERSION:6\n#EXT-X-INDEPENDENT-SEGMENTS:yes\n\n# 音频组\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac-64\",NAME=\"English\",LANGUAGE=\"en\",DEFAULT=YES,AUTOSELECT=YES,CHANNELS=\"2\",URI=\"audio\u002Feng_64.m3u8\"\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac-128\",NAME=\"English\",LANGUAGE=\"en\",DEFAULT=YES,AUTOSELECT=YES,CHANNELS=\"2\",URI=\"audio\u002Feng_128.m3u8\"\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac-64\",NAME=\"Español\",LANGUAGE=\"es\",DEFAULT=NO,AUTOSELECT=YES,CHANNELS=\"2\",URI=\"audio\u002Fspa_64.m3u8\"\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"English\",LANGUAGE=\"en\",DEFAULT=NO,AUTOSELECT=YES,URI=\"subs\u002Fen.m3u8\"\n#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"cc\",NAME=\"CC\",LANGUAGE=\"en\",DEFAULT=NO,AUTOSELECT=YES,INSTREAM-ID=\"CC1\"\n\n# 视频变体\n#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=854x480,CODECS=\"avc1.64001f,mp4a.40.2\",AUDIO=\"aac-64\",SUBTITLES=\"subs\",CLOSED-CAPTIONS=\"cc\"\nv_480.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=1280x720,CODECS=\"avc1.640020,mp4a.40.2\",AUDIO=\"aac-128\",SUBTITLES=\"subs\",CLOSED-CAPTIONS=\"cc\"\nv_720.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1920x1080,CODECS=\"avc1.640028,mp4a.40.2\",AUDIO=\"aac-128\",SUBTITLES=\"subs\",CLOSED-CAPTIONS=\"cc\"\nv_1080.m3u8\n",[20,57,55],{"__ignoreMap":48},[11,59,60,61,64],{},"实际媒体分片根本不在这里。master playlist 指向变体 playlist，每个变体再指向分片。音频还单独在自己的 playlist 里。你的下载器要是只抓 ",[20,62,63],{},"v_1080.m3u8"," 然后拼接分片，得到的就是无声视频。这是\"HLS 下载没声音\"反馈的头号原因。",[66,67,69],"h3",{"id":68},"codecs-字段不是装饰","CODECS 字段不是装饰",[11,71,72,75],{},[20,73,74],{},"CODECS=\"avc1.640028,mp4a.40.2\""," 不是写着好看：",[77,78,79,98],"ul",{},[80,81,82,85,86,89,90,93,94,97],"li",{},[20,83,84],{},"avc1.640028"," — H.264 High profile、level 4.0。",[20,87,88],{},"avc1."," 后面前两个十六进制位编码 profile（",[20,91,92],{},"64"," = High）、constraint flag、level（",[20,95,96],{},"28"," = 4.0）。",[80,99,100,103],{},[20,101,102],{},"mp4a.40.2"," — AAC-LC。",[11,105,106,107,109,110,113,114,117,118,121,122,125],{},"为什么重要：某个变体声明 ",[20,108,84],{},"，你却把分片封装进一个 codec box 写错的 MP4（",[20,111,112],{},"hvc1"," 而不是 ",[20,115,116],{},"avc1","），文件不能播。",[20,119,120],{},".ts"," 分片裸拼接能避开这个问题，因为 MPEG-TS 自带 codec 信息；封装进 MP4 就得解析这个字段、写对 ",[20,123,124],{},"stsd"," box。",[32,127,129],{"id":128},"ext-x-key加密以及直接解密不是一个句子","EXT-X-KEY：加密，以及\"直接解密\"不是一个句子",[11,131,132],{},"清单里有这行：",[40,134,137],{"className":135,"code":136,"language":45},[43],"#EXT-X-KEY:METHOD=AES-128,URI=\"https:\u002F\u002Fcdn.example.com\u002Fkey.bin\",IV=0x1a2b3c4d5e6f7890abcdef1234567890\n",[20,138,136],{"__ignoreMap":48},[11,140,141,142,145],{},"意味着之后每个分片都用 ",[20,143,144],{},"URI"," 处的密钥和指定的 IV 做 AES-128-CBC 加密。解密：",[40,147,151],{"className":148,"code":149,"language":150,"meta":48,"style":48},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","const keyResp = await fetch(keyUri)\nconst keyBytes = await keyResp.arrayBuffer()  \u002F\u002F 16 字节\nconst iv = new Uint8Array(16)\n\u002F\u002F IV 可能是字面值（如上），也可能从序号推导\nconst decrypted = await crypto.subtle.decrypt(\n  { name: 'AES-CBC', iv },\n  await crypto.subtle.importKey('raw', keyBytes, 'AES-CBC', false, ['decrypt']),\n  encryptedSegment\n)\n","js",[20,152,153,181,209,235,241,269,300,362,368],{"__ignoreMap":48},[154,155,158,162,166,170,174,178],"span",{"class":156,"line":157},"line",1,[154,159,161],{"class":160},"spNyl","const",[154,163,165],{"class":164},"sTEyZ"," keyResp ",[154,167,169],{"class":168},"sMK4o","=",[154,171,173],{"class":172},"s7zQu"," await",[154,175,177],{"class":176},"s2Zo4"," fetch",[154,179,180],{"class":164},"(keyUri)\n",[154,182,184,186,189,191,193,196,199,202,205],{"class":156,"line":183},2,[154,185,161],{"class":160},[154,187,188],{"class":164}," keyBytes ",[154,190,169],{"class":168},[154,192,173],{"class":172},[154,194,195],{"class":164}," keyResp",[154,197,198],{"class":168},".",[154,200,201],{"class":176},"arrayBuffer",[154,203,204],{"class":164},"()  ",[154,206,208],{"class":207},"sHwdD","\u002F\u002F 16 字节\n",[154,210,212,214,217,219,222,225,228,232],{"class":156,"line":211},3,[154,213,161],{"class":160},[154,215,216],{"class":164}," iv ",[154,218,169],{"class":168},[154,220,221],{"class":168}," new",[154,223,224],{"class":176}," Uint8Array",[154,226,227],{"class":164},"(",[154,229,231],{"class":230},"sbssI","16",[154,233,234],{"class":164},")\n",[154,236,238],{"class":156,"line":237},4,[154,239,240],{"class":207},"\u002F\u002F IV 可能是字面值（如上），也可能从序号推导\n",[154,242,244,246,249,251,253,256,258,261,263,266],{"class":156,"line":243},5,[154,245,161],{"class":160},[154,247,248],{"class":164}," decrypted ",[154,250,169],{"class":168},[154,252,173],{"class":172},[154,254,255],{"class":164}," crypto",[154,257,198],{"class":168},[154,259,260],{"class":164},"subtle",[154,262,198],{"class":168},[154,264,265],{"class":176},"decrypt",[154,267,268],{"class":164},"(\n",[154,270,272,275,279,282,285,289,292,295,297],{"class":156,"line":271},6,[154,273,274],{"class":168},"  {",[154,276,278],{"class":277},"swJcz"," name",[154,280,281],{"class":168},":",[154,283,284],{"class":168}," '",[154,286,288],{"class":287},"sfazB","AES-CBC",[154,290,291],{"class":168},"'",[154,293,294],{"class":168},",",[154,296,216],{"class":164},[154,298,299],{"class":168},"},\n",[154,301,303,306,308,310,312,314,317,319,321,324,326,328,331,333,335,337,339,341,345,347,350,352,354,356,359],{"class":156,"line":302},7,[154,304,305],{"class":172},"  await",[154,307,255],{"class":164},[154,309,198],{"class":168},[154,311,260],{"class":164},[154,313,198],{"class":168},[154,315,316],{"class":176},"importKey",[154,318,227],{"class":164},[154,320,291],{"class":168},[154,322,323],{"class":287},"raw",[154,325,291],{"class":168},[154,327,294],{"class":168},[154,329,330],{"class":164}," keyBytes",[154,332,294],{"class":168},[154,334,284],{"class":168},[154,336,288],{"class":287},[154,338,291],{"class":168},[154,340,294],{"class":168},[154,342,344],{"class":343},"sfNiH"," false",[154,346,294],{"class":168},[154,348,349],{"class":164}," [",[154,351,291],{"class":168},[154,353,265],{"class":287},[154,355,291],{"class":168},[154,357,358],{"class":164},"])",[154,360,361],{"class":168},",\n",[154,363,365],{"class":156,"line":364},8,[154,366,367],{"class":164},"  encryptedSegment\n",[154,369,371],{"class":156,"line":370},9,[154,372,234],{"class":164},[11,374,375,378],{},[20,376,377],{},"crypto.subtle.decrypt"," 是浏览器里的正确工具——不需要 WASM 写 AES，原生 WebCrypto 又快又是 constant-time。",[11,380,381,382,389,390,393],{},"教程不会告诉你的是：",[383,384,385,388],"strong",{},[20,386,387],{},"METHOD=AES-128"," 是简单情况。"," 大多数主流平台用 ",[20,391,392],{},"METHOD=SAMPLE-AES","，苹果的 FairPlay DRM 邻近方案。SAMPLE-AES：",[77,395,396,399,406],{},[80,397,398],{},"每个分片只有部分被加密（视频 NAL unit，音频和 PES 头不加密）",[80,400,401,402,405],{},"key URI 通常返回的是一个被 FairPlay license 交换包过的 ",[20,403,404],{},"EXT-X-KEY"," 响应——不是裸 16 字节密钥",[80,407,408,409,412,413,417],{},"浏览器标签页里",[383,410,411],{},"无法","解密。这就是 DRM，\"FlowPick 下不了这个 Netflix 视频\"是特性不是 bug。我们在 ",[14,414,416],{"href":415},"\u002Fzh-Hans\u002Fblog\u002Fis-it-legal-to-download-streaming-video","流媒体下载合法性指南"," 里讲了这条法律和技术底线。",[11,419,420,423],{},[383,421,422],{},"我的看法："," 哪个下载器声称能处理 FairPlay、Widevine 或 PlayReady，要么在撒谎，要么在大多数司法管辖区违法。FlowPick 明确拒绝碰 SAMPLE-AES 内容。正确行为是大声报错，不是静默产出损坏文件。",[32,425,427],{"id":426},"ext-x-media音频和字幕轨道","EXT-X-MEDIA：音频和字幕轨道",[11,429,430,433,434,437,438,441],{},[20,431,432],{},"EXT-X-MEDIA"," 标签是 HLS 多音轨的机制。每个 ",[20,435,436],{},"GROUP-ID"," 是一组可互换的音频版本；变体 playlist 的 ",[20,439,440],{},"AUDIO"," 属性指向它用的那组。",[11,443,444],{},"对下载器意味着：一次\"完整下载\"不只是视频分片。是：",[446,447,448,451,461,468],"ol",{},[80,449,450],{},"选定变体 playlist 的视频分片",[80,452,453,454,456,457,460],{},"一条 ",[20,455,432],{}," 条目的音频分片（通常是 ",[20,458,459],{},"DEFAULT=YES"," 那条，但应该让用户选）",[80,462,463,464,467],{},"可选的，",[20,465,466],{},"TYPE=SUBTITLES"," 的字幕分片",[80,469,470],{},"把它们封装进一个 MP4，写正确的轨道元数据",[11,472,473,474,476,477,479],{},"封装这一步最容易翻车。来自不同 playlist 的 MPEG-TS 分片各有自己的 PTS 时间线；把只有视频的 ",[20,475,120],{}," 和只有音频的 ",[20,478,120],{}," 拼进一个 MP4，你得：",[77,481,482,485,488],{},[80,483,484],{},"从每个 TS 包解析 PES 头抽 PTS",[80,486,487],{},"按 PTS 排序包",[80,489,490,491,494],{},"把它们写进 MP4，每个轨道的 ",[20,492,493],{},"mdia\u002Fminf\u002Fstbl"," 结构要正确",[11,496,497,498,502],{},"这是 FlowPick 的 WebAssembly FFmpeg 干的活——见 ",[14,499,501],{"href":500},"\u002Fzh-Hans\u002Fblog\u002Fhow-flowpick-merges-video-segments-in-browser","浏览器内合并深度篇","。重点：\"拼文件\"在出现独立音轨那一刻就不灵了。",[66,504,505],{"id":505},"不连续性标签",[40,507,510],{"className":508,"code":509,"language":45},[43],"#EXT-X-DISCONTINUITY\n",[20,511,509],{"__ignoreMap":48},[11,513,514],{},"意思是\"时间戳在这之后重置\"。常见于：",[77,516,517,520],{},[80,518,519],{},"带广告位的直播（广告有自己的时间线）",[80,521,522],{},"不同编码器拼接的清单",[11,524,525],{},"无脑拼接分片的下载器，在不连续点会让播放器时间倒流。要么：",[446,527,528,531,534],{},[80,529,530],{},"用会处理不连续性的重封装器（FFmpeg 行）",[80,532,533],{},"把不连续性 strip 掉，接受时间戳漂移",[80,535,536,537,539,540,544],{},"直接跳过广告分片（FlowPick 默认这样做，反正广告多是 2 秒的 ",[20,538,120],{}," 污染流——见 ",[14,541,543],{"href":542},"\u002Fzh-Hans\u002Fblog\u002Fflowpick-v1-0-0-first-public-release","v1.0.0 发布说明"," 里的尺寸过滤器）",[32,546,548],{"id":547},"ext-x-version值得记住的版本标签","EXT-X-VERSION：值得记住的版本标签",[550,551,552,568],"table",{},[553,554,555],"thead",{},[556,557,558,562,565],"tr",{},[559,560,561],"th",{},"版本",[559,563,564],{},"加了什么",[559,566,567],{},"为什么你在意",[569,570,571,583,594,605,616],"tbody",{},[556,572,573,577,580],{},[574,575,576],"td",{},"2",[574,578,579],{},"I-frame playlist",[574,581,582],{},"Trick mode（快进\u002F倒退）支持",[556,584,585,588,591],{},[574,586,587],{},"3",[574,589,590],{},"浮点 EXTINF",[574,592,593],{},"低延迟流的亚秒级时长",[556,595,596,599,602],{},[574,597,598],{},"4",[574,600,601],{},"独立音频\u002F字幕的 EXT-X-MEDIA",[574,603,604],{},"多音轨——见上",[556,606,607,610,613],{},[574,608,609],{},"5",[574,611,612],{},"EXT-X-KEY 的 IV 可选",[574,614,615],{},"密钥能从序号推导 IV",[556,617,618,621,624],{},[574,619,620],{},"6",[574,622,623],{},"EXT-X-MAP",[574,625,626],{},"fMP4 的 init 分片——见下一节",[11,628,629,630,633,634,637],{},"看到 ",[20,631,632],{},"#EXT-X-VERSION:6"," 而变体 playlist 里没有 ",[20,635,636],{},"#EXT-X-MAP","，哪里怪怪的。fMP4 HLS 必须有 init 分片；没有它，分片不能播。",[32,639,641],{"id":640},"ext-x-map-与-fragmented-mp4","EXT-X-MAP 与 fragmented MP4",[40,643,646],{"className":644,"code":645,"language":45},[43],"#EXT-X-MAP:URI=\"init.mp4\"\n",[20,647,645],{"__ignoreMap":48},[11,649,650,651,654,655,658,659,662,663,666,667,670],{},"意思是分片不是 MPEG-TS——是 fragmented MP4（fMP4），",[20,652,653],{},"init.mp4"," 含解读它们所需的 ",[20,656,657],{},"ftyp"," 和 ",[20,660,661],{},"moov"," box。没有 init 分片，你手里就是裸 ",[20,664,665],{},"moof","\u002F",[20,668,669],{},"mdat"," box，没有 codec 信息。",[11,672,673],{},"合并 fMP4 HLS：",[446,675,676,682,690],{},[80,677,678,679,681],{},"拉 ",[20,680,653],{},"——这是输出文件的文件头",[80,683,684,685,666,687,689],{},"按序号顺序拼接分片的 ",[20,686,665],{},[20,688,669],{}," 对",[80,691,692,693,696],{},"想要 seek 索引的话可选重写 ",[20,694,695],{},"sidx"," box",[11,698,699],{},"这比 TS 重封装简单得多，因为 fMP4 就是为拼接设计的。苹果的 LL-HLS 现在几乎全用 fMP4。",[32,701,703],{"id":702},"low-latency-hlsll-hls","Low-Latency HLS（LL-HLS）",[11,705,706],{},"苹果 2019 年的规范扩展加了：",[77,708,709,715,721],{},[80,710,711,714],{},[20,712,713],{},"EXT-X-PART"," — 部分分片（通常 200ms 而不是 6s）",[80,716,717,720],{},[20,718,719],{},"EXT-X-PRELOAD-HINT"," — 告诉客户端开始抓还不存在的分片",[80,722,723,726],{},[20,724,725],{},"EXT-X-RENDITION-REPORT"," — 让变体 playlist 直接互相引用",[11,728,729,730,733,734,736],{},"对下载器来说，LL-HLS 主要意味着\"更多请求、更小文件\"。合并逻辑跟普通 HLS 一样——只是除了 ",[20,731,732],{},"EXTINF"," 还要处理 ",[20,735,713],{},"。FlowPick 处理得了；你自己撸的下载器不处理，就会漏掉部分分片。",[32,738,739],{"id":739},"我真见过的坑",[11,741,742,745,746,749,750,753,754,757,758,761],{},[383,743,744],{},"坑 1：相对 URI。"," M3U8 大量用相对 URI。master playlist 在 ",[20,747,748],{},"https:\u002F\u002Fcdn.example.com\u002Fv\u002Fmaster.m3u8","，里面写 ",[20,751,752],{},"v_720.m3u8","，变体就是 ",[20,755,756],{},"https:\u002F\u002Fcdn.example.com\u002Fv\u002Fv_720.m3u8","。但 master 在 ",[20,759,760],{},"https:\u002F\u002Fcdn.example.com\u002Fv\u002Fmaster.m3u8?token=abc","，token 不会自动传过去。query string 继承要手动处理。",[11,763,764],{},[383,765,766],{},"坑 2：字节范围寻址。",[40,768,771],{"className":769,"code":770,"language":45},[43],"#EXT-X-BYTERANGE:522752@1024\n",[20,772,770],{"__ignoreMap":48},[11,774,775,776,779,780,783],{},"意思是\"分片是大文件里的一个字节范围\"。不带 ",[20,777,778],{},"Range"," 头去 ",[20,781,782],{},"fetch"," 这个 URI，你会拿到整个文件。很多 CDN 托管的 HLS playlist 对归档内容这么做——不是几千个小文件，而是一个大文件加字节范围。",[11,785,786,789,790,793,794,797,798,801],{},[383,787,788],{},"坑 3：按 CODECS 选变体。"," master playlist 同时有 ",[20,791,792],{},"avc1.*","（H.264）和 ",[20,795,796],{},"hvc1.*","（HEVC）变体，你的播放器可能选 HEVC。你的下载器只抓第一个变体，可能拿到 HEVC 而你想要的是 H.264。一定让用户选，并解析 ",[20,799,800],{},"CODECS"," 给变体打标签。",[11,803,804,807],{},[383,805,806],{},"坑 4：token 过期。"," URI 里的直播 token 通常几分钟过期。一小时前抓的清单 URL 现在去下，全是 403。FlowPick 在下载时重拉清单，这就是\"先抓 URL 后下\"在直播内容上经常失败的原因。",[32,809,810],{"id":810},"参考资料",[77,812,813,822,830,841],{},[80,814,815,821],{},[14,816,820],{"href":817,"rel":818},"https:\u002F\u002Fdatatracker.ietf.org\u002Fdoc\u002Fhtml\u002Frfc8216",[819],"nofollow","RFC 8216 — HTTP Live Streaming, 2nd Edition"," — 正式规范，107 页",[80,823,824,829],{},[14,825,828],{"href":826,"rel":827},"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fhttp_live_streaming\u002Fhls_authoring_specification_for_apple_devices",[819],"Apple HLS 创作规范"," — 苹果 App Store 上架要求",[80,831,832,837,838,840],{},[14,833,836],{"href":834,"rel":835},"https:\u002F\u002Fffmpeg.org\u002Fffmpeg-formats.html#mpegts",[819],"FFmpeg MPEG-TS 格式文档"," — 理解 ",[20,839,120],{}," 文件里到底有什么",[80,842,843,848],{},[14,844,847],{"href":845,"rel":846},"https:\u002F\u002Fmp4ra.org\u002F",[819],"MP4RA — MP4 注册机构"," — CODECS 属性的 codec 到 FourCC 映射",[32,850,851],{"id":851},"小结",[11,853,854,855,857,858,857,860,857,862,857,865,868,869,874],{},"M3U8 入门叙事——\"它是个 playlist，抓分片，拼起来\"——覆盖大概 60% 的真实流。剩下 40% 涉及多音轨、加密、fMP4 init 分片、不连续性、字节范围、过期 token。一个稳健的 HLS 下载器得正确解析 ",[20,856,432],{},"、",[20,859,404],{},[20,861,623],{},[20,863,864],{},"EXT-X-BYTERANGE",[20,866,867],{},"EXT-X-DISCONTINUITY","。\"正确\"长什么样，看 ",[14,870,873],{"href":871,"rel":872},"https:\u002F\u002Fgithub.com\u002Fezwebtools\u002Fflowpick",[819],"FlowPick 源码","。",[11,876,877,878,882,883,857,886,889,890,893],{},"下一篇是 ",[14,879,881],{"href":880},"\u002Fzh-Hans\u002Fblog\u002Fdash-mpd-deep-dive-segmenttemplate-contentprotection","DASH\u002FMPD 深度篇","，讲 MPEG-DASH 对应的机制——",[20,884,885],{},"SegmentTemplate",[20,887,888],{},"$Number$"," 替换、",[20,891,892],{},"ContentProtection","，以及为什么 DASH 结构上比 HLS 复杂、实操上却更好下。",[895,896],"hr",{},[32,898,899],{"id":899},"推荐阅读",[77,901,902,910,918,926,934,943],{},[80,903,904,909],{},[383,905,906],{},[14,907,908],{"href":16},"如何下载 M3U8\u002FHLS 流：完整入门指南"," — 这篇 60% 的简单情况，那篇做基础",[80,911,912,917],{},[383,913,914],{},[14,915,916],{"href":500},"FlowPick 是怎么在浏览器里把几百个视频切片合成 MP4 的"," — FlowPick 怎么处理多音轨+视频的封装",[80,919,920,925],{},[383,921,922],{},[14,923,924],{"href":880},"DASH 深度：SegmentTemplate、ContentProtection 与多视角 MPD"," — 这篇的 DASH 对应物",[80,927,928,933],{},[383,929,930],{},[14,931,932],{"href":415},"流媒体下载到底合不合法？一份技术和法律清单"," — 为什么 FlowPick 拒绝 SAMPLE-AES \u002F DRM 内容",[80,935,936,942],{},[383,937,938],{},[14,939,941],{"href":940},"\u002Fzh-Hans\u002Fblog\u002Fbrowser-streaming-remux-mp4-isobmff","浏览器端视频重封装：fMP4、ISOBMFF 和不转码的理由"," — 容器格式深度篇",[80,944,945,951],{},[383,946,947],{},[14,948,950],{"href":949},"\u002Fzh-Hans\u002Fblog\u002Fwebcodecs-web-workers-opfs-video-processing","WebCodecs + Web Workers + OPFS：浏览器视频处理实战"," — FFmpeg WASM 不合适时的更底层浏览器 API",[953,954,955],"style",{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":48,"searchDepth":183,"depth":183,"links":957},[958,961,962,965,966,967,968,969,970,971],{"id":34,"depth":183,"text":35,"children":959},[960],{"id":68,"depth":211,"text":69},{"id":128,"depth":183,"text":129},{"id":426,"depth":183,"text":427,"children":963},[964],{"id":505,"depth":211,"text":505},{"id":547,"depth":183,"text":548},{"id":640,"depth":183,"text":641},{"id":702,"depth":183,"text":703},{"id":739,"depth":183,"text":739},{"id":810,"depth":183,"text":810},{"id":851,"depth":183,"text":851},{"id":899,"depth":183,"text":899},"tips","2026-08-04","M3U8 入门之后的进阶——EXT-X-KEY、EXT-X-MEDIA、不连续性、以及那些能让朴素 HLS 下载器翻车的边界情况。含真实清单片段和 FlowPick 的处理逻辑。","md","\u002Fscreenshots\u002Fhls-detection.png",{},true,"\u002Fzh-hans\u002Fblog\u002Fhls-m3u8-deep-dive-encryption-multitrack",16,{"title":5,"description":974},"zh-Hans\u002Fblog\u002Fhls-m3u8-deep-dive-encryption-multitrack",[984,985,986,987,988,989],"hls","m3u8","深度","加密","多音轨","串流","RzPGRJcTdvYXtJvm73i2iGuGD790tCt_uyUf4YaV2PE",[992,999],{"title":993,"path":994,"stem":995,"description":996,"date":997,"category":998,"children":-1},"如何下载 M3U8\u002FHLS 流媒体：完整入门指南","\u002Fzh-hans\u002Fblog\u002Fgetting-started-with-hls-m3u8-download","zh-Hans\u002Fblog\u002Fgetting-started-with-hls-m3u8-download","学习如何在浏览器中下载 M3U8 和 HLS 流媒体，查找播放列表地址、选择 MP4 或 TS、处理跨域和分片问题，并用 FlowPick 完成本地保存。","2026-07-01","tutorials",{"title":916,"path":1000,"stem":1001,"description":1002,"date":1003,"category":972,"children":-1},"\u002Fzh-hans\u002Fblog\u002Fhow-flowpick-merges-video-segments-in-browser","zh-Hans\u002Fblog\u002Fhow-flowpick-merges-video-segments-in-browser","不靠服务器、不装 FFmpeg、不转码——拆解 FlowPick 在浏览器标签页内完成 HLS\u002FDASH 分片合并的完整技术路径。","2026-07-02",1787670587148]