m3u8merge
Version:
A powerful and lightweight SDK for merging M3U8 playlist streams into complete video files. Supports HLS stream parsing, segment downloading, and seamless video concatenation with progress tracking.
15 lines (14 loc) • 16.3 kB
JavaScript
;const U=require("cross-spawn"),l=require("fs-extra"),b=require("path"),I=require("p-limit"),undici=require("undici");function _interopDefaultCompat(h){return h&&typeof h=="object"&&"default"in h?h.default:h}const U__default=_interopDefaultCompat(U),l__default=_interopDefaultCompat(l),b__default=_interopDefaultCompat(b),I__default=_interopDefaultCompat(I);var P=Object.defineProperty,x=(h,u,e)=>u in h?P(h,u,{enumerable:!0,configurable:!0,writable:!0,value:e}):h[u]=e,D=(h,u,e)=>(x(h,typeof u!="symbol"?u+"":u,e),e);class R{constructor(){D(this,"links"),D(this,"totalDuration"),D(this,"downloadMethod","undici"),D(this,"encryptionInfo"),this.links=[],this.totalDuration=0}parseM3U8Content(u){const e=u.split(`
`).map(n=>n.trim()).filter(n=>n);let t=0;console.log(`\u{1F3AC} \u5F00\u59CB\u89E3\u6790 M3U8 \u6587\u4EF6...
`);for(let n=0;n<e.length;n++){const o=e[n];if(o.startsWith("#EXT-X-KEY:"))this.parseEncryptionInfo(o);else if(o.startsWith("#EXTINF:")){const r=o.match(/#EXTINF:([\d.]+)/);r&&(t=parseFloat(r[1]))}else!o.startsWith("#")&&o.includes("http")&&(this.links.push({index:this.links.length+1,url:o,duration:t}),this.totalDuration+=t,t=0)}return this.links}parseEncryptionInfo(u){const e=u.match(/METHOD=([^,]+)/),t=u.match(/URI="([^"]+)"/),n=u.match(/IV=0x([a-fA-F0-9]+)/);e&&(this.encryptionInfo={method:e[1],keyUrl:t?t[1]:void 0,iv:n?n[1]:void 0},console.log(`\u{1F510} \u68C0\u6D4B\u5230\u52A0\u5BC6: ${this.encryptionInfo.method}`),this.encryptionInfo.keyUrl&&console.log(`\u{1F511} \u5BC6\u94A5URL: ${this.encryptionInfo.keyUrl}`),this.encryptionInfo.iv&&console.log(`\u{1F522} IV: ${this.encryptionInfo.iv}`))}async downloadDecryptionKey(){if(!this.encryptionInfo?.keyUrl)return null;try{console.log("\u{1F511} \u4E0B\u8F7D\u89E3\u5BC6\u5BC6\u94A5...");const{statusCode:u,body:e}=await undici.request(this.encryptionInfo.keyUrl,{method:"GET",headers:{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"},headersTimeout:3e4,bodyTimeout:3e4});if(u===200){const t=[];for await(const o of e)t.push(o);const n=Buffer.concat(t);return console.log(`\u2705 \u5BC6\u94A5\u4E0B\u8F7D\u6210\u529F (${n.length} bytes)`),n}}catch(u){console.error(`\u274C \u5BC6\u94A5\u4E0B\u8F7D\u5931\u8D25: ${u.message}`)}return null}async decryptTSData(u,e){if(this.encryptionInfo?.method!=="AES-128")return u;try{const t=await import("crypto");let n;this.encryptionInfo.iv?n=Buffer.from(this.encryptionInfo.iv,"hex"):n=Buffer.alloc(16,0);const o=t.createDecipheriv("aes-128-cbc",e,n);return Buffer.concat([o.update(u),o.final()])}catch(t){return console.error(`\u274C \u89E3\u5BC6\u5931\u8D25: ${t.message}`),u}}parseFromFile(u){try{const e=l__default.readFileSync(u,"utf8");return this.parseM3U8Content(e)}catch(e){return console.error(`\u274C \u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${e.message}`),[]}}parseFromString(u){return this.parseM3U8Content(u)}showStatistics(){console.log("\u{1F4CA} \u89E3\u6790\u7EDF\u8BA1\u4FE1\u606F"),console.log("================"),console.log(`\u603B\u7247\u6BB5\u6570\u91CF: ${this.links.length}`),console.log(`\u603B\u65F6\u957F: ${this.formatDuration(this.totalDuration)}`),console.log(`\u5E73\u5747\u7247\u6BB5\u65F6\u957F: ${(this.totalDuration/this.links.length).toFixed(2)} \u79D2`),console.log("")}formatDuration(u){const e=Math.floor(u/3600),t=Math.floor(u%3600/60),n=Math.floor(u%60);return e>0?`${e}:${t.toString().padStart(2,"0")}:${n.toString().padStart(2,"0")}`:`${t}:${n.toString().padStart(2,"0")}`}exportLinks(u){try{let e="";e=this.links.map(t=>t.url).join(`
`),l__default.writeFileSync(u,e,"utf8"),console.log(`\u2705 \u94FE\u63A5\u5DF2\u5BFC\u51FA\u5230: ${u}`)}catch(e){console.error(`\u274C \u5BFC\u51FA\u5931\u8D25: ${e.message}`)}}printAllLinks(u=!1){console.log("\u{1F517} \u63D0\u53D6\u5230\u7684\u89C6\u9891\u7247\u6BB5\u94FE\u63A5"),console.log("=========================="),this.links.forEach((e,t)=>{u?(console.log(`[${e.index}] \u65F6\u957F: ${e.duration}s`),console.log(`URL: ${e.url}`),console.log("---")):console.log(e.url)})}checkFFmpeg(){try{return U__default.sync("ffmpeg",["-version"],{encoding:"utf8"}).status===0?(console.log("\u2705 FFmpeg \u53EF\u7528"),!0):(console.error("\u274C FFmpeg \u4E0D\u53EF\u7528\uFF0C\u8BF7\u5B89\u88C5 FFmpeg"),!1)}catch{return console.error("\u274C FFmpeg \u4E0D\u53EF\u7528\uFF0C\u8BF7\u5B89\u88C5 FFmpeg"),!1}}createFileList(u,e){const t=b__default.join(u,"filelist.txt");let n="";const o=e.filter(r=>r.success).sort((r,i)=>r.index-i.index);return o.forEach(r=>{n+=`file '${r.fileName}'
`}),l__default.writeFileSync(t,n,"utf8"),console.log(`\u{1F4DD} \u6587\u4EF6\u5217\u8868\u5DF2\u521B\u5EFA: ${t} (${o.length} \u4E2A\u6587\u4EF6)`),t}async downloadWithUndici(u,e,t,n=3,o){const r=`segment_${e.toString().padStart(6,"0")}.ts`,i=b__default.join(t,r),s=Date.now();for(let a=1;a<=n;a++)try{const{statusCode:F,body:d}=await undici.request(u.url,{method:"GET",headers:{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",Accept:"*/*","Accept-Language":"en-US,en;q=0.9","Accept-Encoding":"gzip, deflate, br",Connection:"keep-alive","Cache-Control":"no-cache",Pragma:"no-cache"},headersTimeout:3e4,bodyTimeout:12e4});if(F===200){const c=[];for await(const E of d)c.push(E);const m=Buffer.concat(c);let y=m;o&&this.encryptionInfo?.method==="AES-128"&&(y=await this.decryptTSData(m,o)),l__default.writeFileSync(i,y);const g=l__default.statSync(i);if(g.size>0){const E=Date.now()-s;return{index:e,success:!0,fileName:r,bytesDownloaded:g.size,duration:E}}}throw new Error(`HTTP ${F}`)}catch(F){try{l__default.existsSync(i)&&l__default.unlinkSync(i)}catch{}if(a===n){const c=Date.now()-s;return{index:e,success:!1,fileName:r,error:`undici\u4E0B\u8F7D\u5931\u8D25: ${F.message}`,duration:c}}const d=Math.min(1e3*Math.pow(2,a-1),5e3);await new Promise(c=>setTimeout(c,d))}return{index:e,success:!1,fileName:r,error:"\u672A\u77E5\u9519\u8BEF",duration:Date.now()-s}}downloadWithCurl(u,e,t,n=3,o){return new Promise(r=>{const i=`segment_${e.toString().padStart(6,"0")}.ts`,s=b__default.join(t,i),a=Date.now(),F=async d=>{const c=["-L","-o",s,"--connect-timeout","10","--max-time","120","--retry","0","--speed-time","30","--speed-limit","512","--compressed","--tcp-nodelay","--keepalive-time","60","--location","--fail","--silent","--show-error","--user-agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36","--header","Accept: */*","--header","Accept-Language: en-US,en;q=0.9","--header","Accept-Encoding: gzip, deflate, br","--header","Connection: keep-alive","--header","Cache-Control: no-cache",u.url];let m=!1;const y=setTimeout(()=>{m=!0,g.kill("SIGTERM")},12e4),g=U__default("curl",c,{encoding:"utf8",stdio:["ignore","pipe","pipe"]});g.on("close",async E=>{clearTimeout(y);const A=Date.now()-a;if(!m&&E===0&&l__default.existsSync(s)){const B=l__default.statSync(s);if(B.size>0){if(o&&this.encryptionInfo?.method==="AES-128")try{const w=l__default.readFileSync(s),C=await this.decryptTSData(w,o);l__default.writeFileSync(s,C)}catch(w){console.error(`\u274C \u89E3\u5BC6\u5931\u8D25: ${w.message}`)}r({index:e,success:!0,fileName:i,bytesDownloaded:B.size,duration:A});return}}try{l__default.existsSync(s)&&l__default.unlinkSync(s)}catch{}if(d<n){const B=Math.min(1e3*Math.pow(2,d-1),5e3);setTimeout(()=>F(d+1),B)}else r({index:e,success:!1,fileName:i,error:`curl\u4E0B\u8F7D\u5931\u8D25 (code: ${E}, \u8017\u65F6: ${A}ms)`,duration:A})}),g.on("error",E=>{clearTimeout(y);const A=Date.now()-a;if(d<n){const B=Math.min(1e3*Math.pow(2,d-1),5e3);setTimeout(()=>F(d+1),B)}else r({index:e,success:!1,fileName:i,error:`curl\u8FDB\u7A0B\u9519\u8BEF: ${E.message}`,duration:A})})};F(1)})}async selectOptimalDownloadMethod(u){if(this.links.length===0)return"undici";const e=Math.min(3,this.links.length),t=this.links.slice(0,e);console.log("\u{1F9EA} \u6B63\u5728\u6D4B\u8BD5\u6700\u4F18\u4E0B\u8F7D\u65B9\u6CD5...");const n=Date.now();let o=0;try{const i=t.map((s,a)=>this.downloadWithUndici(s,a,u,1));o=(await Promise.allSettled(i)).filter(s=>s.status==="fulfilled"&&s.value.success).length}catch{}const r=Date.now()-n;try{for(let i=0;i<e;i++){const s=b__default.join(u,`segment_${i.toString().padStart(6,"0")}.ts`);l__default.existsSync(s)&&l__default.unlinkSync(s)}}catch{}return o/e>=.8&&r<15e3?(console.log("\u2705 \u9009\u62E9 undici \u65B9\u6CD5 (\u9AD8\u6027\u80FD)"),"undici"):(console.log("\u2705 \u9009\u62E9 curl \u65B9\u6CD5 (\u7A33\u5B9A\u6027\u4F18\u5148)"),"curl")}downloadSingleSegment(u,e,t,n=3,o){return this.downloadMethod==="undici"?this.downloadWithUndici(u,e,t,n,o):this.downloadWithCurl(u,e,t,n,o)}adjustConcurrency(u,e,t){return u>.95&&e>2?Math.min(t+3,30):u<.8||e<.5?Math.max(t-2,5):t}async downloadSegmentsConcurrent(u,e=20,t=3,n="auto"){l__default.ensureDirSync(u);let o;this.encryptionInfo?.method==="AES-128"&&(o=await this.downloadDecryptionKey(),o||console.warn("\u26A0\uFE0F \u65E0\u6CD5\u4E0B\u8F7D\u89E3\u5BC6\u5BC6\u94A5\uFF0C\u5C06\u5C1D\u8BD5\u4E0D\u89E3\u5BC6\u4E0B\u8F7D")),n==="auto"?this.downloadMethod=await this.selectOptimalDownloadMethod(u):this.downloadMethod=n,console.log(`\u{1F4E5} \u5F00\u59CB\u9AD8\u901F\u5E76\u884C\u4E0B\u8F7D (\u65B9\u6CD5: ${this.downloadMethod}, \u5E76\u53D1\u6570: ${e})...`);const r=I__default(e),i=[];let s=0,a=0,F=0,d=0,c=e;const m=Date.now(),y=setInterval(()=>{const w=(Date.now()-m)/1e3,C=(s/this.links.length*100).toFixed(1),p=s/w||0,f=p>0?Math.floor((this.links.length-s)/p):0,S=s>0?a/s:0,M=(d/1024/1024).toFixed(1);console.log(`\u{1F4CA} \u8FDB\u5EA6: ${s}/${this.links.length} (${C}%) | \u6210\u529F\u7387: ${(S*100).toFixed(1)}% | \u901F\u5EA6: ${p.toFixed(1)}/s | \u5DF2\u4E0B\u8F7D: ${M}MB | ETA: ${f}s`);const $=this.adjustConcurrency(S,p,c);$!==c&&(c=$,console.log(`\u{1F504} \u8C03\u6574\u5E76\u53D1\u6570\u4E3A: ${c}`),r.concurrency=c)},3e3);try{const w=this.links.map((C,p)=>r(()=>this.downloadSingleSegment(C,p,u,t,o)).then(f=>(s++,f.success?(a++,f.bytesDownloaded&&(d+=f.bytesDownloaded)):(F++,F%5===0&&c>5&&(c=Math.max(c-2,5),r.concurrency=c,console.log(`\u26A0\uFE0F \u68C0\u6D4B\u5230\u8FDE\u7EED\u5931\u8D25\uFF0C\u964D\u4F4E\u5E76\u53D1\u6570\u4E3A: ${c}`))),f)).catch(f=>({index:p,success:!1,fileName:`segment_${p.toString().padStart(6,"0")}.ts`,error:f.message,duration:0})));(await Promise.allSettled(w)).forEach((C,p)=>{C.status==="fulfilled"?i.push(C.value):i.push({index:p,success:!1,fileName:`segment_${p.toString().padStart(6,"0")}.ts`,error:C.reason?.message||"\u672A\u77E5\u9519\u8BEF"})})}finally{clearInterval(y)}const g=Math.floor((Date.now()-m)/1e3),E=(a/this.links.length*100).toFixed(1),A=(a/g).toFixed(1),B=(d/1024/1024).toFixed(1);return console.log(`
\u{1F4CA} \u4E0B\u8F7D\u5B8C\u6210! \u6210\u529F: ${a}, \u5931\u8D25: ${F}, \u603B\u8017\u65F6: ${g}s`),console.log(`\u{1F4C8} \u6210\u529F\u7387: ${E}%, \u5E73\u5747\u901F\u5EA6: ${A}/s, \u603B\u4E0B\u8F7D: ${B}MB`),parseFloat(E)<90&&console.log("\u{1F4A1} \u5EFA\u8BAE: \u5931\u8D25\u7387\u8F83\u9AD8\uFF0C\u53EF\u5C1D\u8BD5\u964D\u4F4E\u5E76\u53D1\u6570\u6216\u66F4\u6362\u4E0B\u8F7D\u65B9\u6CD5"),i}async mergeVideos(u){if(!this.checkFFmpeg())return!1;if(this.links.length===0)return console.error("\u274C \u6CA1\u6709\u53EF\u5408\u5E76\u7684\u89C6\u9891\u7247\u6BB5"),!1;try{console.log("\u{1F504} \u5F00\u59CB\u5408\u5E76\u89C6\u9891\u7247\u6BB5..."),l__default.ensureDirSync(u.tempDir),console.log("\u6B65\u9AA4 1: \u5E76\u884C\u4E0B\u8F7D\u89C6\u9891\u7247\u6BB5");const e=await this.downloadSegmentsConcurrent(u.tempDir,u.maxConcurrent||20,u.retryCount||3,u.downloadMethod||"auto"),t=e.filter(i=>i.success);if(t.length===0)return console.error("\u274C \u6CA1\u6709\u6210\u529F\u4E0B\u8F7D\u7684\u7247\u6BB5"),!1;t.length<e.length&&console.warn(`\u26A0\uFE0F \u8B66\u544A: ${e.length-t.length} \u4E2A\u7247\u6BB5\u4E0B\u8F7D\u5931\u8D25\uFF0C\u5C06\u8DF3\u8FC7\u8FD9\u4E9B\u7247\u6BB5`),console.log("\u6B65\u9AA4 2: \u521B\u5EFA\u6587\u4EF6\u5217\u8868");const n=this.createFileList(u.tempDir,e);console.log("\u6B65\u9AA4 3: \u5408\u5E76\u89C6\u9891\u6587\u4EF6");const o=["-f","concat","-safe","0","-i",n];u.videoCodec==="copy"&&u.audioCodec==="copy"?o.push("-c","copy"):(u.videoCodec&&o.push("-c:v",u.videoCodec),u.audioCodec&&o.push("-c:a",u.audioCodec),u.quality&&o.push("-crf",u.quality)),o.push("-y"),o.push(u.outputPath),console.log("\u{1F680} \u6267\u884C FFmpeg \u547D\u4EE4:",`ffmpeg ${o.join(" ")}`);const r=U__default.sync("ffmpeg",o,{encoding:"utf8",stdio:["pipe","pipe","pipe"],cwd:u.tempDir});if(r.status===0){if(console.log("\u2705 \u89C6\u9891\u5408\u5E76\u6210\u529F!"),console.log(`\u{1F4F9} \u8F93\u51FA\u6587\u4EF6: ${u.outputPath}`),l__default.existsSync(u.outputPath)){const i=l__default.statSync(u.outputPath);console.log(`\u{1F4CA} \u6587\u4EF6\u5927\u5C0F: ${(i.size/1024/1024).toFixed(2)} MB`)}return u.keepTempFiles||(console.log("\u{1F9F9} \u6E05\u7406\u4E34\u65F6\u6587\u4EF6..."),l__default.removeSync(u.tempDir)),!0}else return console.error("\u274C FFmpeg \u6267\u884C\u5931\u8D25"),console.error("\u9519\u8BEF\u8F93\u51FA:",r.stderr),!1}catch(e){return console.error(`\u274C \u5408\u5E76\u8FC7\u7A0B\u53D1\u751F\u9519\u8BEF: ${e.message}`),!1}}async processM3U8ToVideo(u,e="./temp_segments",t={}){const n={outputPath:u,tempDir:e,keepTempFiles:!1,videoCodec:"copy",audioCodec:"copy",maxConcurrent:20,retryCount:3,downloadMethod:"auto",...t};return this.mergeVideos(n)}async processFileToVideo(u,e,t="./temp_segments",n={}){let o;try{o=l__default.readFileSync(u,"utf8")}catch(i){return console.error(`\u274C \u8BFB\u53D6 m3u8 \u6587\u4EF6\u5931\u8D25: ${i.message}`),!1}if(this.parseFromString(o).length===0)return console.error("\u274C \u6CA1\u6709\u627E\u5230\u4EFB\u4F55\u94FE\u63A5"),!1;this.showStatistics(),l__default.ensureDirSync(b__default.dirname(e));const r=await this.processM3U8ToVideo(e,t,n);return console.log(r?`
\u{1F389} \u89C6\u9891\u5904\u7406\u5B8C\u6210\uFF01`:`
\u{1F61E} \u89C6\u9891\u5904\u7406\u5931\u8D25`),r}async processUrlToVideo(u,e,t="./temp_segments",n={}){console.log("\u{1F310} \u4ECE\u7F51\u7EDC\u4E0B\u8F7D M3U8 \u6587\u4EF6...");try{const{statusCode:o,body:r}=await undici.request(u,{method:"GET",headers:{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",Accept:"*/*","Accept-Language":"en-US,en;q=0.9,zh-CN;q=0.8","Accept-Encoding":"gzip, deflate, br",Connection:"keep-alive","Cache-Control":"no-cache",Pragma:"no-cache","Sec-Fetch-Dest":"empty","Sec-Fetch-Mode":"cors","Sec-Fetch-Site":"cross-site",Referer:new URL(u).origin},headersTimeout:3e4,bodyTimeout:6e4,maxRedirections:5});if(o!==200)return console.error(`\u274C \u4E0B\u8F7D M3U8 \u6587\u4EF6\u5931\u8D25\uFF0C\u72B6\u6001\u7801: ${o}`),console.log("\u{1F4A1} \u5EFA\u8BAE: \u68C0\u67E5URL\u662F\u5426\u6709\u6548\uFF0C\u6216\u5C1D\u8BD5\u4F7F\u7528\u5176\u4ED6\u5DE5\u5177\u9A8C\u8BC1URL\u53EF\u8BBF\u95EE\u6027"),!1;const i=[];for await(const F of r)i.push(F);const s=Buffer.concat(i).toString("utf8");if(console.log("\u2705 M3U8 \u6587\u4EF6\u4E0B\u8F7D\u6210\u529F"),!s.includes("#EXTM3U")&&!s.includes("#EXT-X-VERSION"))return console.error("\u274C \u4E0B\u8F7D\u7684\u5185\u5BB9\u4E0D\u662F\u6709\u6548\u7684 M3U8 \u6587\u4EF6"),console.log("\u{1F4C4} \u6587\u4EF6\u5185\u5BB9\u9884\u89C8:",s.substring(0,200)),!1;if(this.parseFromString(s).length===0)return console.error("\u274C \u6CA1\u6709\u627E\u5230\u4EFB\u4F55\u89C6\u9891\u7247\u6BB5\u94FE\u63A5"),console.log("\u{1F4C4} M3U8 \u5185\u5BB9:",s),!1;this.showStatistics(),l__default.ensureDirSync(b__default.dirname(e));const a=await this.processM3U8ToVideo(e,t,n);return console.log(a?`
\u{1F389} \u89C6\u9891\u5904\u7406\u5B8C\u6210\uFF01`:`
\u{1F61E} \u89C6\u9891\u5904\u7406\u5931\u8D25`),a}catch(o){return console.error(`\u274C \u4E0B\u8F7D M3U8 \u6587\u4EF6\u5931\u8D25: ${o.message}`),console.log(`\u{1F4A1} \u5EFA\u8BAE:
1. \u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5
2. \u9A8C\u8BC1 URL \u662F\u5426\u6B63\u786E\u548C\u6709\u6548
3. \u67D0\u4E9B M3U8 \u53EF\u80FD\u9700\u8981\u7279\u6B8A\u7684\u8BBF\u95EE\u6743\u9650\u6216 Token
4. \u5C1D\u8BD5\u5728\u6D4F\u89C8\u5668\u4E2D\u76F4\u63A5\u8BBF\u95EE\u8BE5 URL`),!1}}}module.exports=R;