[{"data":1,"prerenderedAt":2511},["ShallowReactive",2],{"blog-ko-hls-m3u8-deep-dive-encryption-multitrack":3,"blog-ko-hls-m3u8-deep-dive-encryption-multitrack-surround":2499},{"id":4,"title":5,"author":6,"body":7,"category":2483,"date":2484,"description":2485,"extension":2486,"image":2487,"lastModified":2484,"meta":2488,"navigation":377,"path":2489,"readingTime":434,"seo":2490,"stem":2491,"tags":2492,"__hash__":2498},"blog_ko\u002Fko\u002Fblog\u002Fhls-m3u8-deep-dive-encryption-multitrack.md","HLS 딥: 암호화, 멀티트랙, 기억해야 할 EXT-X 태그","FlowPick 팀",{"type":8,"value":9,"toc":2461},"minimark",[10,24,27,32,35,62,132,150,153,224,227,472,475,492,593,606,610,615,657,663,667,677,688,695,698,737,755,758,778,783,798,809,820,823,830,867,872,875,878,902,908,926,929,932,939,958,983,991,994,1000,1018,1025,1031,1037,1046,1067,1075,1080,1098,1101,1105,1153,1171,1173,1191,1195,1226,1233,1270,1276,1279,1288,1298,1302,2364,2378,2381,2384,2397,2400,2404,2457],[11,12,13,18,19,23],"p",{},[14,15,17],"a",{"href":16},"\u002Fko\u002Fblog\u002Fgetting-started-with-hls-m3u8-download","HLS\u002FM3U8 입문 가이드","는 3분짜리 짧은 영상의 M3U8을 파싱하는 \"나쁘지 않은\" 케이스를 다뤘다. 이 글은 그 다음: 30개 체인 마스터 플레이리스트, 다중 오디오\u002F자막 트랙, AES-128 암호화, 라이브의 ",[20,21,22],"code",{},"EXT-X-DISCONTINUITY",", 그리고 1000라인짜리 M3U8이 작성되는 이유를 알고 싶은 사람을 위한 것.",[11,25,26],{},"다운로더를 만들고 있거나, \"이 사이트는 되는데 저 사이트는 안 되는\" 이유를 알고 싶다면——이 글이 답이다.",[28,29,31],"h2",{"id":30},"재미있는-부분-암호화","재미있는 부분: 암호화",[11,33,34],{},"HLS의 암호화는 3단계로 이루어진다:",[36,37,38,50,56],"ol",{},[39,40,41,45,46,49],"li",{},[42,43,44],"strong",{},"키 페치","——",[20,47,48],{},"#EXT-X-KEY"," 태그가 키 URL을 가리킴",[39,51,52,55],{},[42,53,54],{},"IV","——초기화 벡터, 태그가 명시하거나 미디어 시퀀스 번호에서 유도",[39,57,58,61],{},[42,59,60],{},"복호화","——각 TS 세그먼트는 AES-128-CBC로 암호화, 키+IV로 해독",[63,64,69],"pre",{"className":65,"code":66,"language":67,"meta":68,"style":68},"language-m3u8 shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:6\n#EXT-X-MEDIA-SEQUENCE:1\n#EXT-X-KEY:METHOD=AES-128,URI=\"https:\u002F\u002Fexample.com\u002Fkey.bin\",IV=0x1234567890abcdef1234567890abcdef\n#EXTINF:6.0,\nsegment-1.ts\n#EXTINF:6.0,\nsegment-2.ts\n#EXT-X-ENDLIST\n","m3u8","",[20,70,71,79,85,91,97,103,109,115,120,126],{"__ignoreMap":68},[72,73,76],"span",{"class":74,"line":75},"line",1,[72,77,78],{},"#EXTM3U\n",[72,80,82],{"class":74,"line":81},2,[72,83,84],{},"#EXT-X-VERSION:3\n",[72,86,88],{"class":74,"line":87},3,[72,89,90],{},"#EXT-X-TARGETDURATION:6\n",[72,92,94],{"class":74,"line":93},4,[72,95,96],{},"#EXT-X-MEDIA-SEQUENCE:1\n",[72,98,100],{"class":74,"line":99},5,[72,101,102],{},"#EXT-X-KEY:METHOD=AES-128,URI=\"https:\u002F\u002Fexample.com\u002Fkey.bin\",IV=0x1234567890abcdef1234567890abcdef\n",[72,104,106],{"class":74,"line":105},6,[72,107,108],{},"#EXTINF:6.0,\n",[72,110,112],{"class":74,"line":111},7,[72,113,114],{},"segment-1.ts\n",[72,116,118],{"class":74,"line":117},8,[72,119,108],{},[72,121,123],{"class":74,"line":122},9,[72,124,125],{},"segment-2.ts\n",[72,127,129],{"class":74,"line":128},10,[72,130,131],{},"#EXT-X-ENDLIST\n",[11,133,134,137,138,141,142,145,146,149],{},[20,135,136],{},"METHOD=AES-128","이 표준. ",[20,139,140],{},"NONE","도 있고(",[20,143,144],{},"#EXT-X-KEY:METHOD=NONE",", \"이후 세그먼트는 암호화 안 함\"을 의미) ",[20,147,148],{},"SAMPLE-AES","(Apple의 특별 포맷, FairPlay와 짝)도 있다.",[11,151,152],{},"키를 페치하려면:",[63,154,158],{"className":155,"code":156,"language":157,"meta":68,"style":68},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","const keyResponse = await fetch(keyUrl)\nconst keyBytes = new Uint8Array(await keyResponse.arrayBuffer())\n\u002F\u002F keyBytes는 정확히 16바이트 (AES-128)\n","js",[20,159,160,185,218],{"__ignoreMap":68},[72,161,162,166,170,174,178,182],{"class":74,"line":75},[72,163,165],{"class":164},"spNyl","const",[72,167,169],{"class":168},"sTEyZ"," keyResponse ",[72,171,173],{"class":172},"sMK4o","=",[72,175,177],{"class":176},"s7zQu"," await",[72,179,181],{"class":180},"s2Zo4"," fetch",[72,183,184],{"class":168},"(keyUrl)\n",[72,186,187,189,192,194,197,200,203,206,209,212,215],{"class":74,"line":81},[72,188,165],{"class":164},[72,190,191],{"class":168}," keyBytes ",[72,193,173],{"class":172},[72,195,196],{"class":172}," new",[72,198,199],{"class":180}," Uint8Array",[72,201,202],{"class":168},"(",[72,204,205],{"class":176},"await",[72,207,208],{"class":168}," keyResponse",[72,210,211],{"class":172},".",[72,213,214],{"class":180},"arrayBuffer",[72,216,217],{"class":168},"())\n",[72,219,220],{"class":74,"line":87},[72,221,223],{"class":222},"sHwdD","\u002F\u002F keyBytes는 정확히 16바이트 (AES-128)\n",[11,225,226],{},"그리고 세그먼트를 복호화:",[63,228,230],{"className":155,"code":229,"language":157,"meta":68,"style":68},"async function decryptSegment(segmentData, keyBytes, iv) {\n  \u002F\u002F WebCrypto API 사용\n  const cryptoKey = await crypto.subtle.importKey(\n    'raw',\n    keyBytes,\n    { name: 'AES-CBC' },\n    false,\n    ['decrypt']\n  )\n\n  const decrypted = await crypto.subtle.decrypt(\n    { name: 'AES-CBC', iv },\n    cryptoKey,\n    segmentData\n  )\n\n  return new Uint8Array(decrypted)\n}\n",[20,231,232,266,271,301,316,323,345,353,368,373,379,403,424,432,438,443,448,466],{"__ignoreMap":68},[72,233,234,237,240,243,245,249,252,255,257,260,263],{"class":74,"line":75},[72,235,236],{"class":164},"async",[72,238,239],{"class":164}," function",[72,241,242],{"class":180}," decryptSegment",[72,244,202],{"class":172},[72,246,248],{"class":247},"sHdIc","segmentData",[72,250,251],{"class":172},",",[72,253,254],{"class":247}," keyBytes",[72,256,251],{"class":172},[72,258,259],{"class":247}," iv",[72,261,262],{"class":172},")",[72,264,265],{"class":172}," {\n",[72,267,268],{"class":74,"line":81},[72,269,270],{"class":222},"  \u002F\u002F WebCrypto API 사용\n",[72,272,273,276,279,282,284,287,289,292,294,297],{"class":74,"line":87},[72,274,275],{"class":164},"  const",[72,277,278],{"class":168}," cryptoKey",[72,280,281],{"class":172}," =",[72,283,177],{"class":176},[72,285,286],{"class":168}," crypto",[72,288,211],{"class":172},[72,290,291],{"class":168},"subtle",[72,293,211],{"class":172},[72,295,296],{"class":180},"importKey",[72,298,300],{"class":299},"swJcz","(\n",[72,302,303,306,310,313],{"class":74,"line":93},[72,304,305],{"class":172},"    '",[72,307,309],{"class":308},"sfazB","raw",[72,311,312],{"class":172},"'",[72,314,315],{"class":172},",\n",[72,317,318,321],{"class":74,"line":99},[72,319,320],{"class":168},"    keyBytes",[72,322,315],{"class":172},[72,324,325,328,331,334,337,340,342],{"class":74,"line":105},[72,326,327],{"class":172},"    {",[72,329,330],{"class":299}," name",[72,332,333],{"class":172},":",[72,335,336],{"class":172}," '",[72,338,339],{"class":308},"AES-CBC",[72,341,312],{"class":172},[72,343,344],{"class":172}," },\n",[72,346,347,351],{"class":74,"line":111},[72,348,350],{"class":349},"sfNiH","    false",[72,352,315],{"class":172},[72,354,355,358,360,363,365],{"class":74,"line":117},[72,356,357],{"class":299},"    [",[72,359,312],{"class":172},[72,361,362],{"class":308},"decrypt",[72,364,312],{"class":172},[72,366,367],{"class":299},"]\n",[72,369,370],{"class":74,"line":122},[72,371,372],{"class":299},"  )\n",[72,374,375],{"class":74,"line":128},[72,376,378],{"emptyLinePlaceholder":377},true,"\n",[72,380,382,384,387,389,391,393,395,397,399,401],{"class":74,"line":381},11,[72,383,275],{"class":164},[72,385,386],{"class":168}," decrypted",[72,388,281],{"class":172},[72,390,177],{"class":176},[72,392,286],{"class":168},[72,394,211],{"class":172},[72,396,291],{"class":168},[72,398,211],{"class":172},[72,400,362],{"class":180},[72,402,300],{"class":299},[72,404,406,408,410,412,414,416,418,420,422],{"class":74,"line":405},12,[72,407,327],{"class":172},[72,409,330],{"class":299},[72,411,333],{"class":172},[72,413,336],{"class":172},[72,415,339],{"class":308},[72,417,312],{"class":172},[72,419,251],{"class":172},[72,421,259],{"class":168},[72,423,344],{"class":172},[72,425,427,430],{"class":74,"line":426},13,[72,428,429],{"class":168},"    cryptoKey",[72,431,315],{"class":172},[72,433,435],{"class":74,"line":434},14,[72,436,437],{"class":168},"    segmentData\n",[72,439,441],{"class":74,"line":440},15,[72,442,372],{"class":299},[72,444,446],{"class":74,"line":445},16,[72,447,378],{"emptyLinePlaceholder":377},[72,449,451,454,456,458,460,463],{"class":74,"line":450},17,[72,452,453],{"class":176},"  return",[72,455,196],{"class":172},[72,457,199],{"class":180},[72,459,202],{"class":299},[72,461,462],{"class":168},"decrypted",[72,464,465],{"class":299},")\n",[72,467,469],{"class":74,"line":468},18,[72,470,471],{"class":172},"}\n",[11,473,474],{},"IV 처리는 두 가지 패턴:",[36,476,477,486],{},[39,478,479,481,482,485],{},[20,480,48],{},"에 ",[20,483,484],{},"IV=0x...","가 있으면 그걸 사용",[39,487,488,489],{},"없으면 미디어 시퀀스 번호를 16바이트로 패딩——",[20,490,491],{},"[0, 0, 0, ..., 0, seqNum]",[63,493,495],{"className":155,"code":494,"language":157,"meta":68,"style":68},"function deriveIV(sequenceNumber) {\n  const iv = new Uint8Array(16)\n  const view = new DataView(iv.buffer)\n  view.setUint32(12, sequenceNumber)\n  return iv\n}\n",[20,496,497,514,534,560,582,589],{"__ignoreMap":68},[72,498,499,502,505,507,510,512],{"class":74,"line":75},[72,500,501],{"class":164},"function",[72,503,504],{"class":180}," deriveIV",[72,506,202],{"class":172},[72,508,509],{"class":247},"sequenceNumber",[72,511,262],{"class":172},[72,513,265],{"class":172},[72,515,516,518,520,522,524,526,528,532],{"class":74,"line":81},[72,517,275],{"class":164},[72,519,259],{"class":168},[72,521,281],{"class":172},[72,523,196],{"class":172},[72,525,199],{"class":180},[72,527,202],{"class":299},[72,529,531],{"class":530},"sbssI","16",[72,533,465],{"class":299},[72,535,536,538,541,543,545,548,550,553,555,558],{"class":74,"line":87},[72,537,275],{"class":164},[72,539,540],{"class":168}," view",[72,542,281],{"class":172},[72,544,196],{"class":172},[72,546,547],{"class":180}," DataView",[72,549,202],{"class":299},[72,551,552],{"class":168},"iv",[72,554,211],{"class":172},[72,556,557],{"class":168},"buffer",[72,559,465],{"class":299},[72,561,562,565,567,570,572,575,577,580],{"class":74,"line":93},[72,563,564],{"class":168},"  view",[72,566,211],{"class":172},[72,568,569],{"class":180},"setUint32",[72,571,202],{"class":299},[72,573,574],{"class":530},"12",[72,576,251],{"class":172},[72,578,579],{"class":168}," sequenceNumber",[72,581,465],{"class":299},[72,583,584,586],{"class":74,"line":99},[72,585,453],{"class":176},[72,587,588],{"class":168}," iv\n",[72,590,591],{"class":74,"line":105},[72,592,471],{"class":172},[11,594,595,598,599,45,602,605],{},[42,596,597],{},"핵심 포인트:"," 키 URL은 보통 토큰화되어 있고, 페치 시 쿠키나 헤더 인증이 필요하다. ",[20,600,601],{},"fetch(keyUrl, { credentials: 'include' })",[20,603,604],{},"include","가 필수, 브라우저는 기본적으로 서드파티 쿠키를 보내지 않는다.",[28,607,609],{"id":608},"hls의-키-로테이션","HLS의 \"키 로테이션\"",[11,611,612,614],{},[20,613,48],{},"는 언제든 다시 나올 수 있다:",[63,616,618],{"className":65,"code":617,"language":67,"meta":68,"style":68},"#EXT-X-KEY:METHOD=AES-128,URI=\"key1.bin\",IV=0x...\n#EXTINF:6.0,\nsegment-1.ts   # key1로 암호화\n#EXTINF:6.0,\nsegment-2.ts   # key1로 암호화\n#EXT-X-KEY:METHOD=AES-128,URI=\"key2.bin\",IV=0x...\n#EXTINF:6.0,\nsegment-3.ts   # key2로 암호화\n",[20,619,620,625,629,634,638,643,648,652],{"__ignoreMap":68},[72,621,622],{"class":74,"line":75},[72,623,624],{},"#EXT-X-KEY:METHOD=AES-128,URI=\"key1.bin\",IV=0x...\n",[72,626,627],{"class":74,"line":81},[72,628,108],{},[72,630,631],{"class":74,"line":87},[72,632,633],{},"segment-1.ts   # key1로 암호화\n",[72,635,636],{"class":74,"line":93},[72,637,108],{},[72,639,640],{"class":74,"line":99},[72,641,642],{},"segment-2.ts   # key1로 암호화\n",[72,644,645],{"class":74,"line":105},[72,646,647],{},"#EXT-X-KEY:METHOD=AES-128,URI=\"key2.bin\",IV=0x...\n",[72,649,650],{"class":74,"line":111},[72,651,108],{},[72,653,654],{"class":74,"line":117},[72,655,656],{},"segment-3.ts   # key2로 암호화\n",[11,658,659,660,662],{},"다운로더는 \"현재 키\" 상태를 추적해야 한다. 새 ",[20,661,48],{},"를 만나면 새 키를 페치, 이후 세그먼트는 새 키로 복호화. 파서를 쓸 때 이 상태 기계가 모든 곳에 들어있어야 한다.",[28,664,666],{"id":665},"sample-aes와-fairplay","SAMPLE-AES와 FairPlay",[11,668,669,672,673,676],{},[20,670,671],{},"METHOD=SAMPLE-AES","는 Apple의 FairPlay DRM과 짝. TS 패킷의 일부——보통 H.264 NAL unit의 슬라이스 데이터——만 암호화, 나머지는 평문. 키 획득은 ",[20,674,675],{},"skd:\u002F\u002F"," URL을 통해 FairPlay의 CDM을 사용.",[11,678,679,682,683,687],{},[42,680,681],{},"이건 DRM이다."," FairPlay는 Apple 독점. 브라우저에서 복호화하려면 EME(Encrypted Media Extensions)를 통해, 그것도 Safari에서만. FlowPick은 SAMPLE-AES를 지원하지 않는다. 정당한 다운로더도 마찬가지다. ",[14,684,686],{"href":685},"\u002Fko\u002Fblog\u002Fis-it-legal-to-download-streaming-video","스트리밍 다운로드의 합법성","에서 이 선을 더 논의.",[28,689,691,692],{"id":690},"멀티-오디오-트랙-ext-x-media","멀티 오디오 트랙: ",[20,693,694],{},"#EXT-X-MEDIA",[11,696,697],{},"다중 언어 오디오, 디렉터 코멘터리, 청각 장애용 트랙 등:",[63,699,701],{"className":65,"code":700,"language":67,"meta":68,"style":68},"#EXTM3U\n#EXT-X-VERSION:6\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio\",NAME=\"English\",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\",URI=\"en.m3u8\"\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio\",NAME=\"Japanese\",AUTOSELECT=YES,LANGUAGE=\"ja\",URI=\"ja.m3u8\"\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio\",NAME=\"Commentary\",AUTOSELECT=NO,LANGUAGE=\"en\",URI=\"commentary.m3u8\"\n#EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1920x1080,AUDIO=\"audio\"\nvideo.m3u8\n",[20,702,703,707,712,717,722,727,732],{"__ignoreMap":68},[72,704,705],{"class":74,"line":75},[72,706,78],{},[72,708,709],{"class":74,"line":81},[72,710,711],{},"#EXT-X-VERSION:6\n",[72,713,714],{"class":74,"line":87},[72,715,716],{},"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio\",NAME=\"English\",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\",URI=\"en.m3u8\"\n",[72,718,719],{"class":74,"line":93},[72,720,721],{},"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio\",NAME=\"Japanese\",AUTOSELECT=YES,LANGUAGE=\"ja\",URI=\"ja.m3u8\"\n",[72,723,724],{"class":74,"line":99},[72,725,726],{},"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio\",NAME=\"Commentary\",AUTOSELECT=NO,LANGUAGE=\"en\",URI=\"commentary.m3u8\"\n",[72,728,729],{"class":74,"line":105},[72,730,731],{},"#EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1920x1080,AUDIO=\"audio\"\n",[72,733,734],{"class":74,"line":111},[72,735,736],{},"video.m3u8\n",[11,738,739,740,743,744,746,747,750,751,754],{},"마스터 플레이리스트는 비디오 변형(",[20,741,742],{},"#EXT-X-STREAM-INF",")과 독립 오디오 트랙(",[20,745,694],{},")을 나열. 각 ",[20,748,749],{},"AUDIO"," 속성은 같은 ",[20,752,753],{},"GROUP-ID","를 공유하는 미디어 세트를 가리킨다.",[11,756,757],{},"다운로더의 흐름:",[36,759,760,763,766,769,772,775],{},[39,761,762],{},"마스터 파싱",[39,764,765],{},"원하는 비디오 변형 선택",[39,767,768],{},"원하는 오디오 트랙 선택",[39,770,771],{},"두 URL을 따로 페치하여 세그먼트 리스트 획득",[39,773,774],{},"비디오와 오디오 각각 결합",[39,776,777],{},"최종 멀티플렉스 (MP4로 만드는 경우)",[779,780,782],"h3",{"id":781},"자막-트랙","자막 트랙",[63,784,786],{"className":65,"code":785,"language":67,"meta":68,"style":68},"#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"English\",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\",URI=\"subs-en.m3u8\"\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"Japanese\",AUTOSELECT=YES,LANGUAGE=\"ja\",URI=\"subs-ja.m3u8\"\n",[20,787,788,793],{"__ignoreMap":68},[72,789,790],{"class":74,"line":75},[72,791,792],{},"#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"English\",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\",URI=\"subs-en.m3u8\"\n",[72,794,795],{"class":74,"line":81},[72,796,797],{},"#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"Japanese\",AUTOSELECT=YES,LANGUAGE=\"ja\",URI=\"subs-ja.m3u8\"\n",[11,799,800,801,804,805,808],{},"WebVTT 포맷. 각 자막 세그먼트는 ",[20,802,803],{},".vtt"," 파일. 결합은 그냥 ",[20,806,807],{},"concat","——단, 마지막 WebVTT 세그먼트의 X-TIMESTAMP-MAP을 주의:",[63,810,814],{"className":811,"code":812,"language":813,"meta":68,"style":68},"language-vtt shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","X-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000\n","vtt",[20,815,816],{"__ignoreMap":68},[72,817,818],{"class":74,"line":75},[72,819,812],{},[11,821,822],{},"이 맵은 WebVTT의 시간이 TS의 PTS와 어떻게 대응하는지. 결합 시 이 매핑이 정확히 유지되어야 자막과 비디오가 싱크된다.",[28,824,826,829],{"id":825},"ext-x-discontinuity-그-다음은-뭘까",[20,827,828],{},"#EXT-X-DISCONTINUITY",": 그 다음은 뭘까",[63,831,833],{"className":65,"code":832,"language":67,"meta":68,"style":68},"#EXTINF:6.0,\nad-1.ts\n#EXTINF:6.0,\nad-2.ts\n#EXT-X-DISCONTINUITY\n#EXTINF:6.0,\nmain-1.ts\n",[20,834,835,839,844,848,853,858,862],{"__ignoreMap":68},[72,836,837],{"class":74,"line":75},[72,838,108],{},[72,840,841],{"class":74,"line":81},[72,842,843],{},"ad-1.ts\n",[72,845,846],{"class":74,"line":87},[72,847,108],{},[72,849,850],{"class":74,"line":93},[72,851,852],{},"ad-2.ts\n",[72,854,855],{"class":74,"line":99},[72,856,857],{},"#EXT-X-DISCONTINUITY\n",[72,859,860],{"class":74,"line":105},[72,861,108],{},[72,863,864],{"class":74,"line":111},[72,865,866],{},"main-1.ts\n",[11,868,869,871],{},[20,870,828],{},"는 이전 세그먼트와 다음 세그먼트 사이에 \"불연속\"이 있음을 선언——codec, 타임스탬프, 해상도, 프레임 레이트가 바뀔 수 있음.",[11,873,874],{},"광고 삽입에서 흔하다. 본편은 1080p\u002F30fps H.264, 광고는 720p\u002F15fps VP9 등.",[11,876,877],{},"다운로더가 주의할 것:",[36,879,880,886,892],{},[39,881,882,885],{},[42,883,884],{},"타임스탬프 재설정."," discontinuity 다음 세그먼트의 PTS는 0에서 시작할 수 있다. 단순히 \"이전 세그먼트 끝 시간 + 길이\"로 계산하면 안 된다.",[39,887,888,891],{},[42,889,890],{},"codec 호환성."," 다른 codec을 그냥 결합하면 파일이 망가진다. 비디오 플레이어는 첫 번째 codec을 쓰다가 두 번째 부분에서 글리치.",[39,893,894,897,898,901],{},[42,895,896],{},"광고 건너뛰기."," 사용자는 광고를 원하지 않는다. ",[20,899,900],{},"discontinuity"," 사이의 세그먼트를 무시하고 본편만 결합——가장 일반적 전략, 타임스탬프를 재조정해야 한다.",[28,903,905],{"id":904},"ext-x-independent-segments",[20,906,907],{},"#EXT-X-INDEPENDENT-SEGMENTS",[63,909,911],{"className":65,"code":910,"language":67,"meta":68,"style":68},"#EXT-X-INDEPENDENT-SEGMENTS\n#EXTINF:6.0,\nsegment-1.ts\n",[20,912,913,918,922],{"__ignoreMap":68},[72,914,915],{"class":74,"line":75},[72,916,917],{},"#EXT-X-INDEPENDENT-SEGMENTS\n",[72,919,920],{"class":74,"line":81},[72,921,108],{},[72,923,924],{"class":74,"line":87},[72,925,114],{},[11,927,928],{},"이 태그는 모든 세그먼트가 독립적으로 디코딩 가능함을 선언——각 세그먼트가 IDR 프레임으로 시작. 다운로더에게 중요: 임의 세그먼트부터 결합해도 올바른 MP4가 나온다.",[11,930,931],{},"이 태그가 없으면, 첫 번째 세그먼트가 IDR 프레임을 갖는다고 가정할 수 없다. \"앞부분 몇 초가 손상된\" 재생 파일이 나오는 원인.",[28,933,935,938],{"id":934},"ext-x-map-fmp4-세그먼트용",[20,936,937],{},"#EXT-X-MAP",": fMP4 세그먼트용",[63,940,942],{"className":65,"code":941,"language":67,"meta":68,"style":68},"#EXT-X-MAP:URI=\"init.mp4\"\n#EXTINF:6.0,\nsegment-1.m4s\n",[20,943,944,949,953],{"__ignoreMap":68},[72,945,946],{"class":74,"line":75},[72,947,948],{},"#EXT-X-MAP:URI=\"init.mp4\"\n",[72,950,951],{"class":74,"line":81},[72,952,108],{},[72,954,955],{"class":74,"line":87},[72,956,957],{},"segment-1.m4s\n",[11,959,960,961,964,965,968,969,972,973,976,977,968,980,211],{},"이 태그는 이후 세그먼트가 MPEG-TS가 아니라 fMP4임을 선언. ",[20,962,963],{},"init.mp4","는 초기화 박스(",[20,966,967],{},"ftyp"," + ",[20,970,971],{},"moov",")를 가진다. 각 ",[20,974,975],{},".m4s","는 ",[20,978,979],{},"moof",[20,981,982],{},"mdat",[11,984,985,986,990],{},"HLS-fMP4는 DASH와 구조적으로 동일——세그먼트를 결합하려면 init + 모든 미디어 세그먼트를 순서대로 붙이면 된다. 자세한 건 ",[14,987,989],{"href":988},"\u002Fko\u002Fblog\u002Fbrowser-streaming-remux-mp4-isobmff","fMP4 결합 글","에서.",[11,992,993],{},"어떤 사이트는 TS와 fMP4를 섞어 쓴다. 마스터에서는 어떤 변형은 TS, 어떤 변형은 fMP4. 다운로더는 양쪽 포맷을 다뤄야 한다.",[28,995,997],{"id":996},"ext-x-program-date-time",[20,998,999],{},"#EXT-X-PROGRAM-DATE-TIME",[63,1001,1003],{"className":65,"code":1002,"language":67,"meta":68,"style":68},"#EXT-X-PROGRAM-DATE-TIME:2026-08-04T18:30:00.000Z\n#EXTINF:6.0,\nsegment-1.ts\n",[20,1004,1005,1010,1014],{"__ignoreMap":68},[72,1006,1007],{"class":74,"line":75},[72,1008,1009],{},"#EXT-X-PROGRAM-DATE-TIME:2026-08-04T18:30:00.000Z\n",[72,1011,1012],{"class":74,"line":81},[72,1013,108],{},[72,1015,1016],{"class":74,"line":87},[72,1017,114],{},[11,1019,1020,1021,1024],{},"이 태그는 세그먼트의 절대 방송 시간. 라이브에서 중요: \"이 세그먼트가 실제로 몇 시 몇 분에 방영되었는가\"를 안다. 다운로더는 이것을 \"사용자가 다운로드하는 부분이 최근인가?\"를 판단하는 데 쓴다——minimum\u002Fmaximum ",[20,1022,1023],{},"PROGRAM-DATE-TIME","을 보고 \"이 라이브가 언제부터 시작했고 지금은 어디쯤\"을 안다.",[11,1026,1027,1030],{},[20,1028,1029],{},"EXT-X-PROGRAM-DATE-TIME","은 optional이고 모든 M3U8에 있는 건 아니다. 하지만 있으면 동기화가 쉬워진다.",[28,1032,1034],{"id":1033},"ext-x-playlist-type",[20,1035,1036],{},"#EXT-X-PLAYLIST-TYPE",[63,1038,1040],{"className":65,"code":1039,"language":67,"meta":68,"style":68},"#EXT-X-PLAYLIST-TYPE:VOD\n",[20,1041,1042],{"__ignoreMap":68},[72,1043,1044],{"class":74,"line":75},[72,1045,1039],{},[11,1047,1048,1049,1052,1053,1056,1057,1059,1060,1063,1064,1066],{},"두 값: ",[20,1050,1051],{},"VOD"," 또는 ",[20,1054,1055],{},"EVENT",". ",[20,1058,1051],{},"는 플레이리스트가 정적——한 번 쓰고 끝, ",[20,1061,1062],{},"#EXT-X-ENDLIST","로 닫힌다. ",[20,1065,1055],{},"는 플레이리스트가 자라나고 있다——라이브가 종료되어 VOD로 전환될 때까지 갱신. 태그가 없으면 알 수 없다.",[11,1068,1069,1071,1072,1074],{},[20,1070,1051],{},"가 가장 쉽다: 한 번 파싱하고 다운로드, 끝. ",[20,1073,1055],{},"와 태그 없음은 재획득 로직이 필요.",[28,1076,1078],{"id":1077},"ext-x-endlist",[20,1079,1062],{},[63,1081,1083],{"className":65,"code":1082,"language":67,"meta":68,"style":68},"#EXTINF:6.0,\nsegment-1000.ts\n#EXT-X-ENDLIST\n",[20,1084,1085,1089,1094],{"__ignoreMap":68},[72,1086,1087],{"class":74,"line":75},[72,1088,108],{},[72,1090,1091],{"class":74,"line":81},[72,1092,1093],{},"segment-1000.ts\n",[72,1095,1096],{"class":74,"line":87},[72,1097,131],{},[11,1099,1100],{},"\"스트림이 여기서 끝남, 이 이후 세그먼트는 없음\"을 선언. 다운로더는 이것을 봐야 \"전부 다운로드했는가?\"를 안다. 라이브에서는 안 보일 수 있다——스트림이 아직 진행 중이므로. 이 경우, \"스트림 종료를 기다린다\" 또는 \"현재까지의 세그먼트로 끝낸다\" 선택.",[28,1102,1104],{"id":1103},"마스터-플레이리스트-파싱","마스터 플레이리스트 파싱",[63,1106,1108],{"className":65,"code":1107,"language":67,"meta":68,"style":68},"#EXTM3U\n#EXT-X-VERSION:6\n#EXT-X-MEDIA:TYPE=AUDIO,...,URI=\"audio-en.m3u8\"\n#EXT-X-STREAM-INF:BANDWIDTH=12000000,RESOLUTION=3840x2160,CODECS=\"avc1.640028,mp4a.40.2\",AUDIO=\"audio\"\n4k.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1920x1080,CODECS=\"avc1.640028,mp4a.40.2\",AUDIO=\"audio\"\n1080p.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=2000000,RESOLUTION=1280x720,CODECS=\"avc1.64001f,mp4a.40.2\",AUDIO=\"audio\"\n720p.m3u8\n",[20,1109,1110,1114,1118,1123,1128,1133,1138,1143,1148],{"__ignoreMap":68},[72,1111,1112],{"class":74,"line":75},[72,1113,78],{},[72,1115,1116],{"class":74,"line":81},[72,1117,711],{},[72,1119,1120],{"class":74,"line":87},[72,1121,1122],{},"#EXT-X-MEDIA:TYPE=AUDIO,...,URI=\"audio-en.m3u8\"\n",[72,1124,1125],{"class":74,"line":93},[72,1126,1127],{},"#EXT-X-STREAM-INF:BANDWIDTH=12000000,RESOLUTION=3840x2160,CODECS=\"avc1.640028,mp4a.40.2\",AUDIO=\"audio\"\n",[72,1129,1130],{"class":74,"line":99},[72,1131,1132],{},"4k.m3u8\n",[72,1134,1135],{"class":74,"line":105},[72,1136,1137],{},"#EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1920x1080,CODECS=\"avc1.640028,mp4a.40.2\",AUDIO=\"audio\"\n",[72,1139,1140],{"class":74,"line":111},[72,1141,1142],{},"1080p.m3u8\n",[72,1144,1145],{"class":74,"line":117},[72,1146,1147],{},"#EXT-X-STREAM-INF:BANDWIDTH=2000000,RESOLUTION=1280x720,CODECS=\"avc1.64001f,mp4a.40.2\",AUDIO=\"audio\"\n",[72,1149,1150],{"class":74,"line":122},[72,1151,1152],{},"720p.m3u8\n",[11,1154,1155,1158,1159,1162,1163,1166,1167,1170],{},[20,1156,1157],{},"CODECS"," 속성이 중요——",[20,1160,1161],{},"avc1.640028","은 H.264, ",[20,1164,1165],{},"hvc1.1.6.L120.B0","은 H.265, ",[20,1168,1169],{},"vp09.00.10.08","은 VP9. 이 문자열로 세그먼트를 디코딩할 codec을 안다.",[11,1172,757],{},[36,1174,1175,1177,1183,1186],{},[39,1176,762],{},[39,1178,1179,1180,1182],{},"최고 비트레이트의 ",[20,1181,742],{},"를 찾는다 (사용자가 최고 화질을 원한다면)",[39,1184,1185],{},"해당 변형의 URL을 재귀적으로 페치하여 세그먼트 리스트 획득",[39,1187,1188,1190],{},[20,1189,1157],{}," 문자열에서 codec을 알아내어 디코더를 설정",[28,1192,1194],{"id":1193},"실제로-부딪힌-어려운-부분","실제로 부딪힌 어려운 부분",[11,1196,1197,1200,1201,1204,1205,1208,1209,1212,1213,1216,1217,1208,1219,1221,1222,1225],{},[42,1198,1199],{},"어려움 1: 세그먼트 URL의 상대 경로."," M3U8이 ",[20,1202,1203],{},"https:\u002F\u002Fcdn.example.com\u002Fv\u002Fx.m3u8","에 있고, 안에 ",[20,1206,1207],{},"segment-1.ts","라고 쓰여 있으면, 절대 URL은 ",[20,1210,1211],{},"https:\u002F\u002Fcdn.example.com\u002Fv\u002Fsegment-1.ts",". 하지만 ",[20,1214,1215],{},"https:\u002F\u002Fcdn.example.com\u002Fv\u002F"," 안에 또 다른 M3U8이 있고, 그 안에 ",[20,1218,1207],{},[20,1220,1211],{},"가 아닌 ",[20,1223,1224],{},"https:\u002F\u002Fcdn.example.com\u002Fv\u002F\u003Csubdir>\u002Fsegment-1.ts","가 될 수 있다.",[11,1227,1228,1229,1232],{},"RFC 3986의 상대 URL 해석 규칙을 따라야 한다. JS의 ",[20,1230,1231],{},"new URL(relative, base)","가 해준다:",[63,1234,1236],{"className":155,"code":1235,"language":157,"meta":68,"style":68},"const segmentUrl = new URL('segment-1.ts', playlistUrl).href\n",[20,1237,1238],{"__ignoreMap":68},[72,1239,1240,1242,1245,1247,1249,1252,1254,1256,1258,1260,1262,1265,1267],{"class":74,"line":75},[72,1241,165],{"class":164},[72,1243,1244],{"class":168}," segmentUrl ",[72,1246,173],{"class":172},[72,1248,196],{"class":172},[72,1250,1251],{"class":180}," URL",[72,1253,202],{"class":168},[72,1255,312],{"class":172},[72,1257,1207],{"class":308},[72,1259,312],{"class":172},[72,1261,251],{"class":172},[72,1263,1264],{"class":168}," playlistUrl)",[72,1266,211],{"class":172},[72,1268,1269],{"class":168},"href\n",[11,1271,1272,1275],{},[42,1273,1274],{},"어려움 2: 라이브 스트림의 다운로드 시점."," 라이브는 새 세그먼트를 계속 만든다. \"다운로드 시작\"을 누른 순간의 스냅샷만 가질 수 있다——스트림이 아직 살아있으면, 스냅샷 이후에 추가되는 세그먼트는 놓친다. \"라이브가 끝날 때까지 기다리기\" 옵션이 있으면 좋지만, 스트림이 영원히 지속되는 라이브(24시간 뉴스 등)에는 안 통한다.",[11,1277,1278],{},"FlowPick의 접근: 다운로드 시작 시점의 스냅샷을 찍고, 사용자에게 \"이후에 다시 클릭하여 새 세그먼트 추가\"를 허용.",[11,1280,1281,1284,1285,1287],{},[42,1282,1283],{},"어려움 3: 청크 단위 인코딩."," 일부 라이브는 CMAF(chunked CMAF)를 사용——세그먼트가 완성되기 전에 청크 단위로 전송. ",[20,1286,48],{},"가 바뀌는 중간에 부분적 청크가 올 수 있어, 파서는 청크 경계를 정확히 처리해야 한다.",[11,1289,1290,1293,1294,1297],{},[42,1291,1292],{},"어려움 4: 토큰 기반 CDN."," 일부 CDN은 서명된 URL을 사용——세그먼트 URL이 ",[20,1295,1296],{},"?token=abc&expires=1234567890"," 형태. 이 토큰은 짧은 TTL(몇 초)을 가져서, M3U8 파싱 직후에 세그먼트를 페치해야 한다. 몇 시간 전에 파싱하고 지금 다운로드하는 배치 다운로더는 실패.",[28,1299,1301],{"id":1300},"다운로더의-간단한-구조","다운로더의 간단한 구조",[63,1303,1305],{"className":155,"code":1304,"language":157,"meta":68,"style":68},"async function downloadHls(masterUrl) {\n  const masterText = await fetch(masterUrl).then(r => r.text())\n  const masterPlaylist = parseM3u8(masterText)\n\n  \u002F\u002F 최고 비트레이트 변형 선택\n  const variants = masterPlaylist.streamInf || [{ url: masterUrl }]\n  const best = variants.sort((a, b) => b.bandwidth - a.bandwidth)[0]\n\n  \u002F\u002F 변형 플레이리스트 페치\n  const variantUrl = new URL(best.url, masterUrl).href\n  const variantText = await fetch(variantUrl).then(r => r.text())\n  const variantPlaylist = parseM3u8(variantText)\n\n  \u002F\u002F fMP4인가 TS인가\n  const isFmp4 = variantPlaylist.map !== undefined\n  const initUrl = isFmp4\n    ? new URL(variantPlaylist.map.uri, variantUrl).href\n    : null\n\n  \u002F\u002F 키 페치\n  let keyBytes = null\n  let iv = null\n  if (variantPlaylist.key && variantPlaylist.key.method === 'AES-128') {\n    const keyUrl = new URL(variantPlaylist.key.uri, variantUrl).href\n    keyBytes = await fetch(keyUrl, { credentials: 'include' })\n      .then(r => r.arrayBuffer())\n      .then(b => new Uint8Array(b))\n    iv = variantPlaylist.key.iv\n      ? hexToBytes(variantPlaylist.key.iv)\n      : deriveIV(variantPlaylist.mediaSequence)\n  }\n\n  \u002F\u002F 세그먼트 페치\n  const segments = variantPlaylist.segments\n  const segmentUrls = segments.map(s => new URL(s.uri, variantUrl).href)\n\n  \u002F\u002F 병렬 다운로드\n  const segmentData = await parallelFetch(segmentUrls, 6)\n\n  \u002F\u002F 필요시 복호화\n  if (keyBytes) {\n    for (let i = 0; i \u003C segmentData.length; i++) {\n      const segmentIv = variantPlaylist.key.iv\n        ? hexToBytes(variantPlaylist.key.iv)\n        : deriveIV(variantPlaylist.mediaSequence + i)\n      segmentData[i] = await decryptSegment(segmentData[i], keyBytes, segmentIv)\n    }\n  }\n\n  \u002F\u002F 결합\n  if (isFmp4) {\n    const initBytes = await fetch(initUrl).then(r => r.arrayBuffer())\n    return mergeFmp4(initBytes, segmentData)\n  } else {\n    return mergeTsSegments(segmentData)  \u002F\u002F TS 디먹싱 필요, FFmpeg WASM 또는 mux.js\n  }\n}\n",[20,1306,1307,1325,1367,1386,1390,1395,1433,1489,1493,1498,1531,1569,1587,1591,1596,1618,1630,1663,1671,1676,1682,1694,1705,1751,1788,1824,1846,1871,1890,1913,1932,1938,1943,1949,1966,2014,2019,2025,2052,2057,2063,2077,2122,2143,2165,2188,2230,2236,2241,2246,2252,2266,2305,2325,2336,2354,2359],{"__ignoreMap":68},[72,1308,1309,1311,1313,1316,1318,1321,1323],{"class":74,"line":75},[72,1310,236],{"class":164},[72,1312,239],{"class":164},[72,1314,1315],{"class":180}," downloadHls",[72,1317,202],{"class":172},[72,1319,1320],{"class":247},"masterUrl",[72,1322,262],{"class":172},[72,1324,265],{"class":172},[72,1326,1327,1329,1332,1334,1336,1338,1340,1342,1344,1346,1349,1351,1354,1357,1360,1362,1365],{"class":74,"line":81},[72,1328,275],{"class":164},[72,1330,1331],{"class":168}," masterText",[72,1333,281],{"class":172},[72,1335,177],{"class":176},[72,1337,181],{"class":180},[72,1339,202],{"class":299},[72,1341,1320],{"class":168},[72,1343,262],{"class":299},[72,1345,211],{"class":172},[72,1347,1348],{"class":180},"then",[72,1350,202],{"class":299},[72,1352,1353],{"class":247},"r",[72,1355,1356],{"class":164}," =>",[72,1358,1359],{"class":168}," r",[72,1361,211],{"class":172},[72,1363,1364],{"class":180},"text",[72,1366,217],{"class":299},[72,1368,1369,1371,1374,1376,1379,1381,1384],{"class":74,"line":87},[72,1370,275],{"class":164},[72,1372,1373],{"class":168}," masterPlaylist",[72,1375,281],{"class":172},[72,1377,1378],{"class":180}," parseM3u8",[72,1380,202],{"class":299},[72,1382,1383],{"class":168},"masterText",[72,1385,465],{"class":299},[72,1387,1388],{"class":74,"line":93},[72,1389,378],{"emptyLinePlaceholder":377},[72,1391,1392],{"class":74,"line":99},[72,1393,1394],{"class":222},"  \u002F\u002F 최고 비트레이트 변형 선택\n",[72,1396,1397,1399,1402,1404,1406,1408,1411,1414,1417,1420,1423,1425,1428,1431],{"class":74,"line":105},[72,1398,275],{"class":164},[72,1400,1401],{"class":168}," variants",[72,1403,281],{"class":172},[72,1405,1373],{"class":168},[72,1407,211],{"class":172},[72,1409,1410],{"class":168},"streamInf",[72,1412,1413],{"class":172}," ||",[72,1415,1416],{"class":299}," [",[72,1418,1419],{"class":172},"{",[72,1421,1422],{"class":299}," url",[72,1424,333],{"class":172},[72,1426,1427],{"class":168}," masterUrl",[72,1429,1430],{"class":172}," }",[72,1432,367],{"class":299},[72,1434,1435,1437,1440,1442,1444,1446,1449,1451,1453,1455,1457,1460,1462,1464,1466,1468,1471,1474,1477,1479,1481,1484,1487],{"class":74,"line":111},[72,1436,275],{"class":164},[72,1438,1439],{"class":168}," best",[72,1441,281],{"class":172},[72,1443,1401],{"class":168},[72,1445,211],{"class":172},[72,1447,1448],{"class":180},"sort",[72,1450,202],{"class":299},[72,1452,202],{"class":172},[72,1454,14],{"class":247},[72,1456,251],{"class":172},[72,1458,1459],{"class":247}," b",[72,1461,262],{"class":172},[72,1463,1356],{"class":164},[72,1465,1459],{"class":168},[72,1467,211],{"class":172},[72,1469,1470],{"class":168},"bandwidth",[72,1472,1473],{"class":172}," -",[72,1475,1476],{"class":168}," a",[72,1478,211],{"class":172},[72,1480,1470],{"class":168},[72,1482,1483],{"class":299},")[",[72,1485,1486],{"class":530},"0",[72,1488,367],{"class":299},[72,1490,1491],{"class":74,"line":117},[72,1492,378],{"emptyLinePlaceholder":377},[72,1494,1495],{"class":74,"line":122},[72,1496,1497],{"class":222},"  \u002F\u002F 변형 플레이리스트 페치\n",[72,1499,1500,1502,1505,1507,1509,1511,1513,1516,1518,1521,1523,1525,1527,1529],{"class":74,"line":128},[72,1501,275],{"class":164},[72,1503,1504],{"class":168}," variantUrl",[72,1506,281],{"class":172},[72,1508,196],{"class":172},[72,1510,1251],{"class":180},[72,1512,202],{"class":299},[72,1514,1515],{"class":168},"best",[72,1517,211],{"class":172},[72,1519,1520],{"class":168},"url",[72,1522,251],{"class":172},[72,1524,1427],{"class":168},[72,1526,262],{"class":299},[72,1528,211],{"class":172},[72,1530,1269],{"class":168},[72,1532,1533,1535,1538,1540,1542,1544,1546,1549,1551,1553,1555,1557,1559,1561,1563,1565,1567],{"class":74,"line":381},[72,1534,275],{"class":164},[72,1536,1537],{"class":168}," variantText",[72,1539,281],{"class":172},[72,1541,177],{"class":176},[72,1543,181],{"class":180},[72,1545,202],{"class":299},[72,1547,1548],{"class":168},"variantUrl",[72,1550,262],{"class":299},[72,1552,211],{"class":172},[72,1554,1348],{"class":180},[72,1556,202],{"class":299},[72,1558,1353],{"class":247},[72,1560,1356],{"class":164},[72,1562,1359],{"class":168},[72,1564,211],{"class":172},[72,1566,1364],{"class":180},[72,1568,217],{"class":299},[72,1570,1571,1573,1576,1578,1580,1582,1585],{"class":74,"line":405},[72,1572,275],{"class":164},[72,1574,1575],{"class":168}," variantPlaylist",[72,1577,281],{"class":172},[72,1579,1378],{"class":180},[72,1581,202],{"class":299},[72,1583,1584],{"class":168},"variantText",[72,1586,465],{"class":299},[72,1588,1589],{"class":74,"line":426},[72,1590,378],{"emptyLinePlaceholder":377},[72,1592,1593],{"class":74,"line":434},[72,1594,1595],{"class":222},"  \u002F\u002F fMP4인가 TS인가\n",[72,1597,1598,1600,1603,1605,1607,1609,1612,1615],{"class":74,"line":440},[72,1599,275],{"class":164},[72,1601,1602],{"class":168}," isFmp4",[72,1604,281],{"class":172},[72,1606,1575],{"class":168},[72,1608,211],{"class":172},[72,1610,1611],{"class":168},"map",[72,1613,1614],{"class":172}," !==",[72,1616,1617],{"class":172}," undefined\n",[72,1619,1620,1622,1625,1627],{"class":74,"line":445},[72,1621,275],{"class":164},[72,1623,1624],{"class":168}," initUrl",[72,1626,281],{"class":172},[72,1628,1629],{"class":168}," isFmp4\n",[72,1631,1632,1635,1637,1639,1641,1644,1646,1648,1650,1653,1655,1657,1659,1661],{"class":74,"line":450},[72,1633,1634],{"class":172},"    ?",[72,1636,196],{"class":172},[72,1638,1251],{"class":180},[72,1640,202],{"class":299},[72,1642,1643],{"class":168},"variantPlaylist",[72,1645,211],{"class":172},[72,1647,1611],{"class":168},[72,1649,211],{"class":172},[72,1651,1652],{"class":168},"uri",[72,1654,251],{"class":172},[72,1656,1504],{"class":168},[72,1658,262],{"class":299},[72,1660,211],{"class":172},[72,1662,1269],{"class":168},[72,1664,1665,1668],{"class":74,"line":468},[72,1666,1667],{"class":172},"    :",[72,1669,1670],{"class":172}," null\n",[72,1672,1674],{"class":74,"line":1673},19,[72,1675,378],{"emptyLinePlaceholder":377},[72,1677,1679],{"class":74,"line":1678},20,[72,1680,1681],{"class":222},"  \u002F\u002F 키 페치\n",[72,1683,1685,1688,1690,1692],{"class":74,"line":1684},21,[72,1686,1687],{"class":164},"  let",[72,1689,254],{"class":168},[72,1691,281],{"class":172},[72,1693,1670],{"class":172},[72,1695,1697,1699,1701,1703],{"class":74,"line":1696},22,[72,1698,1687],{"class":164},[72,1700,259],{"class":168},[72,1702,281],{"class":172},[72,1704,1670],{"class":172},[72,1706,1708,1711,1714,1716,1718,1721,1724,1726,1728,1730,1732,1735,1738,1740,1743,1745,1748],{"class":74,"line":1707},23,[72,1709,1710],{"class":176},"  if",[72,1712,1713],{"class":299}," (",[72,1715,1643],{"class":168},[72,1717,211],{"class":172},[72,1719,1720],{"class":168},"key",[72,1722,1723],{"class":172}," &&",[72,1725,1575],{"class":168},[72,1727,211],{"class":172},[72,1729,1720],{"class":168},[72,1731,211],{"class":172},[72,1733,1734],{"class":168},"method",[72,1736,1737],{"class":172}," ===",[72,1739,336],{"class":172},[72,1741,1742],{"class":308},"AES-128",[72,1744,312],{"class":172},[72,1746,1747],{"class":299},") ",[72,1749,1750],{"class":172},"{\n",[72,1752,1754,1757,1760,1762,1764,1766,1768,1770,1772,1774,1776,1778,1780,1782,1784,1786],{"class":74,"line":1753},24,[72,1755,1756],{"class":164},"    const",[72,1758,1759],{"class":168}," keyUrl",[72,1761,281],{"class":172},[72,1763,196],{"class":172},[72,1765,1251],{"class":180},[72,1767,202],{"class":299},[72,1769,1643],{"class":168},[72,1771,211],{"class":172},[72,1773,1720],{"class":168},[72,1775,211],{"class":172},[72,1777,1652],{"class":168},[72,1779,251],{"class":172},[72,1781,1504],{"class":168},[72,1783,262],{"class":299},[72,1785,211],{"class":172},[72,1787,1269],{"class":168},[72,1789,1791,1793,1795,1797,1799,1801,1804,1806,1809,1812,1814,1816,1818,1820,1822],{"class":74,"line":1790},25,[72,1792,320],{"class":168},[72,1794,281],{"class":172},[72,1796,177],{"class":176},[72,1798,181],{"class":180},[72,1800,202],{"class":299},[72,1802,1803],{"class":168},"keyUrl",[72,1805,251],{"class":172},[72,1807,1808],{"class":172}," {",[72,1810,1811],{"class":299}," credentials",[72,1813,333],{"class":172},[72,1815,336],{"class":172},[72,1817,604],{"class":308},[72,1819,312],{"class":172},[72,1821,1430],{"class":172},[72,1823,465],{"class":299},[72,1825,1827,1830,1832,1834,1836,1838,1840,1842,1844],{"class":74,"line":1826},26,[72,1828,1829],{"class":172},"      .",[72,1831,1348],{"class":180},[72,1833,202],{"class":299},[72,1835,1353],{"class":247},[72,1837,1356],{"class":164},[72,1839,1359],{"class":168},[72,1841,211],{"class":172},[72,1843,214],{"class":180},[72,1845,217],{"class":299},[72,1847,1849,1851,1853,1855,1858,1860,1862,1864,1866,1868],{"class":74,"line":1848},27,[72,1850,1829],{"class":172},[72,1852,1348],{"class":180},[72,1854,202],{"class":299},[72,1856,1857],{"class":247},"b",[72,1859,1356],{"class":164},[72,1861,196],{"class":172},[72,1863,199],{"class":180},[72,1865,202],{"class":299},[72,1867,1857],{"class":168},[72,1869,1870],{"class":299},"))\n",[72,1872,1874,1877,1879,1881,1883,1885,1887],{"class":74,"line":1873},28,[72,1875,1876],{"class":168},"    iv",[72,1878,281],{"class":172},[72,1880,1575],{"class":168},[72,1882,211],{"class":172},[72,1884,1720],{"class":168},[72,1886,211],{"class":172},[72,1888,1889],{"class":168},"iv\n",[72,1891,1893,1896,1899,1901,1903,1905,1907,1909,1911],{"class":74,"line":1892},29,[72,1894,1895],{"class":172},"      ?",[72,1897,1898],{"class":180}," hexToBytes",[72,1900,202],{"class":299},[72,1902,1643],{"class":168},[72,1904,211],{"class":172},[72,1906,1720],{"class":168},[72,1908,211],{"class":172},[72,1910,552],{"class":168},[72,1912,465],{"class":299},[72,1914,1916,1919,1921,1923,1925,1927,1930],{"class":74,"line":1915},30,[72,1917,1918],{"class":172},"      :",[72,1920,504],{"class":180},[72,1922,202],{"class":299},[72,1924,1643],{"class":168},[72,1926,211],{"class":172},[72,1928,1929],{"class":168},"mediaSequence",[72,1931,465],{"class":299},[72,1933,1935],{"class":74,"line":1934},31,[72,1936,1937],{"class":172},"  }\n",[72,1939,1941],{"class":74,"line":1940},32,[72,1942,378],{"emptyLinePlaceholder":377},[72,1944,1946],{"class":74,"line":1945},33,[72,1947,1948],{"class":222},"  \u002F\u002F 세그먼트 페치\n",[72,1950,1952,1954,1957,1959,1961,1963],{"class":74,"line":1951},34,[72,1953,275],{"class":164},[72,1955,1956],{"class":168}," segments",[72,1958,281],{"class":172},[72,1960,1575],{"class":168},[72,1962,211],{"class":172},[72,1964,1965],{"class":168},"segments\n",[72,1967,1969,1971,1974,1976,1978,1980,1982,1984,1987,1989,1991,1993,1995,1997,1999,2001,2003,2005,2007,2009,2012],{"class":74,"line":1968},35,[72,1970,275],{"class":164},[72,1972,1973],{"class":168}," segmentUrls",[72,1975,281],{"class":172},[72,1977,1956],{"class":168},[72,1979,211],{"class":172},[72,1981,1611],{"class":180},[72,1983,202],{"class":299},[72,1985,1986],{"class":247},"s",[72,1988,1356],{"class":164},[72,1990,196],{"class":172},[72,1992,1251],{"class":180},[72,1994,202],{"class":299},[72,1996,1986],{"class":168},[72,1998,211],{"class":172},[72,2000,1652],{"class":168},[72,2002,251],{"class":172},[72,2004,1504],{"class":168},[72,2006,262],{"class":299},[72,2008,211],{"class":172},[72,2010,2011],{"class":168},"href",[72,2013,465],{"class":299},[72,2015,2017],{"class":74,"line":2016},36,[72,2018,378],{"emptyLinePlaceholder":377},[72,2020,2022],{"class":74,"line":2021},37,[72,2023,2024],{"class":222},"  \u002F\u002F 병렬 다운로드\n",[72,2026,2028,2030,2033,2035,2037,2040,2042,2045,2047,2050],{"class":74,"line":2027},38,[72,2029,275],{"class":164},[72,2031,2032],{"class":168}," segmentData",[72,2034,281],{"class":172},[72,2036,177],{"class":176},[72,2038,2039],{"class":180}," parallelFetch",[72,2041,202],{"class":299},[72,2043,2044],{"class":168},"segmentUrls",[72,2046,251],{"class":172},[72,2048,2049],{"class":530}," 6",[72,2051,465],{"class":299},[72,2053,2055],{"class":74,"line":2054},39,[72,2056,378],{"emptyLinePlaceholder":377},[72,2058,2060],{"class":74,"line":2059},40,[72,2061,2062],{"class":222},"  \u002F\u002F 필요시 복호화\n",[72,2064,2066,2068,2070,2073,2075],{"class":74,"line":2065},41,[72,2067,1710],{"class":176},[72,2069,1713],{"class":299},[72,2071,2072],{"class":168},"keyBytes",[72,2074,1747],{"class":299},[72,2076,1750],{"class":172},[72,2078,2080,2083,2085,2088,2091,2093,2096,2099,2101,2104,2106,2108,2111,2113,2115,2118,2120],{"class":74,"line":2079},42,[72,2081,2082],{"class":176},"    for",[72,2084,1713],{"class":299},[72,2086,2087],{"class":164},"let",[72,2089,2090],{"class":168}," i",[72,2092,281],{"class":172},[72,2094,2095],{"class":530}," 0",[72,2097,2098],{"class":172},";",[72,2100,2090],{"class":168},[72,2102,2103],{"class":172}," \u003C",[72,2105,2032],{"class":168},[72,2107,211],{"class":172},[72,2109,2110],{"class":168},"length",[72,2112,2098],{"class":172},[72,2114,2090],{"class":168},[72,2116,2117],{"class":172},"++",[72,2119,1747],{"class":299},[72,2121,1750],{"class":172},[72,2123,2125,2128,2131,2133,2135,2137,2139,2141],{"class":74,"line":2124},43,[72,2126,2127],{"class":164},"      const",[72,2129,2130],{"class":168}," segmentIv",[72,2132,281],{"class":172},[72,2134,1575],{"class":168},[72,2136,211],{"class":172},[72,2138,1720],{"class":168},[72,2140,211],{"class":172},[72,2142,1889],{"class":168},[72,2144,2146,2149,2151,2153,2155,2157,2159,2161,2163],{"class":74,"line":2145},44,[72,2147,2148],{"class":172},"        ?",[72,2150,1898],{"class":180},[72,2152,202],{"class":299},[72,2154,1643],{"class":168},[72,2156,211],{"class":172},[72,2158,1720],{"class":168},[72,2160,211],{"class":172},[72,2162,552],{"class":168},[72,2164,465],{"class":299},[72,2166,2168,2171,2173,2175,2177,2179,2181,2184,2186],{"class":74,"line":2167},45,[72,2169,2170],{"class":172},"        :",[72,2172,504],{"class":180},[72,2174,202],{"class":299},[72,2176,1643],{"class":168},[72,2178,211],{"class":172},[72,2180,1929],{"class":168},[72,2182,2183],{"class":172}," +",[72,2185,2090],{"class":168},[72,2187,465],{"class":299},[72,2189,2191,2194,2197,2200,2203,2205,2207,2209,2211,2213,2215,2217,2220,2222,2224,2226,2228],{"class":74,"line":2190},46,[72,2192,2193],{"class":168},"      segmentData",[72,2195,2196],{"class":299},"[",[72,2198,2199],{"class":168},"i",[72,2201,2202],{"class":299},"] ",[72,2204,173],{"class":172},[72,2206,177],{"class":176},[72,2208,242],{"class":180},[72,2210,202],{"class":299},[72,2212,248],{"class":168},[72,2214,2196],{"class":299},[72,2216,2199],{"class":168},[72,2218,2219],{"class":299},"]",[72,2221,251],{"class":172},[72,2223,254],{"class":168},[72,2225,251],{"class":172},[72,2227,2130],{"class":168},[72,2229,465],{"class":299},[72,2231,2233],{"class":74,"line":2232},47,[72,2234,2235],{"class":172},"    }\n",[72,2237,2239],{"class":74,"line":2238},48,[72,2240,1937],{"class":172},[72,2242,2244],{"class":74,"line":2243},49,[72,2245,378],{"emptyLinePlaceholder":377},[72,2247,2249],{"class":74,"line":2248},50,[72,2250,2251],{"class":222},"  \u002F\u002F 결합\n",[72,2253,2255,2257,2259,2262,2264],{"class":74,"line":2254},51,[72,2256,1710],{"class":176},[72,2258,1713],{"class":299},[72,2260,2261],{"class":168},"isFmp4",[72,2263,1747],{"class":299},[72,2265,1750],{"class":172},[72,2267,2269,2271,2274,2276,2278,2280,2282,2285,2287,2289,2291,2293,2295,2297,2299,2301,2303],{"class":74,"line":2268},52,[72,2270,1756],{"class":164},[72,2272,2273],{"class":168}," initBytes",[72,2275,281],{"class":172},[72,2277,177],{"class":176},[72,2279,181],{"class":180},[72,2281,202],{"class":299},[72,2283,2284],{"class":168},"initUrl",[72,2286,262],{"class":299},[72,2288,211],{"class":172},[72,2290,1348],{"class":180},[72,2292,202],{"class":299},[72,2294,1353],{"class":247},[72,2296,1356],{"class":164},[72,2298,1359],{"class":168},[72,2300,211],{"class":172},[72,2302,214],{"class":180},[72,2304,217],{"class":299},[72,2306,2308,2311,2314,2316,2319,2321,2323],{"class":74,"line":2307},53,[72,2309,2310],{"class":176},"    return",[72,2312,2313],{"class":180}," mergeFmp4",[72,2315,202],{"class":299},[72,2317,2318],{"class":168},"initBytes",[72,2320,251],{"class":172},[72,2322,2032],{"class":168},[72,2324,465],{"class":299},[72,2326,2328,2331,2334],{"class":74,"line":2327},54,[72,2329,2330],{"class":172},"  }",[72,2332,2333],{"class":176}," else",[72,2335,265],{"class":172},[72,2337,2339,2341,2344,2346,2348,2351],{"class":74,"line":2338},55,[72,2340,2310],{"class":176},[72,2342,2343],{"class":180}," mergeTsSegments",[72,2345,202],{"class":299},[72,2347,248],{"class":168},[72,2349,2350],{"class":299},")  ",[72,2352,2353],{"class":222},"\u002F\u002F TS 디먹싱 필요, FFmpeg WASM 또는 mux.js\n",[72,2355,2357],{"class":74,"line":2356},56,[72,2358,1937],{"class":172},[72,2360,2362],{"class":74,"line":2361},57,[72,2363,471],{"class":172},[11,2365,2366,976,2369,2372,2373,2377],{},[20,2367,2368],{},"parallelFetch",[20,2370,2371],{},"Promise.all","과 워커 풀 패턴——",[14,2374,2376],{"href":2375},"\u002Fko\u002Fblog\u002Fdash-mpd-deep-dive-segmenttemplate-contentprotection","DASH 글","에서 같은 패턴을 설명.",[28,2379,2380],{"id":2380},"마치며",[11,2382,2383],{},"HLS는 \"그냥 텍스트 파일 파싱\"이라고 생각하기 쉽지만, 프로덕션 스트림에는 암호화, 멀티 트랙, discontinuity, 라이브 갱신이 섞여 있다. 이 모든 것을 다루는 견고한 다운로더는 몇백 줄 코드로는 안 된다. 하지만 핵심 상태 기계——현재 키, 현재 IV, 마지막 PTS, 마지막 discontinuity 시퀀스——를 올바르게 다루면 대부분의 스트림을 처리할 수 있다.",[11,2385,2386,2389,2390,1052,2393,211],{},[14,2387,2388],{"href":16},"HLS 입문 가이드","는 \"이게 뭔가\"를, 이 글은 \"진짜로 동작시키려면 뭘 알아야 하나\"를 다룬다. 다음은 ",[14,2391,2392],{"href":988},"fMP4 세그먼트를 결합하는 법",[14,2394,2396],{"href":2395},"\u002Fko\u002Fblog\u002Fwebcodecs-web-workers-opfs-video-processing","WebCodecs로 더 빠른 경로",[2398,2399],"hr",{},[28,2401,2403],{"id":2402},"추천-글","추천 글",[2405,2406,2407,2415,2423,2431,2440,2448],"ul",{},[39,2408,2409,2414],{},[42,2410,2411],{},[14,2412,2413],{"href":16},"HLS\u002FM3U8 스트리밍이란?기본 가이드","——이 글의 입문판",[39,2416,2417,2422],{},[42,2418,2419],{},[14,2420,2421],{"href":2375},"DASH 딥: SegmentTemplate, ContentProtection, 시점 전환 MPD","——DASH 쪽의 대응물. 두 포맷의 트레이드오프를 이해",[39,2424,2425,2430],{},[42,2426,2427],{},[14,2428,2429],{"href":988},"브라우저에서 비디오 리먹스: fMP4, ISOBMFF, 트랜스코딩하지 않는 이유","——fMP4 세그먼트를 결합하는 방법",[39,2432,2433,2439],{},[42,2434,2435],{},[14,2436,2438],{"href":2437},"\u002Fko\u002Fblog\u002Fhow-flowpick-merges-video-segments-in-browser","FlowPick은 어떻게 브라우저에서 수백 개의 비디오 세그먼트를 하나의 MP4로 합치는가","——이 글의 패턴이 FlowPick에서 어떻게 구현되는가",[39,2441,2442,2447],{},[42,2443,2444],{},[14,2445,2446],{"href":685},"스트리밍 다운로드는 합법인가?기술과 법적 체크리스트","——SAMPLE-AES가 하드한 선인 이유",[39,2449,2450,2456],{},[42,2451,2452],{},[14,2453,2455],{"href":2454},"\u002Fko\u002Fblog\u002Fbrowser-video-processing-performance-benchmarks","브라우저 비디오 처리 성능 벤치마크","——FFmpeg WASM, WebCodecs, 네이티브의 풀 벤치마크",[2458,2459,2460],"style",{},"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);}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 .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}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 pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":68,"searchDepth":81,"depth":81,"links":2462},[2463,2464,2465,2466,2470,2472,2473,2475,2476,2477,2478,2479,2480,2481,2482],{"id":30,"depth":81,"text":31},{"id":608,"depth":81,"text":609},{"id":665,"depth":81,"text":666},{"id":690,"depth":81,"text":2467,"children":2468},"멀티 오디오 트랙: #EXT-X-MEDIA",[2469],{"id":781,"depth":87,"text":782},{"id":825,"depth":81,"text":2471},"#EXT-X-DISCONTINUITY: 그 다음은 뭘까",{"id":904,"depth":81,"text":907},{"id":934,"depth":81,"text":2474},"#EXT-X-MAP: fMP4 세그먼트용",{"id":996,"depth":81,"text":999},{"id":1033,"depth":81,"text":1036},{"id":1077,"depth":81,"text":1062},{"id":1103,"depth":81,"text":1104},{"id":1193,"depth":81,"text":1194},{"id":1300,"depth":81,"text":1301},{"id":2380,"depth":81,"text":2380},{"id":2402,"depth":81,"text":2403},"tips","2026-08-04","M3U8 입문을 지나 암호화, 멀티 오디오\u002F자막 트랙, DISCONTINUITY와 Independent Segment, 그리고 스트리밍 다운로드를 실제로 지원하는 EXT-X 태그를 살펴본다.","md","\u002Fscreenshots\u002Fhls-downloader.png",{},"\u002Fko\u002Fblog\u002Fhls-m3u8-deep-dive-encryption-multitrack",{"title":5,"description":2485},"ko\u002Fblog\u002Fhls-m3u8-deep-dive-encryption-multitrack",[2493,67,2494,2495,2496,2497],"hls","암호화","멀티트랙","딥","스트리밍","dSG-Z6Zhmd1SwkEDG0Z14oqVw4ET-CGsnsyA2gv9EVs",[2500,2506],{"title":2501,"path":16,"stem":2502,"description":2503,"date":2504,"category":2505,"children":-1},"M3U8\u002FHLS 스트리밍 다운로드 방법: 완전 입문 가이드","ko\u002Fblog\u002Fgetting-started-with-hls-m3u8-download","브라우저에서 M3U8 및 HLS 동영상을 다운로드하는 방법을 안내합니다. 재생목록 URL 확인, MP4·TS 선택, CORS와 세그먼트 오류 해결, FlowPick 로컬 저장까지 다룹니다.","2026-07-01","tutorials",{"title":2507,"path":2437,"stem":2508,"description":2509,"date":2510,"category":2483,"children":-1},"FlowPick이 브라우저에서 수백 개 동영상 세그먼트를 MP4로 병합하는 방법","ko\u002Fblog\u002Fhow-flowpick-merges-video-segments-in-browser","서버도, FFmpeg 설치도, 트랜스코딩도 없이 — FlowPick이 브라우저 탭 내에서 HLS\u002FDASH 세그먼트 병합을 완료하는 전체 기술 경로를 분석합니다.","2026-07-02",1787670593250]