[{"data":1,"prerenderedAt":1206},["ShallowReactive",2],{"blog-ko-browser-streaming-remux-mp4-isobmff":3,"blog-ko-browser-streaming-remux-mp4-isobmff-surround":1194},{"id":4,"title":5,"author":6,"body":7,"category":1178,"date":1179,"description":1180,"extension":1181,"image":1182,"lastModified":1179,"meta":1183,"navigation":301,"path":1184,"readingTime":474,"seo":1185,"stem":1186,"tags":1187,"__hash__":1193},"blog_ko\u002Fko\u002Fblog\u002Fbrowser-streaming-remux-mp4-isobmff.md","브라우저에서 비디오 리먹스: fMP4, ISOBMFF, 그리고 트랜스코딩하지 않는 이유","FlowPick 팀",{"type":8,"value":9,"toc":1160},"minimark",[10,14,17,22,29,35,48,51,57,61,64,67,78,88,101,105,108,111,138,144,150,154,157,182,185,592,595,627,634,637,641,644,692,698,705,714,720,733,742,748,753,759,762,784,793,797,800,806,809,840,853,857,863,876,889,893,899,913,919,923,926,1000,1003,1012,1016,1070,1073,1076,1082,1085,1096,1099,1103,1156],[11,12,13],"p",{},"\"리먹스\"와 \"트랜스코딩\"을 혼용하는 사람이 많다. 이 글의 전부다. 이 둘은 완전히 다른 작업——리먹스는 빠르고 트랜스코딩은 느리고, 브라우저에서는 리먹스가 90%의 문제를 해결하며 트랜스코딩은 보통 피해야 할 느긋한 일이다.",[11,15,16],{},"이 글은 MP4 컨테이너가 내부에서 어떻게 생겼는지, 분할화 MP4(fMP4)가 왜 존재하고 HLS\u002FDASH 스트림에서 그냥 붙여서 하나의 파일로 만드는 게 가능한지, 그리고 FFmpeg WASM은 정확히 언제 필요한지를 다룬다.",[18,19,21],"h2",{"id":20},"리먹스-vs-트랜스코딩","리먹스 vs. 트랜스코딩",[11,23,24,28],{},[25,26,27],"strong",{},"트랜스코딩","은 인코딩된 비디오\u002F오디오 스트림을 디코딩해서 새로운 codec으로 재인코딩. H.264 비디오를 VP9로 바꾸면 트랜스코딩. 1080p 비디오를 720p로 스케일하면 트랜스코딩. 매 프레임을 디코딩하고 새로 인코딩하므로 느리다——보통 실시간 이상, 심하면 그 이상.",[11,30,31,34],{},[25,32,33],{},"리먹스","는 컨테이너만 바꾼다. 인코딩된 비디오\u002F오디오 데이터는 그대로, 다른 패키징으로 옮긴다. TS의 H.264 스트림을 MP4의 H.264 스트림으로 바꾸면 리먹스. 작업은:",[36,37,38,42,45],"ol",{},[39,40,41],"li",{},"날것의 NAL unit(인코딩된 H.264 데이터)을 TS 패킷에서 꺼낸다",[39,43,44],{},"TS 타임스탬프를 MP4 타임스탬프로 변환",[39,46,47],{},"MP4 박스 구조를 빌드하고 그 안에 NAL unit을 넣는다",[11,49,50],{},"비디오 데이터를 디코딩하지 않는다. 그래서 거의 순간——보통 동일한 codec이라면 1분 비디오를 수초로 끝낸다.",[11,52,53,56],{},[25,54,55],{},"핵심 통찰:"," HLS와 DASH 스트림은 이미 비디오가 H.264나 H.265로 인코딩되어 있다. MP4 컨테이너를 원하면? 리먹스하면 된다. 트랜스코딩은 필요없다.",[18,58,60],{"id":59},"isobmff-기본","ISOBMFF 기본",[11,62,63],{},"MP4 파일 포맷은 ISO Base Media File Format(ISOBMFF)에 정의. 모든 것은 \"박스\"——4바이트 크기 + 4바이트 타입 + 데이터——의 중첩.",[11,65,66],{},"전형적인 단일 파일 MP4(프로그레시브, 스트리밍이 아닌):",[68,69,74],"pre",{"className":70,"code":72,"language":73},[71],"language-text","ftyp     # 파일 타입 식별자\nmoov     # 메타데이터 컨테이너\n  mvhd   # 전체 헤더 (타임스케일, 길이)\n  trak   # 트랙 1 (비디오)\n    tkhd # 트랙 헤더\n    mdia # 미디어 정보\n      mdhd\n      hdlr\n      minf\n        stbl # 샘플 테이블\n          stsd # 샘플 설명 (codec 파라미터)\n          stts # 디코딩-표시 시간 변환\n          stsz # 프레임 크기\n          stsc # 청크-샘플 매핑\n          stco # 청크 오프셋\n  trak   # 트랙 2 (오디오)\n    ...\nmdat     # 실제 미디어 데이터\n","text",[75,76,72],"code",{"__ignoreMap":77},"",[11,79,80,83,84,87],{},[75,81,82],{},"stbl"," 안의 박스가 핵심: 비디오\u002F오디오 데이터가 ",[75,85,86],{},"mdat","에 어디 있는지, 어느 프레임이 몇 바이트인지, DTS\u002FPTS가 어떻게 매핑되는지를 모두 알려준다.",[11,89,90,93,94,96,97,100],{},[25,91,92],{},"스트리밍 문제:"," 이 구조는 ",[75,95,86],{},"의 모든 바이트 오프셋을 ",[75,98,99],{},"stco","가 알아야 한다고 가정. 파일을 스트리밍하면? 아직 존재하지 않는 프레임의 오프셋을 쓸 수 없다. 이것이 \"MP4는 스트리밍을 못 한다\"는 말의 진짜 의미——프로그레시브 MP4는 완전한 파일이 있어야 올바르고, 파일 생성 중엔 메타데이터를 쓸 수 없다.",[18,102,104],{"id":103},"fmp4-스트리밍-mp4","fMP4: 스트리밍 MP4",[11,106,107],{},"분할화 MP4(fMP4)는 메타데이터를 분산시킨다. 파일의 시작이나 각 세그먼트마다 메타데이터가 있어서 스트리밍을 가능하게 한다.",[11,109,110],{},"fMP4 스트림은 2가지 종류의 파일로 구성:",[36,112,113,127],{},[39,114,115,118,119,122,123,126],{},[25,116,117],{},"초기화 세그먼트(init segment)","——",[75,120,121],{},"ftyp"," + ",[75,124,125],{},"moov"," 박스. 모든 트랙 메타데이터, codec 파라미터, 타임스케일 포함. 미디어 데이터 없음. 보통 몇 KB.",[39,128,129,118,132,122,135,137],{},[25,130,131],{},"미디어 세그먼트(media segment)",[75,133,134],{},"moof",[75,136,86],{},". 이 세그먼트의 프레임을 위한 메타데이터 포함. 각 세그먼트는 자기 자신만으로 디코딩 가능.",[68,139,142],{"className":140,"code":141,"language":73},[71],"init.mp4:\n  ftyp\n  moov\n    mvhd, trak, trak, ...\n\nsegment-1.m4s:\n  styp   # 세그먼트 타입\n  moof    # 무비 프래그먼트 헤더\n    mfhd  # 프래그먼트 시퀀스 번호\n    traf  # 트랙 프래그먼트\n      tfhd # 트랙 헤더\n      tfdt # 프래그먼트 디코딩 시간\n      trun # 트랙 런 (이 세그먼트의 샘플)\n  mdat    # 실제 미디어 데이터\n",[75,143,141],{"__ignoreMap":77},[11,145,146,147,149],{},"핵심: 각 ",[75,148,134],{}," 박스는 그 세그먼트 내 프레임의 오프셋과 크기를 모두 알려준다. init을 갖고 있으면 각 세그먼트는 독립적으로 디코딩 가능.",[18,151,153],{"id":152},"왜-fmp4-세그먼트를-그냥-붙이면-되는가","왜 fMP4 세그먼트를 그냥 붙이면 되는가",[11,155,156],{},"DASH나 HLS-fMP4 스트림을 다운로드할 때, 받는 건:",[36,158,159,169],{},[39,160,161,164,165,168],{},[75,162,163],{},"init.mp4"," (또는 ",[75,166,167],{},"init.m4s",")",[39,170,171,174,175,174,178,181],{},[75,172,173],{},"1.m4s",", ",[75,176,177],{},"2.m4s",[75,179,180],{},"3.m4s","…… N개의 미디어 세그먼트",[11,183,184],{},"재생 가능한 MP4 파일은 init + 모든 미디어 세그먼트를 순서대로 결합하면 된다:",[68,186,190],{"className":187,"code":188,"language":189,"meta":77,"style":77},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","async function mergeFmp4(initUrl, segmentUrls) {\n  const init = new Uint8Array(await (await fetch(initUrl)).arrayBuffer())\n  const chunks = [init]\n\n  for (const url of segmentUrls) {\n    chunks.push(new Uint8Array(await (await fetch(url)).arrayBuffer()))\n  }\n\n  const total = chunks.reduce((s, c) => s + c.byteLength, 0)\n  const out = new Uint8Array(total)\n  let offset = 0\n  for (const chunk of chunks) {\n    out.set(chunk, offset)\n    offset += chunk.byteLength\n  }\n\n  return new Blob([out], { type: 'video\u002Fmp4' })\n}\n","js",[75,191,192,227,277,296,303,328,370,376,381,437,458,472,492,514,530,535,540,586],{"__ignoreMap":77},[193,194,197,201,204,208,212,216,219,222,224],"span",{"class":195,"line":196},"line",1,[193,198,200],{"class":199},"spNyl","async",[193,202,203],{"class":199}," function",[193,205,207],{"class":206},"s2Zo4"," mergeFmp4",[193,209,211],{"class":210},"sMK4o","(",[193,213,215],{"class":214},"sHdIc","initUrl",[193,217,218],{"class":210},",",[193,220,221],{"class":214}," segmentUrls",[193,223,168],{"class":210},[193,225,226],{"class":210}," {\n",[193,228,230,233,237,240,243,246,249,253,256,258,261,263,265,268,271,274],{"class":195,"line":229},2,[193,231,232],{"class":199},"  const",[193,234,236],{"class":235},"sTEyZ"," init",[193,238,239],{"class":210}," =",[193,241,242],{"class":210}," new",[193,244,245],{"class":206}," Uint8Array",[193,247,211],{"class":248},"swJcz",[193,250,252],{"class":251},"s7zQu","await",[193,254,255],{"class":248}," (",[193,257,252],{"class":251},[193,259,260],{"class":206}," fetch",[193,262,211],{"class":248},[193,264,215],{"class":235},[193,266,267],{"class":248},"))",[193,269,270],{"class":210},".",[193,272,273],{"class":206},"arrayBuffer",[193,275,276],{"class":248},"())\n",[193,278,280,282,285,287,290,293],{"class":195,"line":279},3,[193,281,232],{"class":199},[193,283,284],{"class":235}," chunks",[193,286,239],{"class":210},[193,288,289],{"class":248}," [",[193,291,292],{"class":235},"init",[193,294,295],{"class":248},"]\n",[193,297,299],{"class":195,"line":298},4,[193,300,302],{"emptyLinePlaceholder":301},true,"\n",[193,304,306,309,311,314,317,320,322,325],{"class":195,"line":305},5,[193,307,308],{"class":251},"  for",[193,310,255],{"class":248},[193,312,313],{"class":199},"const",[193,315,316],{"class":235}," url",[193,318,319],{"class":210}," of",[193,321,221],{"class":235},[193,323,324],{"class":248},") ",[193,326,327],{"class":210},"{\n",[193,329,331,334,336,339,341,344,346,348,350,352,354,356,358,361,363,365,367],{"class":195,"line":330},6,[193,332,333],{"class":235},"    chunks",[193,335,270],{"class":210},[193,337,338],{"class":206},"push",[193,340,211],{"class":248},[193,342,343],{"class":210},"new",[193,345,245],{"class":206},[193,347,211],{"class":248},[193,349,252],{"class":251},[193,351,255],{"class":248},[193,353,252],{"class":251},[193,355,260],{"class":206},[193,357,211],{"class":248},[193,359,360],{"class":235},"url",[193,362,267],{"class":248},[193,364,270],{"class":210},[193,366,273],{"class":206},[193,368,369],{"class":248},"()))\n",[193,371,373],{"class":195,"line":372},7,[193,374,375],{"class":210},"  }\n",[193,377,379],{"class":195,"line":378},8,[193,380,302],{"emptyLinePlaceholder":301},[193,382,384,386,389,391,393,395,398,400,402,405,407,410,412,415,418,421,423,425,428,430,434],{"class":195,"line":383},9,[193,385,232],{"class":199},[193,387,388],{"class":235}," total",[193,390,239],{"class":210},[193,392,284],{"class":235},[193,394,270],{"class":210},[193,396,397],{"class":206},"reduce",[193,399,211],{"class":248},[193,401,211],{"class":210},[193,403,404],{"class":214},"s",[193,406,218],{"class":210},[193,408,409],{"class":214}," c",[193,411,168],{"class":210},[193,413,414],{"class":199}," =>",[193,416,417],{"class":235}," s",[193,419,420],{"class":210}," +",[193,422,409],{"class":235},[193,424,270],{"class":210},[193,426,427],{"class":235},"byteLength",[193,429,218],{"class":210},[193,431,433],{"class":432},"sbssI"," 0",[193,435,436],{"class":248},")\n",[193,438,440,442,445,447,449,451,453,456],{"class":195,"line":439},10,[193,441,232],{"class":199},[193,443,444],{"class":235}," out",[193,446,239],{"class":210},[193,448,242],{"class":210},[193,450,245],{"class":206},[193,452,211],{"class":248},[193,454,455],{"class":235},"total",[193,457,436],{"class":248},[193,459,461,464,467,469],{"class":195,"line":460},11,[193,462,463],{"class":199},"  let",[193,465,466],{"class":235}," offset",[193,468,239],{"class":210},[193,470,471],{"class":432}," 0\n",[193,473,475,477,479,481,484,486,488,490],{"class":195,"line":474},12,[193,476,308],{"class":251},[193,478,255],{"class":248},[193,480,313],{"class":199},[193,482,483],{"class":235}," chunk",[193,485,319],{"class":210},[193,487,284],{"class":235},[193,489,324],{"class":248},[193,491,327],{"class":210},[193,493,495,498,500,503,505,508,510,512],{"class":195,"line":494},13,[193,496,497],{"class":235},"    out",[193,499,270],{"class":210},[193,501,502],{"class":206},"set",[193,504,211],{"class":248},[193,506,507],{"class":235},"chunk",[193,509,218],{"class":210},[193,511,466],{"class":235},[193,513,436],{"class":248},[193,515,517,520,523,525,527],{"class":195,"line":516},14,[193,518,519],{"class":235},"    offset",[193,521,522],{"class":210}," +=",[193,524,483],{"class":235},[193,526,270],{"class":210},[193,528,529],{"class":235},"byteLength\n",[193,531,533],{"class":195,"line":532},15,[193,534,375],{"class":210},[193,536,538],{"class":195,"line":537},16,[193,539,302],{"emptyLinePlaceholder":301},[193,541,543,546,548,551,554,557,560,562,565,568,571,574,578,581,584],{"class":195,"line":542},17,[193,544,545],{"class":251},"  return",[193,547,242],{"class":210},[193,549,550],{"class":206}," Blob",[193,552,553],{"class":248},"([",[193,555,556],{"class":235},"out",[193,558,559],{"class":248},"]",[193,561,218],{"class":210},[193,563,564],{"class":210}," {",[193,566,567],{"class":248}," type",[193,569,570],{"class":210},":",[193,572,573],{"class":210}," '",[193,575,577],{"class":576},"sfazB","video\u002Fmp4",[193,579,580],{"class":210},"'",[193,582,583],{"class":210}," }",[193,585,436],{"class":248},[193,587,589],{"class":195,"line":588},18,[193,590,591],{"class":210},"}\n",[11,593,594],{},"이게 진짜로 동작하는 이유:",[36,596,597,603,613,621],{},[39,598,599,600,602],{},"init은 ",[75,601,125],{},"에 트랙 메타데이터를 갖고 있음",[39,604,605,606,608,609,612],{},"각 ",[75,607,134],{},"는 그 세그먼트의 샘플을 가리키는 ",[75,610,611],{},"trun","을 가짐",[39,614,615,617,618,620],{},[75,616,611],{},"의 오프셋은 각 세그먼트의 ",[75,619,134],{},"에 대해 상대적——그래서 init을 파일 앞에 놓고 세그먼트를 순서대로 놓으면, 전체 파일의 오프셋은 정확히 맞는다(이래서 작동)",[39,622,623,626],{},[75,624,625],{},"tfdt","는 각 세그먼트의 시작 디코딩 시간을 준다. 즉, 세그먼트는 순서대로 놓기만 하면 됨",[11,628,629,630,633],{},"이게 DASH\u002FHLS-fMP4 다운로드가 거의 무료인 이유. 세그먼트를 페치하고 ",[75,631,632],{},"Blob","로 합치고 끝. FFmpeg 불필요.",[11,635,636],{},"이게 FlowPick의 기본 경로——DASH 스트림의 90%가 fMP4 세그먼트를 사용하므로, 다운로드는 \"페치하고 합치고\"가 핵심 로직. FFmpeg WASM이 필요 없어 다운로드 사이즈가 25MB 적고, 속도가 빠르다.",[18,638,640],{"id":639},"그럼-ffmpeg-wasm은-언제-필요한가","그럼 FFmpeg WASM은 언제 필요한가",[11,642,643],{},"다음은 FFmpeg WASM 없이 처리 곤란한 것:",[36,645,646,656,674,680,686],{},[39,647,648,651,652,655],{},[25,649,650],{},"HLS-MPEG-TS 스트림."," TS 패킷(188바이트 고정)을 디먹싱해서 H.264 NAL unit을 빼고 PTS\u002FDTS를 추출해서 fMP4 박스를 빌드해야 한다. 직접 파서를 만들 수도 있고, ",[75,653,654],{},"mux.js","(npm)를 쓸 수도 있고, FFmpeg WASM에 맡길 수도 있다. 마지막이 가장 쉽고 신뢰할 수 있다.",[39,657,658,661,662,665,666,669,670,673],{},[25,659,660],{},"오디오\u002F비디오 인터리빙."," 비디오 세그먼트와 오디오 세그먼트가 따로 오는 경우(분할 트랙 DASH에서 흔함), 결합은 두 트랙을 시간순으로 인터리브하는 MP4를 만들어야 한다. fMP4로 가능하지만 까다롭고 ",[75,663,664],{},"tfhd","의 ",[75,667,668],{},"default-sample-flags"," 같은 박스 이해가 필요. FFmpeg은 ",[75,671,672],{},"-i video.mp4 -i audio.mp4 -c copy out.mp4","로 끝.",[39,675,676,679],{},[25,677,678],{},"codec 변환."," H.265를 H.264로 바꾸는 등. 이것은 트랜스코딩이지 리먹스가 아니다. FFmpeg WASM으로 가능하지만 느리고 브라우저에서 이걸 하는 건 미친 짓이다——사용자의 CPU를 태우는 것이고, 브라우저 탭은 보통 우선순위가 낮아 스로틀된다.",[39,681,682,685],{},[25,683,684],{},"컨테이너 변환."," MKV를 MP4로, FLV를 MP4로. 날것의 codec 데이터를 다른 컨테이너 포맷으로 다시 패키징. FFmpeg은 이걸 잘한다.",[39,687,688,691],{},[25,689,690],{},"필터 체인."," 회전 메타데이터 정정, 오디오 지연 보정, 자막 트랙 삽입. 이런 작업은 FFmpeg 필터 그래프가 만들어져 있다.",[11,693,694,697],{},[25,695,696],{},"간단한 결론:"," fMP4 세그먼트를 다루고 있고, codec을 안 바꾸고, 트랙 인터리빙이 간단하거나 없으면 → FFmpeg 불필요. 직접 결합. 그 외 → FFmpeg WASM.",[18,699,701,704],{"id":700},"styp-박스는-뭘까",[75,702,703],{},"styp"," 박스는 뭘까",[11,706,707,708,710,711,713],{},"각 미디어 세그먼트는 ",[75,709,134],{}," 앞에 ",[75,712,703],{}," 박스가 있다:",[68,715,718],{"className":716,"code":717,"language":73},[71],"00 00 00 18 73 74 79 70 6D 73 64 68 00 00 00 00 6D 73 64 68 6D 70 34 64 69 73 6F 6D\n                s  t  y  p  m  s  d  h              m  s  d  h  m  p  4  d  i  s  o  m\n",[75,719,717],{"__ignoreMap":77},[11,721,722,724,725,728,729,732],{},[75,723,703],{},"는 \"segment type box\"——이 파일이 fMP4 세그먼트임을 선언. ",[75,726,727],{},"msdh"," (movie fragment, random access 가능)는 \"이 세그먼트는 단독으로 디코딩 가능\"을 의미. ",[75,730,731],{},"mp4diso","는 MP4 ISO 호환.",[11,734,735,736,738,739,741],{},"이건 결합 시 무시 가능——init에 이미 ",[75,737,121],{},"가 있으므로. 하지만 일부 플레이어는 ",[75,740,703],{},"를 좋아하므로 남겨두는 게 안전.",[18,743,745,747],{"id":744},"trun-박스-샘플-테이블",[75,746,611],{}," 박스: 샘플 테이블",[11,749,750,752],{},[75,751,611],{},"(track run) 박스가 각 세그먼트 내 프레임을 기술:",[68,754,757],{"className":755,"code":756,"language":73},[71],"aligned(8) class TrackRunBox\n  extends FullBox('trun', version, flags) {\n  unsigned int(32) sample_count;\n  \u002F\u002F flags에 따라:\n  for (i=0; i\u003Csample_count; i++) {\n    unsigned int(32) data_offset;        \u002F\u002F flag 0x000001\n    unsigned int(32) first_sample_flags; \u002F\u002F flag 0x000004\n    \u002F\u002F 각 샘플:\n    unsigned int(32) sample_duration;   \u002F\u002F flag 0x000100\n    unsigned int(32) sample_size;       \u002F\u002F flag 0x000200\n    unsigned int(32) sample_flags;      \u002F\u002F flag 0x000400\n    unsigned int(32) sample_composition_time_offset; \u002F\u002F flag 0x000800\n  }\n}\n",[75,758,756],{"__ignoreMap":77},[11,760,761],{},"이게 MP4가 각 프레임을 \"어느 샘플이 몇 바이트이고, 얼마나 오래 재생하고, DTS는 어디고, PTS 오프셋은 어디인지\"를 아는 방법. 효율적이지만 읽기 어려운 포맷.",[11,763,764,767,768,770,771,773,774,777,778,780,781,783],{},[25,765,766],{},"결합 시 왜 이게 중요한가:"," 다중 세그먼트를 결합하면, 각 ",[75,769,611],{},"의 오프셋은 여전히 자기 ",[75,772,134],{},"에 대해 상대적. ",[75,775,776],{},"data_offset"," 필드는 ",[75,779,134],{}," 끝에서 ",[75,782,86],{}," 데이터 시작까지의 거리——이것은 세그먼트 내에서 변하지 않으므로 결합 후에도 그대로.",[11,785,786,787,789,790,792],{},"하지만 ",[75,788,625],{}," 박스——\"track fragment decode time\"——은 세그먼트의 절대 시작 시간. 세그먼트를 순서대로 놓으면 ",[75,791,625],{},"도 단조 증가. 결합된 파일은 유효.",[18,794,796],{"id":795},"멀티-트랙-fmp4의-결합","멀티 트랙 fMP4의 결합",[11,798,799],{},"비디오와 오디오가 별개 세그먼트로 오는 경우:",[68,801,804],{"className":802,"code":803,"language":73},[71],"init-video.mp4   video-1.m4s  video-2.m4s ...\ninit-audio.mp4   audio-1.m4s  audio-2.m4s ...\n",[75,805,803],{"__ignoreMap":77},[11,807,808],{},"다운로드는 각 트랙의 세그먼트를 따로 페치한 후 결합:",[36,810,811,823,834],{},[39,812,813,122,816,819,820],{},[75,814,815],{},"init-video.mp4",[75,817,818],{},"video-N.m4s","를 순서대로 → ",[75,821,822],{},"video.mp4",[39,824,825,122,828,819,831],{},[75,826,827],{},"init-audio.mp4",[75,829,830],{},"audio-N.m4s",[75,832,833],{},"audio.mp4",[39,835,836,837],{},"두 파일을 멀티플렉스: ",[75,838,839],{},"ffmpeg -i video.mp4 -i audio.mp4 -c copy out.mp4",[11,841,842,843,845,846,848,849,852],{},"1, 2단계는 순수 JS로 가능(위의 결합 코드). 3단계는 FFmpeg이 필요, 각 트랙의 ",[75,844,611],{}," 테이블을 재작성해서 단일 ",[75,847,125],{}," 아래 양쪽 ",[75,850,851],{},"trak","으로 합쳐야 한다. 직접 하려면 박스 레벨 조작이 필요하고, FFmpeg이 더 쉽다.",[18,854,856],{"id":855},"결합이-실패하는-경우","결합이 실패하는 경우",[11,858,859,862],{},[25,860,861],{},"실패 케이스 1: codec 파라미터 변화."," 일부 라이브 스트림은 인코딩 중 SPS\u002FPPS를 바꾼다(해상도 변경, IDR 프레임에서 codec 프로파일 변경). init 세그먼트가 첫 번째 codec 파라미터를 기술하면, 이후 세그먼트는 다를 수 있다. 결합된 파일은 첫 부분은 재생되다가 변경 이후에 글리치.",[11,864,865,868,869,871,872,875],{},[25,866,867],{},"실패 케이스 2: 세그먼트 번호 불연속."," 라이브 스트림에서 세그먼트 번호가 건너뛰면 (서버가 죽었다가 살아나면), ",[75,870,625],{}," 시간은 연속이지만 ",[75,873,874],{},"mfhd"," 시퀀스는 건너뛴다. 대부분의 플레이어는 신경 안 쓰지만 일부는 고집.",[11,877,878,881,882,884,885,888],{},[25,879,880],{},"실패 케이스 3: 타임스탬프 겹침."," 일부 인코더는 ",[75,883,625],{},"가 정확히 연속이 아닌 미디어를 만든다 (1-2 프레임 겹침). 결합하면 두 프레임이 같은 시간을 가리켜 디코더가 혼란. 수정은 세그먼트 사이의 타임스탬프를 조정——FFmpeg은 ",[75,886,887],{},"mkvmerge","로 가능하지만 fMP4 결합은 직접 조정이 필요.",[18,890,892],{"id":891},"코딩-시-주의할-것","코딩 시 주의할 것",[11,894,895,898],{},[25,896,897],{},"바이트 정렬."," ISOBMFF 박스는 8바이트 정렬(대부분). 박스를 읽거나 쓸 때는 정렬을 유지. 특정 박스는 4바이트 정렬도 있으므로 사양을 확인.",[11,900,901,904,905,908,909,912],{},[25,902,903],{},"엔디안."," 모든 ISOBMFF 정수는 빅 엔디언. JS의 ",[75,906,907],{},"DataView","는 기본이 빅 엔디언이라 편리, 그냥 ",[75,910,911],{},"getUint32(offset)","를 쓰면 된다.",[11,914,915,918],{},[25,916,917],{},"박스 크기 특수값."," 크기가 0이면 \"파일 끝까지\". 크기가 1이면 다음 8바이트가 64비트 크기. 항상 확인.",[18,920,922],{"id":921},"벤치마크-순-js-결합-vs-ffmpeg-wasm-vs-네이티브","벤치마크: 순 JS 결합 vs FFmpeg WASM vs 네이티브",[11,924,925],{},"동일 워크로드——300개 fMP4 세그먼트 결합, 약 500MB:",[927,928,929,948],"table",{},[930,931,932],"thead",{},[933,934,935,939,942,945],"tr",{},[936,937,938],"th",{},"수단",[936,940,941],{},"실측 시간",[936,943,944],{},"피크 메모리",[936,946,947],{},"다운로드 사이즈",[949,950,951,966,984],"tbody",{},[933,952,953,957,960,963],{},[954,955,956],"td",{},"순 JS 결합",[954,958,959],{},"0.4s",[954,961,962],{},"510MB",[954,964,965],{},"0",[933,967,968,975,978,981],{},[954,969,970,971,974],{},"FFmpeg WASM (transcode 아님, ",[75,972,973],{},"-c copy"," 리먹스)",[954,976,977],{},"4.2s",[954,979,980],{},"660MB",[954,982,983],{},"25MB",[933,985,986,991,994,997],{},[954,987,988,989,168],{},"네이티브 FFmpeg (",[75,990,973],{},[954,992,993],{},"0.6s",[954,995,996],{},"80MB",[954,998,999],{},"0(설치됨)",[11,1001,1002],{},"순 JS 결합이 가장 빠르다——FFmpeg은 WASM 초기화, FFmpeg 자체 메모리 관리, 멀티플렉스 오버헤드가 붙는다. 하지만 어디서나 작동, 인스톨 불필요, 510MB 피크 메모리는 큰편이지만 감당 가능.",[11,1004,1005,1006,1011],{},"전체 벤치마크는 ",[1007,1008,1010],"a",{"href":1009},"\u002Fko\u002Fblog\u002Fbrowser-video-processing-performance-benchmarks","브라우저 비디오 처리 성능 벤치마크","에서.",[18,1013,1015],{"id":1014},"참고-자료","참고 자료",[1017,1018,1019,1028,1036,1051,1063],"ul",{},[39,1020,1021,1027],{},[1007,1022,1026],{"href":1023,"rel":1024},"https:\u002F\u002Fwww.iso.org\u002Fstandard\u002F79110.html",[1025],"nofollow","ISO\u002FIEC 14496-12 — ISOBMFF 사양","——공식 사양, 유료",[39,1029,1030,1035],{},[1007,1031,1034],{"href":1032,"rel":1033},"https:\u002F\u002Fgithub.com\u002Fbbc\u002Fmp4san",[1025],"MP4 Sanitizer by BBC","——오픈소스 Rust ISOBMFF 파서. 깨끗한 참고 구현",[39,1037,1038,1043,1044,174,1047,1050],{},[1007,1039,1042],{"href":1040,"rel":1041},"https:\u002F\u002Fgithub.com\u002Faxiomatic-systems\u002FBento4",[1025],"Bento4","——C++ MP4 툴킷. CLI 도구 ",[75,1045,1046],{},"mp4info",[75,1048,1049],{},"mp4dump","가 디버깅에 편리",[39,1052,1053,1058,1059,1062],{},[1007,1054,1057],{"href":1055,"rel":1056},"https:\u002F\u002Fffmpeg.org\u002Fffmpeg-formats.html",[1025],"FFmpeg 문서","——포맷 레퍼런스, ",[75,1060,1061],{},"movflags"," 옵션의 의미",[39,1064,1065,1069],{},[1007,1066,654],{"href":1067,"rel":1068},"https:\u002F\u002Fgithub.com\u002Fvideojs\u002Fmux.js",[1025],"——video.js의 JS MPEG-TS\u002FFLV 파서, fMP4 생성기. FFmpeg 없이 처리하고 싶을 때",[18,1071,1072],{"id":1072},"정리",[11,1074,1075],{},"MP4 리먹스는 \"박스를 열어 데이터를 꺼내 다른 박스에 넣는 것\". 트랜스코딩은 박스 안의 데이터를 디코딩\u002F재인코딩. 둘의 작업량 차이는 100배——브라우저에서는 트랜스코딩은 피하고 리먹스를 택하라.",[11,1077,1078,1079,1081],{},"fMP4 스트림의 경우, 리먹스는 거의 \"세그먼트를 순서대로 붙이는 것\"이고, 박스 구조가 모든 메타데이터를 세그먼트마다 가지므로 결합은 그냥 ",[75,1080,632],{}," 결합. 직접 구현은 100줄 미만, 어느 브라우저에서든 작동.",[11,1083,1084],{},"FFmpeg WASM은 정말 필요할 때만——TS 디먹싱, 멀티 트랙 멀티플렉싱, 필터 체인, codec 변환——이런 시나리오의 가장 좋은 도구. 하지만 fMP4 세그먼트를 다운로드하고 있는 거라면, FFmpeg은 오버킬.",[11,1086,1087,1088,1092,1093,1011],{},"다음은 ",[1007,1089,1091],{"href":1090},"\u002Fko\u002Fblog\u002Fwebcodecs-web-workers-opfs-video-processing","WebCodecs로 더 빠른 디코딩 경로"," 또는 ",[1007,1094,1095],{"href":1009},"여러 코덱\u002F해상도의 벤치마크",[1097,1098],"hr",{},[18,1100,1102],{"id":1101},"추천-글","추천 글",[1017,1104,1105,1114,1123,1132,1139,1147],{},[39,1106,1107,1113],{},[25,1108,1109],{},[1007,1110,1112],{"href":1111},"\u002Fko\u002Fblog\u002Fhls-m3u8-deep-dive-encryption-multitrack","HLS 딥: 암호화, 멀티트랙, 기억해야 할 EXT-X 태그","——HLS의 TS 세그먼트가 왜 fMP4보다 다루기 어려운가",[39,1115,1116,1122],{},[25,1117,1118],{},[1007,1119,1121],{"href":1120},"\u002Fko\u002Fblog\u002Fdash-mpd-deep-dive-segmenttemplate-contentprotection","DASH 딥: SegmentTemplate, ContentProtection, 시점 전환 MPD","——fMP4 세그먼트의 공급원",[39,1124,1125,1131],{},[25,1126,1127],{},[1007,1128,1130],{"href":1129},"\u002Fko\u002Fblog\u002Fhow-flowpick-merges-video-segments-in-browser","FlowPick은 어떻게 브라우저에서 수백 개의 비디오 세그먼트를 하나의 MP4로 합치는가","——이 글의 패턴이 FlowPick에서 어떻게 쓰이는가",[39,1133,1134,1138],{},[25,1135,1136],{},[1007,1137,1010],{"href":1009},"——FFmpeg WASM, WebCodecs, 네이티브의 풀 벤치마크",[39,1140,1141,1146],{},[25,1142,1143],{},[1007,1144,1145],{"href":1090},"WebCodecs + Web Workers + OPFS: 브라우저 비디오 처리 실전","——FFmpeg을 아예 안 쓰는 더 빠른 경로",[39,1148,1149,1155],{},[25,1150,1151],{},[1007,1152,1154],{"href":1153},"\u002Fko\u002Fblog\u002Fis-it-legal-to-download-streaming-video","스트리밍 다운로드는 합법인가?기술과 법적 체크리스트","——DRM이 아닌 콘텐츠를 다운로드하는 법적 고려",[1157,1158,1159],"style",{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}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 .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}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 .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}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":77,"searchDepth":229,"depth":229,"links":1161},[1162,1163,1164,1165,1166,1167,1169,1171,1172,1173,1174,1175,1176,1177],{"id":20,"depth":229,"text":21},{"id":59,"depth":229,"text":60},{"id":103,"depth":229,"text":104},{"id":152,"depth":229,"text":153},{"id":639,"depth":229,"text":640},{"id":700,"depth":229,"text":1168},"styp 박스는 뭘까",{"id":744,"depth":229,"text":1170},"trun 박스: 샘플 테이블",{"id":795,"depth":229,"text":796},{"id":855,"depth":229,"text":856},{"id":891,"depth":229,"text":892},{"id":921,"depth":229,"text":922},{"id":1014,"depth":229,"text":1015},{"id":1072,"depth":229,"text":1072},{"id":1101,"depth":229,"text":1102},"tips","2026-08-04","비디오 리먹스와 트랜스코딩의 차이, 왜 fMP4 세그먼트를 그냥 붙이면 되는지, ISOBMFF 박스 구조를 살피고 브라우저에서 FFmpeg WASM을 언제 쓸지 판단.","md","\u002Fscreenshots\u002Fformat-conversion.png",{},"\u002Fko\u002Fblog\u002Fbrowser-streaming-remux-mp4-isobmff",{"title":5,"description":1180},"ko\u002Fblog\u002Fbrowser-streaming-remux-mp4-isobmff",[1188,1189,1190,33,1191,1192],"mp4","fmp4","isobmff","wasm","딥","tzWNZFSyENdd8N2ecpG1gZlcKsSzs341WczuTRtgdUg",[1195,1202],{"title":1196,"path":1197,"stem":1198,"description":1199,"date":1200,"category":1201,"children":-1},"웹페이지 이미지 일괄 다운로드 방법","\u002Fko\u002Fblog\u002Fbatch-download-images-from-any-website","ko\u002Fblog\u002Fbatch-download-images-from-any-website","FlowPick 내장 이미지 다운로더로 웹페이지의 이미지를 한 번에 추출하고 저장하세요. JPG·PNG·WebP 형식, 파일 크기와 리소스 필터, 일괄 다운로드 과정을 안내합니다.","2026-07-01","tutorials",{"title":1203,"path":1009,"stem":1204,"description":1205,"date":1179,"category":1178,"children":-1},"브라우저 비디오 처리 성능 벤치마크: FFmpeg WASM vs WebCodecs vs 네이티브","ko\u002Fblog\u002Fbrowser-video-processing-performance-benchmarks","실제 하드웨어에서의 실제 데이터——1080p\u002F4K\u002F8K 리먹스와 디코딩 벤치마크. FFmpeg WASM, WebCodecs, 네이티브 FFmpeg을 횡단. 방법론, 생 데이터, 숫자가 의미하는 바 포함.",1787670592792]