[{"data":1,"prerenderedAt":1019},["ShallowReactive",2],{"blog-en-hls-m3u8-deep-dive-encryption-multitrack":3,"blog-en-hls-m3u8-deep-dive-encryption-multitrack-surround":1007},{"id":4,"title":5,"author":6,"body":7,"category":988,"date":989,"description":990,"extension":991,"image":992,"lastModified":989,"meta":993,"navigation":994,"path":995,"readingTime":996,"seo":997,"stem":998,"tags":999,"__hash__":1006},"blog\u002Fblog\u002Fhls-m3u8-deep-dive-encryption-multitrack.md","HLS Deep Dive: Encryption, Multi-Track Audio, and the EXT-X Tags Actually Worth Knowing","FlowPick Team",{"type":8,"value":9,"toc":971},"minimark",[10,29,32,37,40,50,53,59,66,71,77,105,127,131,134,140,147,374,380,395,414,420,424,438,441,466,475,490,498,502,508,511,519,522,541,545,623,634,638,644,667,670,693,696,700,703,723,733,737,759,764,770,781,799,805,809,847,851,874,893,896,900,934,938,967],[11,12,13,14,19,20,24,25,28],"p",{},"The ",[15,16,18],"a",{"href":17},"\u002Fblog\u002Fgetting-started-with-hls-m3u8-download","beginner HLS guide"," covers \"what is M3U8, how do I download it.\" This is the other one — the one for people who've already tried to download a stream and gotten a 200KB ",[21,22,23],"code",{},".m3u8"," that references 8 audio tracks, 4 subtitle languages, and a ",[21,26,27],{},"#EXT-X-KEY"," tag that makes VLC spit out garbage.",[11,30,31],{},"If you've ever looked at a real-world HLS manifest from Netflix, Disney+, or a paid course platform and wondered why your downloader produced silent video or a corrupted file, this is why.",[33,34,36],"h2",{"id":35},"what-a-real-hls-manifest-looks-like","What a real HLS manifest looks like",[11,38,39],{},"Most tutorials use this as the example:",[41,42,47],"pre",{"className":43,"code":45,"language":46},[44],"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",[21,48,45],{"__ignoreMap":49},"",[11,51,52],{},"Cute. Real manifests don't look like this. Here's an actual master playlist from a major platform, trimmed but representative:",[41,54,57],{"className":55,"code":56,"language":46},[44],"#EXTM3U\n#EXT-X-VERSION:6\n#EXT-X-INDEPENDENT-SEGMENTS:yes\n\n# AUDIO groups\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# VIDEO variants\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",[21,58,56],{"__ignoreMap":49},[11,60,61,62,65],{},"The actual media segments aren't in here at all. The master playlist points to variant playlists, each of which points to segments. And audio lives in its own playlists. If your downloader just grabs ",[21,63,64],{},"v_1080.m3u8"," and concats the segments, you get video with no audio. This is the #1 cause of \"my HLS download has no sound\" reports.",[67,68,70],"h3",{"id":69},"the-codecs-field-matters","The CODECS field matters",[11,72,73,76],{},[21,74,75],{},"CODECS=\"avc1.640028,mp4a.40.2\""," isn't decoration. That's:",[78,79,80,99],"ul",{},[81,82,83,86,87,90,91,94,95,98],"li",{},[21,84,85],{},"avc1.640028"," — H.264 High profile, level 4.0. The first two hex digits after ",[21,88,89],{},"avc1."," encode profile (",[21,92,93],{},"64"," = High), constraint flags, and level (",[21,96,97],{},"28"," = 4.0).",[81,100,101,104],{},[21,102,103],{},"mp4a.40.2"," — AAC-LC.",[11,106,107,108,110,111,114,115,118,119,122,123,126],{},"Why this matters: if a variant says ",[21,109,85],{}," but you try to mux the segments into an MP4 with the wrong codec box (",[21,112,113],{},"hvc1"," instead of ",[21,116,117],{},"avc1","), the file won't play. Naive concatenation of ",[21,120,121],{},".ts"," segments avoids this because MPEG-TS carries its own codec info; remuxing into MP4 requires you to parse this field and write the correct ",[21,124,125],{},"stsd"," box.",[33,128,130],{"id":129},"ext-x-key-encryption-and-why-just-decrypt-it-isnt-a-sentence","EXT-X-KEY: encryption, and why \"just decrypt it\" isn't a sentence",[11,132,133],{},"When a manifest includes:",[41,135,138],{"className":136,"code":137,"language":46},[44],"#EXT-X-KEY:METHOD=AES-128,URI=\"https:\u002F\u002Fcdn.example.com\u002Fkey.bin\",IV=0x1a2b3c4d5e6f7890abcdef1234567890\n",[21,139,137],{"__ignoreMap":49},[11,141,142,143,146],{},"every subsequent segment is AES-128-CBC encrypted with the key at ",[21,144,145],{},"URI"," and the IV specified. To decrypt:",[41,148,152],{"className":149,"code":150,"language":151,"meta":49,"style":49},"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 bytes\nconst iv = new Uint8Array(16)\n\u002F\u002F IV may be literal (as above) or derived from the sequence number\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",[21,153,154,182,210,236,242,270,301,363,369],{"__ignoreMap":49},[155,156,159,163,167,171,175,179],"span",{"class":157,"line":158},"line",1,[155,160,162],{"class":161},"spNyl","const",[155,164,166],{"class":165},"sTEyZ"," keyResp ",[155,168,170],{"class":169},"sMK4o","=",[155,172,174],{"class":173},"s7zQu"," await",[155,176,178],{"class":177},"s2Zo4"," fetch",[155,180,181],{"class":165},"(keyUri)\n",[155,183,185,187,190,192,194,197,200,203,206],{"class":157,"line":184},2,[155,186,162],{"class":161},[155,188,189],{"class":165}," keyBytes ",[155,191,170],{"class":169},[155,193,174],{"class":173},[155,195,196],{"class":165}," keyResp",[155,198,199],{"class":169},".",[155,201,202],{"class":177},"arrayBuffer",[155,204,205],{"class":165},"()  ",[155,207,209],{"class":208},"sHwdD","\u002F\u002F 16 bytes\n",[155,211,213,215,218,220,223,226,229,233],{"class":157,"line":212},3,[155,214,162],{"class":161},[155,216,217],{"class":165}," iv ",[155,219,170],{"class":169},[155,221,222],{"class":169}," new",[155,224,225],{"class":177}," Uint8Array",[155,227,228],{"class":165},"(",[155,230,232],{"class":231},"sbssI","16",[155,234,235],{"class":165},")\n",[155,237,239],{"class":157,"line":238},4,[155,240,241],{"class":208},"\u002F\u002F IV may be literal (as above) or derived from the sequence number\n",[155,243,245,247,250,252,254,257,259,262,264,267],{"class":157,"line":244},5,[155,246,162],{"class":161},[155,248,249],{"class":165}," decrypted ",[155,251,170],{"class":169},[155,253,174],{"class":173},[155,255,256],{"class":165}," crypto",[155,258,199],{"class":169},[155,260,261],{"class":165},"subtle",[155,263,199],{"class":169},[155,265,266],{"class":177},"decrypt",[155,268,269],{"class":165},"(\n",[155,271,273,276,280,283,286,290,293,296,298],{"class":157,"line":272},6,[155,274,275],{"class":169},"  {",[155,277,279],{"class":278},"swJcz"," name",[155,281,282],{"class":169},":",[155,284,285],{"class":169}," '",[155,287,289],{"class":288},"sfazB","AES-CBC",[155,291,292],{"class":169},"'",[155,294,295],{"class":169},",",[155,297,217],{"class":165},[155,299,300],{"class":169},"},\n",[155,302,304,307,309,311,313,315,318,320,322,325,327,329,332,334,336,338,340,342,346,348,351,353,355,357,360],{"class":157,"line":303},7,[155,305,306],{"class":173},"  await",[155,308,256],{"class":165},[155,310,199],{"class":169},[155,312,261],{"class":165},[155,314,199],{"class":169},[155,316,317],{"class":177},"importKey",[155,319,228],{"class":165},[155,321,292],{"class":169},[155,323,324],{"class":288},"raw",[155,326,292],{"class":169},[155,328,295],{"class":169},[155,330,331],{"class":165}," keyBytes",[155,333,295],{"class":169},[155,335,285],{"class":169},[155,337,289],{"class":288},[155,339,292],{"class":169},[155,341,295],{"class":169},[155,343,345],{"class":344},"sfNiH"," false",[155,347,295],{"class":169},[155,349,350],{"class":165}," [",[155,352,292],{"class":169},[155,354,266],{"class":288},[155,356,292],{"class":169},[155,358,359],{"class":165},"])",[155,361,362],{"class":169},",\n",[155,364,366],{"class":157,"line":365},8,[155,367,368],{"class":165},"  encryptedSegment\n",[155,370,372],{"class":157,"line":371},9,[155,373,235],{"class":165},[11,375,376,379],{},[21,377,378],{},"crypto.subtle.decrypt"," is the right tool in-browser — no need for a WASM AES implementation, the native WebCrypto is both faster and constant-time.",[11,381,382,383,390,391,394],{},"Here's the part tutorials skip: ",[384,385,386,389],"strong",{},[21,387,388],{},"METHOD=AES-128"," is the easy case."," Most major platforms use ",[21,392,393],{},"METHOD=SAMPLE-AES",", which is Apple's FairPlay DRM-adjacent scheme. With SAMPLE-AES:",[78,396,397,400,407],{},[81,398,399],{},"Only part of each segment is encrypted (the video NAL units, not the audio or the PES headers).",[81,401,402,403,406],{},"The key URI usually returns an ",[21,404,405],{},"EXT-X-KEY"," response that's wrapped in FairPlay's license exchange — not a raw 16-byte key.",[81,408,409,410,199],{},"You cannot decrypt this in a browser tab. Period. This is DRM, and it's the reason \"FlowPick can't download this Netflix video\" is a feature, not a bug. We covered the legal and technical line in ",[15,411,413],{"href":412},"\u002Fblog\u002Fis-it-legal-to-download-streaming-video","the streaming download legality guide",[11,415,416,419],{},[384,417,418],{},"Original opinion:"," If a downloader claims to handle FairPlay, Widevine, or PlayReady, it's either lying or it's breaking the law in most jurisdictions. FlowPick explicitly refuses to touch SAMPLE-AES content. The right behavior is to fail loudly, not silently produce a broken file.",[33,421,423],{"id":422},"ext-x-media-audio-and-subtitle-tracks","EXT-X-MEDIA: audio and subtitle tracks",[11,425,13,426,429,430,433,434,437],{},[21,427,428],{},"EXT-X-MEDIA"," tag is how HLS does multi-track audio. Each ",[21,431,432],{},"GROUP-ID"," is a set of interchangeable audio renditions; the variant playlist's ",[21,435,436],{},"AUDIO"," attribute points to which group it uses.",[11,439,440],{},"The implication for downloaders: a \"complete download\" isn't just the video segments. It's:",[442,443,444,447,457,463],"ol",{},[81,445,446],{},"Video segments from the chosen variant playlist",[81,448,449,450,452,453,456],{},"Audio segments from one ",[21,451,428],{}," entry (usually ",[21,454,455],{},"DEFAULT=YES",", but you should let users pick)",[81,458,459,460],{},"Optionally, subtitle segments from ",[21,461,462],{},"TYPE=SUBTITLES",[81,464,465],{},"Mux them into a single MP4 with proper track metadata",[11,467,468,469,471,472,474],{},"The muxing step is where it gets spicy. MPEG-TS segments from different playlists have their own PTS timelines; when you concatenate video-only ",[21,470,121],{}," and audio-only ",[21,473,121],{}," into a single MP4, you have to:",[78,476,477,480,483],{},[81,478,479],{},"Parse PES headers from each TS packet to extract PTS",[81,481,482],{},"Sort packets by PTS",[81,484,485,486,489],{},"Write them into an MP4 with the correct ",[21,487,488],{},"mdia\u002Fminf\u002Fstbl"," structure for each track",[11,491,492,493,497],{},"This is what FlowPick's WebAssembly FFmpeg handles — see ",[15,494,496],{"href":495},"\u002Fblog\u002Fhow-flowpick-merges-video-segments-in-browser","the in-browser merge deep dive",". The point is: \"concat the files\" stops working the moment you have separate audio.",[67,499,501],{"id":500},"the-discontinuity-tag","The discontinuity tag",[41,503,506],{"className":504,"code":505,"language":46},[44],"#EXT-X-DISCONTINUITY\n",[21,507,505],{"__ignoreMap":49},[11,509,510],{},"This means \"the timestamps reset after this point.\" Common in:",[78,512,513,516],{},[81,514,515],{},"Live streams with ad breaks (the ad has its own timeline)",[81,517,518],{},"Stitched playlists where segments come from different encoders",[11,520,521],{},"A downloader that just concatenates segments will produce a file where the player jumps backward in time at the discontinuity. You have to either:",[442,523,524,527,530],{},[81,525,526],{},"Use a remuxer that handles discontinuities (FFmpeg does)",[81,528,529],{},"Strip the discontinuity and accept timestamp drift",[81,531,532,533,535,536,540],{},"Skip ad segments entirely (FlowPick's default, since they're usually 2-second ",[21,534,121],{}," files polluting the stream — see the ",[15,537,539],{"href":538},"\u002Fblog\u002Fflowpick-v1-0-0-first-public-release","v1.0.0 release notes"," for the size filter that catches these)",[33,542,544],{"id":543},"ext-x-version-the-version-tags-that-matter","EXT-X-VERSION: the version tags that matter",[546,547,548,564],"table",{},[549,550,551],"thead",{},[552,553,554,558,561],"tr",{},[555,556,557],"th",{},"Version",[555,559,560],{},"What it added",[555,562,563],{},"Why you care",[565,566,567,579,590,601,612],"tbody",{},[552,568,569,573,576],{},[570,571,572],"td",{},"2",[570,574,575],{},"I-frame playlists",[570,577,578],{},"Trick mode (fast-forward\u002Frewind) support",[552,580,581,584,587],{},[570,582,583],{},"3",[570,585,586],{},"Floating-point EXTINF",[570,588,589],{},"Sub-second durations for low-latency streams",[552,591,592,595,598],{},[570,593,594],{},"4",[570,596,597],{},"EXT-X-MEDIA for independent audio\u002Fsubtitles",[570,599,600],{},"Multi-track audio — see above",[552,602,603,606,609],{},[570,604,605],{},"5",[570,607,608],{},"EXT-X-KEY IV optional",[570,610,611],{},"Keys can derive IV from sequence number",[552,613,614,617,620],{},[570,615,616],{},"6",[570,618,619],{},"EXT-X-MAP",[570,621,622],{},"Initialization segment for fMP4 — see next section",[11,624,625,626,629,630,633],{},"If you see ",[21,627,628],{},"#EXT-X-VERSION:6"," and there's no ",[21,631,632],{},"#EXT-X-MAP"," in the variant playlist, something's weird. fMP4 HLS requires the init segment; without it, the segments are unplayable.",[33,635,637],{"id":636},"ext-x-map-and-fragmented-mp4","EXT-X-MAP and fragmented MP4",[41,639,642],{"className":640,"code":641,"language":46},[44],"#EXT-X-MAP:URI=\"init.mp4\"\n",[21,643,641],{"__ignoreMap":49},[11,645,646,647,650,651,654,655,658,659,662,663,666],{},"This means segments aren't MPEG-TS — they're fragmented MP4 (fMP4), and ",[21,648,649],{},"init.mp4"," contains the ",[21,652,653],{},"ftyp"," and ",[21,656,657],{},"moov"," boxes needed to interpret them. Without the init segment, you have raw ",[21,660,661],{},"moof","\u002F",[21,664,665],{},"mdat"," boxes with no codec information.",[11,668,669],{},"To merge fMP4 HLS:",[442,671,672,678,686],{},[81,673,674,675,677],{},"Fetch ",[21,676,649],{}," — this is your output file's header",[81,679,680,681,662,683,685],{},"Concatenate segment ",[21,682,661],{},[21,684,665],{}," pairs in sequence order",[81,687,688,689,692],{},"Optionally rewrite the ",[21,690,691],{},"sidx"," box if you want a seek index",[11,694,695],{},"This is dramatically easier than TS remuxing because fMP4 was designed for concatenation. Apple's LL-HLS uses fMP4 almost exclusively now.",[33,697,699],{"id":698},"low-latency-hls-ll-hls","Low-Latency HLS (LL-HLS)",[11,701,702],{},"Apple's 2019 spec extension adds:",[78,704,705,711,717],{},[81,706,707,710],{},[21,708,709],{},"EXT-X-PART"," — partial segments (typically 200ms instead of 6s)",[81,712,713,716],{},[21,714,715],{},"EXT-X-PRELOAD-HINT"," — tells the client to start fetching a segment that doesn't exist yet",[81,718,719,722],{},[21,720,721],{},"EXT-X-RENDITION-REPORT"," — lets variant playlists reference each other directly",[11,724,725,726,728,729,732],{},"For a downloader, LL-HLS mostly means \"more requests, smaller files.\" The merge logic is identical to regular HLS — you just have to handle ",[21,727,709],{}," tags in addition to ",[21,730,731],{},"EXTINF",". FlowPick does; if your hand-rolled downloader doesn't, you'll miss partial segments entirely.",[33,734,736],{"id":735},"common-pitfalls-ive-actually-seen","Common pitfalls I've actually seen",[11,738,739,742,743,746,747,750,751,754,755,758],{},[384,740,741],{},"Pitfall 1: relative URIs."," M3U8s use relative URIs extensively. If the master playlist is at ",[21,744,745],{},"https:\u002F\u002Fcdn.example.com\u002Fv\u002Fmaster.m3u8"," and contains ",[21,748,749],{},"v_720.m3u8",", the variant is at ",[21,752,753],{},"https:\u002F\u002Fcdn.example.com\u002Fv\u002Fv_720.m3u8",". But if the master is at ",[21,756,757],{},"https:\u002F\u002Fcdn.example.com\u002Fv\u002Fmaster.m3u8?token=abc",", the token doesn't automatically propagate. You need to handle query string inheritance manually.",[11,760,761],{},[384,762,763],{},"Pitfall 2: byte-range addressing.",[41,765,768],{"className":766,"code":767,"language":46},[44],"#EXT-X-BYTERANGE:522752@1024\n",[21,769,767],{"__ignoreMap":49},[11,771,772,773,776,777,780],{},"This means \"the segment is a range of bytes within a larger file.\" If you ",[21,774,775],{},"fetch"," the URI without the ",[21,778,779],{},"Range"," header, you get the whole file. Many CDN-hosted HLS playlists use this for archive content — instead of thousands of tiny files, they have one big file with byte ranges.",[11,782,783,786,787,790,791,794,795,798],{},[384,784,785],{},"Pitfall 3: variant selection by CODECS."," If a master playlist has both ",[21,788,789],{},"avc1.*"," (H.264) and ",[21,792,793],{},"hvc1.*"," (HEVC) variants, your player might pick HEVC. If your downloader just grabs the first variant, you might get HEVC when you wanted H.264. Always let the user choose, and parse ",[21,796,797],{},"CODECS"," to label variants.",[11,800,801,804],{},[384,802,803],{},"Pitfall 4: expired tokens."," Live stream tokens in the URI often expire in minutes. If you capture a manifest URL and try to download it an hour later, you get 403s. FlowPick re-fetches manifests at download time, which is why \"capture then download\" sometimes fails on live content.",[33,806,808],{"id":807},"references","References",[78,810,811,820,828,839],{},[81,812,813,819],{},[15,814,818],{"href":815,"rel":816},"https:\u002F\u002Fdatatracker.ietf.org\u002Fdoc\u002Fhtml\u002Frfc8216",[817],"nofollow","RFC 8216 — HTTP Live Streaming, 2nd Edition"," — The actual spec, all 107 pages",[81,821,822,827],{},[15,823,826],{"href":824,"rel":825},"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fhttp_live_streaming\u002Fhls_authoring_specification_for_apple_devices",[817],"Apple's HLS Authoring Specification"," — What Apple requires for App Store approval",[81,829,830,835,836,838],{},[15,831,834],{"href":832,"rel":833},"https:\u002F\u002Fffmpeg.org\u002Fffmpeg-formats.html#mpegts",[817],"FFmpeg MPEG-TS format docs"," — Useful for understanding what's actually in a ",[21,837,121],{}," file",[81,840,841,846],{},[15,842,845],{"href":843,"rel":844},"https:\u002F\u002Fmp4ra.org\u002F",[817],"MP4RA — MP4 Registration Authority"," — Codec-to-FourCC mappings for the CODECS attribute",[33,848,850],{"id":849},"summary","Summary",[11,852,853,854,856,857,856,859,856,861,864,865,868,869,199],{},"The beginner M3U8 story — \"it's a playlist, fetch the segments, concat them\" — covers maybe 60% of real streams. The other 40% involves multi-track audio, encryption, fMP4 init segments, discontinuities, byte ranges, and tokens that expire. A robust HLS downloader has to parse all of ",[21,855,428],{},", ",[21,858,405],{},[21,860,619],{},[21,862,863],{},"EXT-X-BYTERANGE",", and ",[21,866,867],{},"EXT-X-DISCONTINUITY"," correctly. If you want to see what \"correctly\" looks like in practice, ",[15,870,873],{"href":871,"rel":872},"https:\u002F\u002Fgithub.com\u002Fezwebtools\u002Fflowpick",[817],"FlowPick's open source",[11,875,876,877,881,882,856,885,888,889,892],{},"The next post in this series is the ",[15,878,880],{"href":879},"\u002Fblog\u002Fdash-mpd-deep-dive-segmenttemplate-contentprotection","DASH\u002FMPD deep dive",", which covers the equivalent machinery for MPEG-DASH — ",[21,883,884],{},"SegmentTemplate",[21,886,887],{},"$Number$"," substitution, ",[21,890,891],{},"ContentProtection",", and why DASH is structurally more complex than HLS but practically easier to download.",[894,895],"hr",{},[33,897,899],{"id":898},"related-articles","Related articles",[78,901,902,910,918,926],{},[81,903,904,909],{},[384,905,906],{},[15,907,908],{"href":17},"How to Download M3U8\u002FHLS Streams: Beginner's Guide"," — The 60% case this article builds past",[81,911,912,917],{},[384,913,914],{},[15,915,916],{"href":495},"How FlowPick Merges Hundreds of Video Segments in Your Browser"," — How FlowPick handles the multi-track audio + video muxing problem",[81,919,920,925],{},[384,921,922],{},[15,923,924],{"href":879},"DASH Deep Dive: SegmentTemplate, ContentProtection, and Multi-View"," — The DASH counterpart to this article",[81,927,928,933],{},[384,929,930],{},[15,931,932],{"href":412},"Is It Legal to Download Streaming Video?"," — Why FlowPick refuses SAMPLE-AES \u002F DRM content",[33,935,937],{"id":936},"recommended-reading","Recommended reading",[78,939,940,949,958],{},[81,941,942,948],{},[384,943,944],{},[15,945,947],{"href":946},"\u002Fblog\u002Fwhat-is-dash-mpd-streaming","What Is DASH Streaming? MPD File Explained"," — The DASH beginner guide",[81,950,951,957],{},[384,952,953],{},[15,954,956],{"href":955},"\u002Fblog\u002Fbrowser-streaming-remux-mp4-isobmff","Browser-Side Video Remuxing: fMP4, ISOBMFF, and Why We Don't Transcode"," — The container format deep dive",[81,959,960,966],{},[384,961,962],{},[15,963,965],{"href":964},"\u002Fblog\u002Fwebcodecs-web-workers-opfs-video-processing","WebCodecs + Web Workers + OPFS: Practical Video Processing in the Browser"," — The lower-level browser APIs for when FFmpeg WASM isn't the right tool",[968,969,970],"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":49,"searchDepth":184,"depth":184,"links":972},[973,976,977,980,981,982,983,984,985,986,987],{"id":35,"depth":184,"text":36,"children":974},[975],{"id":69,"depth":212,"text":70},{"id":129,"depth":184,"text":130},{"id":422,"depth":184,"text":423,"children":978},[979],{"id":500,"depth":212,"text":501},{"id":543,"depth":184,"text":544},{"id":636,"depth":184,"text":637},{"id":698,"depth":184,"text":699},{"id":735,"depth":184,"text":736},{"id":807,"depth":184,"text":808},{"id":849,"depth":184,"text":850},{"id":898,"depth":184,"text":899},{"id":936,"depth":184,"text":937},"tips","2026-08-04","Past the beginner M3U8 guide — EXT-X-KEY, EXT-X-MEDIA, discontinuities, and the corner cases that break naive HLS downloaders. With real manifest snippets and the logic FlowPick uses to handle them.","md","\u002Fscreenshots\u002Fhls-detection.png",{},true,"\u002Fblog\u002Fhls-m3u8-deep-dive-encryption-multitrack",16,{"title":5,"description":990},"blog\u002Fhls-m3u8-deep-dive-encryption-multitrack",[1000,1001,1002,1003,1004,1005],"hls","m3u8","deep-dive","encryption","multi-track","streaming","BofKp9AzlMbReZkJL19f19RMF8B2z4jGSZjP6dG19xE",[1008,1014],{"title":1009,"path":17,"stem":1010,"description":1011,"date":1012,"category":1013,"children":-1},"How to Download M3U8\u002FHLS Streams: A Complete Beginner's Guide","blog\u002Fgetting-started-with-hls-m3u8-download","Learn how to download M3U8 and HLS streams in a browser, find a playlist URL, choose MP4 or TS, and troubleshoot common download failures with FlowPick.","2026-07-01","tutorials",{"title":1015,"path":495,"stem":1016,"description":1017,"date":1018,"category":988,"children":-1},"How FlowPick Merges Hundreds of Video Segments into MP4 in the Browser","blog\u002Fhow-flowpick-merges-video-segments-in-browser","No server, no FFmpeg install, no transcoding — a full technical breakdown of how FlowPick handles HLS\u002FDASH segment merging entirely within a browser tab.","2026-07-02",1787670588696]