From 196b8b3b041f704a19af1e10ddefe6d853cada01 Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Mon, 1 Dec 2025 12:33:18 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=BA=D0=BE=D0=BD=D0=B2=D0=B5=D1=80=D1=82=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20dash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/cli.js | 94 ++++-- docs/VIDEO_QUALITY_TESTING.md | 558 ++++++++++++++++++++++++++++++++++ src/cli.ts | 73 ++++- src/core/converter.ts | 39 ++- src/core/encoding.ts | 93 ++++-- src/core/manifest.ts | 256 ++++++++++++++++ src/core/packaging.ts | 270 ++-------------- src/types/index.ts | 25 ++ src/utils/index.ts | 3 +- src/utils/system.ts | 58 ++++ 10 files changed, 1182 insertions(+), 287 deletions(-) create mode 100644 docs/VIDEO_QUALITY_TESTING.md create mode 100644 src/core/manifest.ts diff --git a/bin/cli.js b/bin/cli.js index a00fa27..fc40313 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,25 +1,81 @@ #!/usr/bin/env node -import{createRequire as Pu}from"node:module";var Tu=Object.create;var{getPrototypeOf:bu,defineProperty:xD,getOwnPropertyNames:vu}=Object;var hu=Object.prototype.hasOwnProperty;var DD=(u,D,F)=>{F=u!=null?Tu(bu(u)):{};let C=D||!u||!u.__esModule?xD(F,"default",{value:u,enumerable:!0}):F;for(let E of vu(u))if(!hu.call(C,E))xD(C,E,{get:()=>u[E],enumerable:!0});return C};var k=(u,D)=>()=>(D||u((D={exports:{}}).exports,D),D.exports);var l=Pu(import.meta.url);var mD=k((GC,gD)=>{class fD{constructor(u,D,F){this.etaBufferLength=u||100,this.valueBuffer=[F],this.timeBuffer=[D],this.eta="0"}update(u,D,F){this.valueBuffer.push(D),this.timeBuffer.push(u),this.calculate(F-D)}getTime(){return this.eta}calculate(u){let D=this.valueBuffer.length,F=Math.min(this.etaBufferLength,D),C=this.valueBuffer[D-1]-this.valueBuffer[D-F],E=this.timeBuffer[D-1]-this.timeBuffer[D-F],A=C/E;this.valueBuffer=this.valueBuffer.slice(-this.etaBufferLength),this.timeBuffer=this.timeBuffer.slice(-this.etaBufferLength);let B=Math.ceil(u/A/1000);if(isNaN(B))this.eta="NULL";else if(!isFinite(B))this.eta="INF";else if(B>1e7)this.eta="INF";else if(B<0)this.eta=0;else this.eta=B}}gD.exports=fD});var BD=k(($C,lD)=>{var y=l("readline");class cD{constructor(u){this.stream=u,this.linewrap=!0,this.dy=0}cursorSave(){if(!this.stream.isTTY)return;this.stream.write("\x1B7")}cursorRestore(){if(!this.stream.isTTY)return;this.stream.write("\x1B8")}cursor(u){if(!this.stream.isTTY)return;if(u)this.stream.write("\x1B[?25h");else this.stream.write("\x1B[?25l")}cursorTo(u=null,D=null){if(!this.stream.isTTY)return;y.cursorTo(this.stream,u,D)}cursorRelative(u=null,D=null){if(!this.stream.isTTY)return;this.dy=this.dy+D,y.moveCursor(this.stream,u,D)}cursorRelativeReset(){if(!this.stream.isTTY)return;y.moveCursor(this.stream,0,-this.dy),y.cursorTo(this.stream,0,null),this.dy=0}clearRight(){if(!this.stream.isTTY)return;y.clearLine(this.stream,1)}clearLine(){if(!this.stream.isTTY)return;y.clearLine(this.stream,0)}clearBottom(){if(!this.stream.isTTY)return;y.clearScreenDown(this.stream)}newline(){this.stream.write(` -`),this.dy++}write(u,D=!1){if(this.linewrap===!0&&D===!1)this.stream.write(u.substr(0,this.getWidth()));else this.stream.write(u)}lineWrapping(u){if(!this.stream.isTTY)return;if(this.linewrap=u,u)this.stream.write("\x1B[?7h");else this.stream.write("\x1B[?7l")}isTTY(){return this.stream.isTTY===!0}getWidth(){return this.stream.columns||(this.stream.isTTY?80:200)}}lD.exports=cD});var nD=k((RC,dD)=>{dD.exports=({onlyFirst:u=!1}={})=>{let D=["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)","(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"].join("|");return new RegExp(D,u?void 0:"g")}});var aD=k((UC,pD)=>{var uF=nD();pD.exports=(u)=>typeof u==="string"?u.replace(uF(),""):u});var rD=k((kC,AD)=>{var sD=(u)=>{if(Number.isNaN(u))return!1;if(u>=4352&&(u<=4447||u===9001||u===9002||11904<=u&&u<=12871&&u!==12351||12880<=u&&u<=19903||19968<=u&&u<=42182||43360<=u&&u<=43388||44032<=u&&u<=55203||63744<=u&&u<=64255||65040<=u&&u<=65049||65072<=u&&u<=65131||65281<=u&&u<=65376||65504<=u&&u<=65510||110592<=u&&u<=110593||127488<=u&&u<=127569||131072<=u&&u<=262141))return!0;return!1};AD.exports=sD;AD.exports.default=sD});var oD=k((_C,iD)=>{iD.exports=function(){return/\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F|\uD83D\uDC68(?:\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68\uD83C\uDFFB|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFE])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D[\uDC66\uDC67])|[\u2695\u2696\u2708]\uFE0F|\uD83D[\uDC66\uDC67]|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|(?:\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708])\uFE0F|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C[\uDFFB-\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFB\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)\uD83C\uDFFB|\uD83E\uDDD1(?:\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])|\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1)|(?:\uD83E\uDDD1\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFE])|(?:\uD83E\uDDD1\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)(?:\uD83C[\uDFFB\uDFFC])|\uD83D\uDC69(?:\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFC-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|(?:\uD83E\uDDD1\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)(?:\uD83C[\uDFFB-\uDFFD])|\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83D\uDC69(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|(?:(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)\uFE0F|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF])\u200D[\u2640\u2642]|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD6-\uDDDD])(?:(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|\u200D[\u2640\u2642])|\uD83C\uDFF4\u200D\u2620)\uFE0F|\uD83D\uDC69\u200D\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|\uD83D\uDC15\u200D\uD83E\uDDBA|\uD83D\uDC69\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC67|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF4\uD83C\uDDF2|\uD83C\uDDF6\uD83C\uDDE6|[#\*0-9]\uFE0F\u20E3|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83D\uDC69(?:\uD83C[\uDFFB-\uDFFF])|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270A-\u270D]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC70\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDCAA\uDD74\uDD7A\uDD90\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD36\uDDB5\uDDB6\uDDBB\uDDD2-\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5\uDEEB\uDEEC\uDEF4-\uDEFA\uDFE0-\uDFEB]|\uD83E[\uDD0D-\uDD3A\uDD3C-\uDD45\uDD47-\uDD71\uDD73-\uDD76\uDD7A-\uDDA2\uDDA5-\uDDAA\uDDAE-\uDDCA\uDDCD-\uDDFF\uDE70-\uDE73\uDE78-\uDE7A\uDE80-\uDE82\uDE90-\uDE95])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDED5\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEFA\uDFE0-\uDFEB]|\uD83E[\uDD0D-\uDD3A\uDD3C-\uDD45\uDD47-\uDD71\uDD73-\uDD76\uDD7A-\uDDA2\uDDA5-\uDDAA\uDDAE-\uDDCA\uDDCD-\uDDFF\uDE70-\uDE73\uDE78-\uDE7A\uDE80-\uDE82\uDE90-\uDE95])\uFE0F|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDC8F\uDC91\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD0F\uDD18-\uDD1F\uDD26\uDD30-\uDD39\uDD3C-\uDD3E\uDDB5\uDDB6\uDDB8\uDDB9\uDDBB\uDDCD-\uDDCF\uDDD1-\uDDDD])/g}});var eD=k((IC,qD)=>{var FF=aD(),CF=rD(),EF=oD(),tD=(u)=>{if(typeof u!=="string"||u.length===0)return 0;if(u=FF(u),u.length===0)return 0;u=u.replace(EF()," ");let D=0;for(let F=0;F=127&&C<=159)continue;if(C>=768&&C<=879)continue;if(C>65535)F++;D+=CF(C)?2:1}return D};qD.exports=tD;qD.exports.default=tD});var ZD=k((HC,Du)=>{Du.exports=function(D,F,C){if(F.autopadding!==!0)return D;function E(A,B){return(F.autopaddingChar+A).slice(-B)}switch(C){case"percentage":return E(D,3);default:return D}}});var QD=k((WC,uu)=>{uu.exports=function(D,F){let C=Math.round(D*F.barsize),E=F.barsize-C;return F.barCompleteString.substr(0,C)+F.barGlue+F.barIncompleteString.substr(0,E)}});var XD=k((VC,Fu)=>{Fu.exports=function(D,F,C){function E(B){if(C)return C*Math.round(B/C);else return B}function A(B){return(F.autopaddingChar+B).slice(-2)}if(D>3600)return A(Math.floor(D/3600))+"h"+A(E(D%3600/60))+"m";else if(D>60)return A(Math.floor(D/60))+"m"+A(E(D%60))+"s";else if(D>10)return A(E(D))+"s";else return A(D)+"s"}});var YD=k((zC,Cu)=>{var BF=eD(),AF=ZD(),qF=QD(),ZF=XD();Cu.exports=function(D,F,C){let E=D.format,A=D.formatTime||ZF,B=D.formatValue||AF,q=D.formatBar||qF,Y=Math.floor(F.progress*100)+"",K=F.stopTime||Date.now(),Q=Math.round((K-F.startTime)/1000),L=Object.assign({},C,{bar:q(F.progress,D),percentage:B(Y,D,"percentage"),total:B(F.total,D,"total"),value:B(F.value,D,"value"),eta:B(F.eta,D,"eta"),eta_formatted:A(F.eta,D,5),duration:B(Q,D,"duration"),duration_formatted:A(Q,D,1)});E=E.replace(/\{(\w+)\}/g,function(X,J){if(typeof L[J]<"u")return L[J];return X});let G=Math.max(0,F.maxWidth-BF(E)-2),Z=Math.floor(G/2);switch(D.align){case"right":E=G>0?" ".repeat(G)+E:E;break;case"center":E=Z>0?" ".repeat(Z)+E:E;break;case"left":default:break}return E}});var s=k((xC,Eu)=>{function $(u,D){if(typeof u>"u"||u===null)return D;else return u}Eu.exports={parse:function(D,F){let C={},E=Object.assign({},F,D);return C.throttleTime=1000/$(E.fps,10),C.stream=$(E.stream,process.stderr),C.terminal=$(E.terminal,null),C.clearOnComplete=$(E.clearOnComplete,!1),C.stopOnComplete=$(E.stopOnComplete,!1),C.barsize=$(E.barsize,40),C.align=$(E.align,"left"),C.hideCursor=$(E.hideCursor,!1),C.linewrap=$(E.linewrap,!1),C.barGlue=$(E.barGlue,""),C.barCompleteChar=$(E.barCompleteChar,"="),C.barIncompleteChar=$(E.barIncompleteChar,"-"),C.format=$(E.format,"progress [{bar}] {percentage}% | ETA: {eta}s | {value}/{total}"),C.formatTime=$(E.formatTime,null),C.formatValue=$(E.formatValue,null),C.formatBar=$(E.formatBar,null),C.etaBufferLength=$(E.etaBuffer,10),C.etaAsynchronousUpdate=$(E.etaAsynchronousUpdate,!1),C.progressCalculationRelative=$(E.progressCalculationRelative,!1),C.synchronousUpdate=$(E.synchronousUpdate,!0),C.noTTYOutput=$(E.noTTYOutput,!1),C.notTTYSchedule=$(E.notTTYSchedule,2000),C.emptyOnZero=$(E.emptyOnZero,!1),C.forceRedraw=$(E.forceRedraw,!1),C.autopadding=$(E.autopadding,!1),C.gracefulExit=$(E.gracefulExit,!1),C},assignDerivedOptions:function(D){return D.barCompleteString=D.barCompleteChar.repeat(D.barsize+1),D.barIncompleteString=D.barIncompleteChar.repeat(D.barsize+1),D.autopaddingChar=D.autopadding?$(D.autopaddingChar," "):"",D}}});var JD=k((MC,Au)=>{var Bu=mD(),QF=BD(),XF=YD(),YF=s(),JF=l("events");Au.exports=class extends JF{constructor(D){super();this.options=YF.assignDerivedOptions(D),this.terminal=this.options.terminal?this.options.terminal:new QF(this.options.stream),this.value=0,this.startValue=0,this.total=100,this.lastDrawnString=null,this.startTime=null,this.stopTime=null,this.lastRedraw=Date.now(),this.eta=new Bu(this.options.etaBufferLength,0,0),this.payload={},this.isActive=!1,this.formatter=typeof this.options.format==="function"?this.options.format:XF}render(D=!1){let F={progress:this.getProgress(),eta:this.eta.getTime(),startTime:this.startTime,stopTime:this.stopTime,total:this.total,value:this.value,maxWidth:this.terminal.getWidth()};if(this.options.etaAsynchronousUpdate)this.updateETA();let C=this.formatter(this.options,F,this.payload);if(D||this.options.forceRedraw||this.options.noTTYOutput&&!this.terminal.isTTY()||this.lastDrawnString!=C)this.emit("redraw-pre"),this.terminal.cursorTo(0,null),this.terminal.write(C),this.terminal.clearRight(),this.lastDrawnString=C,this.lastRedraw=Date.now(),this.emit("redraw-post")}start(D,F,C){this.value=F||0,this.total=typeof D<"u"&&D>=0?D:100,this.startValue=F||0,this.payload=C||{},this.startTime=Date.now(),this.stopTime=null,this.lastDrawnString="",this.eta=new Bu(this.options.etaBufferLength,this.startTime,this.value),this.isActive=!0,this.emit("start",D,F)}stop(){this.isActive=!1,this.stopTime=Date.now(),this.emit("stop",this.total,this.value)}update(D,F={}){if(typeof D==="number")this.value=D,this.eta.update(Date.now(),D,this.total);let C=(typeof D==="object"?D:F)||{};this.emit("update",this.total,this.value);for(let E in C)this.payload[E]=C[E];if(this.value>=this.getTotal()&&this.options.stopOnComplete)this.stop()}getProgress(){let D=this.value/this.total;if(this.options.progressCalculationRelative)D=(this.value-this.startValue)/(this.total-this.startValue);if(isNaN(D))D=this.options&&this.options.emptyOnZero?0:1;return D=Math.min(Math.max(D,0),1),D}increment(D=1,F={}){if(typeof D==="object")this.update(this.value+1,D);else this.update(this.value+D,F)}getTotal(){return this.total}setTotal(D){if(typeof D<"u"&&D>=0)this.total=D}updateETA(){this.eta.update(Date.now(),this.value,this.total)}}});var Zu=k((NC,qu)=>{var KF=JD(),LF=s();qu.exports=class extends KF{constructor(D,F){super(LF.parse(D,F));if(this.timer=null,this.options.noTTYOutput&&this.terminal.isTTY()===!1)this.options.synchronousUpdate=!1;this.schedulingRate=this.terminal.isTTY()?this.options.throttleTime:this.options.notTTYSchedule,this.sigintCallback=null}render(){if(this.timer)clearTimeout(this.timer),this.timer=null;if(super.render(),this.options.noTTYOutput&&this.terminal.isTTY()===!1)this.terminal.newline();this.timer=setTimeout(this.render.bind(this),this.schedulingRate)}update(D,F){if(!this.timer)return;if(super.update(D,F),this.options.synchronousUpdate&&this.lastRedraw+this.options.throttleTime*2{var GF=BD(),$F=JD(),RF=s(),UF=l("events");Qu.exports=class extends UF{constructor(D,F){super();this.bars=[],this.options=RF.parse(D,F),this.options.synchronousUpdate=!1,this.terminal=this.options.terminal?this.options.terminal:new GF(this.options.stream),this.timer=null,this.isActive=!1,this.schedulingRate=this.terminal.isTTY()?this.options.throttleTime:this.options.notTTYSchedule,this.loggingBuffer=[],this.sigintCallback=null}create(D,F,C,E={}){let A=new $F(Object.assign({},this.options,{terminal:this.terminal},E));if(this.bars.push(A),this.options.noTTYOutput===!1&&this.terminal.isTTY()===!1)return A;if(this.sigintCallback===null&&this.options.gracefulExit)this.sigintCallback=this.stop.bind(this),process.once("SIGINT",this.sigintCallback),process.once("SIGTERM",this.sigintCallback);if(!this.isActive){if(this.options.hideCursor===!0)this.terminal.cursor(!1);if(this.options.linewrap===!1)this.terminal.lineWrapping(!1);this.timer=setTimeout(this.update.bind(this),this.schedulingRate)}return this.isActive=!0,A.start(D,F,C),this.emit("start"),A}remove(D){let F=this.bars.indexOf(D);if(F<0)return!1;return this.bars.splice(F,1),this.update(),this.terminal.newline(),this.terminal.clearBottom(),!0}update(){if(this.timer)clearTimeout(this.timer),this.timer=null;if(this.emit("update-pre"),this.terminal.cursorRelativeReset(),this.emit("redraw-pre"),this.loggingBuffer.length>0){this.terminal.clearLine();while(this.loggingBuffer.length>0)this.terminal.write(this.loggingBuffer.shift(),!0)}for(let D=0;D0)this.terminal.newline();this.bars[D].render()}if(this.emit("redraw-post"),this.options.noTTYOutput&&this.terminal.isTTY()===!1)this.terminal.newline(),this.terminal.newline();if(this.timer=setTimeout(this.update.bind(this),this.schedulingRate),this.emit("update-post"),this.options.stopOnComplete&&!this.bars.find((D)=>D.isActive))this.stop()}stop(){if(clearTimeout(this.timer),this.timer=null,this.sigintCallback)process.removeListener("SIGINT",this.sigintCallback),process.removeListener("SIGTERM",this.sigintCallback),this.sigintCallback=null;if(this.isActive=!1,this.options.hideCursor===!0)this.terminal.cursor(!0);if(this.options.linewrap===!1)this.terminal.lineWrapping(!0);if(this.terminal.cursorRelativeReset(),this.emit("stop-pre-clear"),this.options.clearOnComplete)this.terminal.clearBottom();else{for(let D=0;D0)this.terminal.newline();this.bars[D].render(),this.bars[D].stop()}this.terminal.newline()}this.emit("stop")}log(D){this.loggingBuffer.push(D)}}});var Ju=k((wC,Yu)=>{Yu.exports={format:"progress [{bar}] {percentage}% | ETA: {eta}s | {value}/{total}",barCompleteChar:"=",barIncompleteChar:"-"}});var Lu=k((OC,Ku)=>{Ku.exports={format:" {bar} {percentage}% | ETA: {eta}s | {value}/{total}",barCompleteChar:"█",barIncompleteChar:"░"}});var $u=k((yC,Gu)=>{Gu.exports={format:" \x1B[90m{bar}\x1B[0m {percentage}% | ETA: {eta}s | {value}/{total}",barCompleteChar:"█",barIncompleteChar:"░"}});var Uu=k((SC,Ru)=>{Ru.exports={format:" {bar}■ {percentage}% | ETA: {eta}s | {value}/{total}",barCompleteChar:"■",barIncompleteChar:" "}});var _u=k((TC,ku)=>{var kF=Ju(),_F=Lu(),IF=$u(),HF=Uu();ku.exports={legacy:kF,shades_classic:_F,shades_grey:IF,rect:HF}});var Wu=k((bC,Hu)=>{var Iu=Zu(),WF=Xu(),VF=_u(),zF=YD(),xF=ZD(),MF=QD(),NF=XD();Hu.exports={Bar:Iu,SingleBar:Iu,MultiBar:WF,Presets:VF,Format:{Formatter:zF,BarFormat:MF,ValueFormat:xF,TimeFormat:NF}}});import{join as hD,basename as ou,extname as tu}from"node:path";import{randomUUID as eu}from"node:crypto";import{rm as PD}from"node:fs/promises";import{spawn as S}from"node:child_process";async function T(){return new Promise((u)=>{let D=S("ffmpeg",["-version"]);D.on("error",()=>u(!1)),D.on("close",(F)=>u(F===0))})}async function b(){return new Promise((u)=>{let D=S("MP4Box",["-version"]);D.on("error",()=>u(!1)),D.on("close",(F)=>u(F===0))})}async function v(){return new Promise((u)=>{let D=S("ffmpeg",["-hide_banner","-encoders"]),F="";D.stdout.on("data",(C)=>{F+=C.toString()}),D.on("error",()=>u(!1)),D.on("close",(C)=>{if(C!==0)u(!1);else u(F.includes("h264_nvenc")||F.includes("hevc_nvenc"))})})}async function h(){return new Promise((u)=>{let D=S("ffmpeg",["-hide_banner","-encoders"]),F="";D.stdout.on("data",(C)=>{F+=C.toString()}),D.on("error",()=>u({available:!1})),D.on("close",(C)=>{if(C!==0)u({available:!1});else if(F.includes("av1_nvenc"))u({available:!0,encoder:"av1_nvenc"});else if(F.includes("av1_qsv"))u({available:!0,encoder:"av1_qsv"});else if(F.includes("av1_amf"))u({available:!0,encoder:"av1_amf"});else u({available:!1})})})}async function w(u,D,F){return new Promise((C,E)=>{let A=S("ffmpeg",u),B="";A.stderr.on("data",(q)=>{let Y=q.toString();if(B+=Y,D&&F){let K=Y.match(/time=(\d{2}):(\d{2}):(\d{2}\.\d{2})/);if(K){let Q=parseInt(K[1]),L=parseInt(K[2]),G=parseFloat(K[3]),Z=Q*3600+L*60+G,X=Math.min(100,Z/F*100);D(X)}}}),A.on("error",(q)=>{E(Error(`FFmpeg error: ${q.message}`))}),A.on("close",(q)=>{if(q===0)C();else E(Error(`FFmpeg failed with exit code ${q} -${B}`))})})}async function uD(u){return new Promise((D,F)=>{let C=S("MP4Box",u),E="",A="";C.stdout.on("data",(B)=>{E+=B.toString()}),C.stderr.on("data",(B)=>{A+=B.toString()}),C.on("error",(B)=>{F(Error(`MP4Box error: ${B.message}`))}),C.on("close",(B)=>{if(B===0)D();else F(Error(`MP4Box failed with exit code ${B} -${A||E}`))})})}import{spawn as fu}from"node:child_process";async function P(u){return new Promise((D,F)=>{let C=fu("ffprobe",["-v","error","-show_entries","stream=width,height,duration,r_frame_rate,codec_name,codec_type,bit_rate","-show_entries","format=duration","-of","json",u]),E="";C.stdout.on("data",(A)=>{E+=A.toString()}),C.on("error",(A)=>{F(Error(`ffprobe error: ${A.message}`))}),C.on("close",(A)=>{if(A!==0){F(Error(`ffprobe failed with exit code ${A}`));return}try{let B=JSON.parse(E),q=B.streams.find((X)=>X.codec_type==="video"),Y=B.streams.find((X)=>X.codec_type==="audio"&&X.bit_rate),K=B.format;if(!q){F(Error("No video stream found in input file"));return}let Q=30;if(q.r_frame_rate){let[X,J]=q.r_frame_rate.split("/").map(Number);if(X&&J&&J!==0)Q=X/J}let L=parseFloat(q.duration||K.duration||"0"),G=Y?.bit_rate?Math.round(parseInt(Y.bit_rate)/1000):void 0,Z=q.bit_rate?Math.round(parseInt(q.bit_rate)/1000):void 0;D({width:q.width,height:q.height,duration:L,fps:Q,codec:q.codec_name,audioBitrate:G,videoBitrate:Z})}catch(B){F(Error(`Failed to parse ffprobe output: ${B}`))}})})}function n(u,D=256){if(!u)return`${D}k`;let F=Math.min(u,D);if(F<=64)return"64k";if(F<=96)return"96k";if(F<=128)return"128k";if(F<=192)return"192k";return"256k"}function p(u){let D=Math.floor(u/3600),F=Math.floor(u%3600/60),C=u%60;return`${String(D).padStart(2,"0")}:${String(F).padStart(2,"0")}:${C.toFixed(3).padStart(6,"0")}`}import{mkdir as gu,access as mu,constants as cu}from"node:fs/promises";async function f(u){try{await mu(u,cu.F_OK)}catch{await gu(u,{recursive:!0})}}function lu(u,D){let F=u*D;if(F<=230400)return 0.08;if(F<=409920)return 0.075;if(F<=921600)return 0.07;if(F<=2073600)return 0.065;if(F<=3686400)return 0.06;return 0.055}function M(u,D,F=30,C){let E=lu(u,D),A=Math.round(u*D*F*E/1000);if(C&&A>C)A=C;return`${A}k`}var FD=[{name:"360p",width:640,height:360,videoBitrate:M(640,360,30),audioBitrate:"192k"},{name:"480p",width:854,height:480,videoBitrate:M(854,480,30),audioBitrate:"192k"},{name:"720p",width:1280,height:720,videoBitrate:M(1280,720,30),audioBitrate:"192k"},{name:"1080p",width:1920,height:1080,videoBitrate:M(1920,1080,30),audioBitrate:"256k"},{name:"1440p",width:2560,height:1440,videoBitrate:M(2560,1440,30),audioBitrate:"256k"},{name:"2160p",width:3840,height:2160,videoBitrate:M(3840,2160,30),audioBitrate:"256k"}];function CD(u,D,F=30,C){let E=[],A=FD.filter((B)=>{return B.width<=u&&B.height<=D});for(let B of A)E.push({...B,videoBitrate:M(B.width,B.height,30,C)});if(F>=45)for(let B of A)E.push(a(B,60,C));if(F>=75)for(let B of A)E.push(a(B,90,C));if(F>=95)for(let B of A)E.push(a(B,120,C));return E}function a(u,D,F){return{...u,name:`${u.name}-${D}`,videoBitrate:M(u.width,u.height,D,F)}}function MD(u){let F=u.trim().match(/^(\d+)p?(?:[@-](\d+))?$/i);if(!F)return null;let C=F[1]+"p",E=F[2]?parseInt(F[2]):30;return{resolution:C,fps:E}}function ND(u,D=30,F){let C=FD.find((E)=>E.name===u);if(!C)return null;if(D===30)return{...C,videoBitrate:M(C.width,C.height,30,F)};return a(C,D,F)}function du(u,D,F,C){let E=MD(u);if(!E)return`Invalid profile format: ${u}. Use format like: 360, 720@60, 1080-60`;let A=ND(E.resolution,E.fps);if(!A)return`Unknown resolution: ${E.resolution}. Available: 360, 480, 720, 1080, 1440, 2160`;if(A.width>D||A.height>F)return`Source resolution (${D}x${F}) is lower than ${u} (${A.width}x${A.height})`;if(E.fps>C)return`Source FPS (${C}) is lower than requested ${E.fps} FPS in ${u}`;return null}function jD(u,D,F,C,E){let A=[],B=[];for(let q of u){let Y=du(q,D,F,C);if(Y){B.push(Y);continue}let K=MD(q);if(!K)continue;let Q=ND(K.resolution,K.fps,E);if(Q)A.push(Q)}return{profiles:A,errors:B}}import{join as j}from"node:path";import{readdir as nu,unlink as wD,rmdir as pu,writeFile as OD}from"node:fs/promises";async function yD(u,D,F="00:00:01"){let C=j(D,"poster.jpg"),E=/^\d+(\.\d+)?$/.test(F)?F:F;return await w(["-ss",E,"-i",u,"-vframes","1","-q:v","2","-y",C]),C}async function SD(u,D,F,C){let{width:E,height:A,interval:B,columns:q}=C,Y=j(D,".thumbnails_temp");await f(Y),await OD(j(Y,".keep"),"");let K=j(Y,"thumb_%04d.jpg");await w(["-i",u,"-vf",`fps=1/${B},scale=${E}:${A}`,"-q:v","5",K]);let L=(await nu(Y)).filter((U)=>U.startsWith("thumb_")&&U.endsWith(".jpg")).sort();if(L.length===0)throw Error("No thumbnails generated");let G=L.length,Z=Math.ceil(G/q),X=j(D,"thumbnails.jpg"),J=`tile=${q}x${Z}`;await w(["-i",K,"-filter_complex",J,"-q:v","5",X]);let W=j(D,"thumbnails.vtt"),I=au(G,B,E,A,q,"thumbnails.jpg");await OD(W,I);for(let U of L)await wD(j(Y,U));return await wD(j(Y,".keep")),await pu(Y),{spritePath:X,vttPath:W}}function au(u,D,F,C,E,A){let B=`WEBVTT +import{createRequire as FF}from"node:module";var tu=Object.create;var{getPrototypeOf:eu,defineProperty:gD,getOwnPropertyNames:DF}=Object;var uF=Object.prototype.hasOwnProperty;var e=(D,u,F)=>{F=D!=null?tu(eu(D)):{};let E=u||!D||!D.__esModule?gD(F,"default",{value:D,enumerable:!0}):F;for(let C of DF(D))if(!uF.call(E,C))gD(E,C,{get:()=>D[C],enumerable:!0});return E};var V=(D,u)=>()=>(u||D((u={exports:{}}).exports,u),u.exports);var f=FF(import.meta.url);var Eu=V((f3,Fu)=>{class uu{constructor(D,u,F){this.etaBufferLength=D||100,this.valueBuffer=[F],this.timeBuffer=[u],this.eta="0"}update(D,u,F){this.valueBuffer.push(u),this.timeBuffer.push(D),this.calculate(F-u)}getTime(){return this.eta}calculate(D){let u=this.valueBuffer.length,F=Math.min(this.etaBufferLength,u),E=this.valueBuffer[u-1]-this.valueBuffer[u-F],C=this.timeBuffer[u-1]-this.timeBuffer[u-F],A=E/C;this.valueBuffer=this.valueBuffer.slice(-this.etaBufferLength),this.timeBuffer=this.timeBuffer.slice(-this.etaBufferLength);let B=Math.ceil(D/A/1000);if(isNaN(B))this.eta="NULL";else if(!isFinite(B))this.eta="INF";else if(B>1e7)this.eta="INF";else if(B<0)this.eta=0;else this.eta=B}}Fu.exports=uu});var QD=V((g3,Bu)=>{var c=f("readline");class Cu{constructor(D){this.stream=D,this.linewrap=!0,this.dy=0}cursorSave(){if(!this.stream.isTTY)return;this.stream.write("\x1B7")}cursorRestore(){if(!this.stream.isTTY)return;this.stream.write("\x1B8")}cursor(D){if(!this.stream.isTTY)return;if(D)this.stream.write("\x1B[?25h");else this.stream.write("\x1B[?25l")}cursorTo(D=null,u=null){if(!this.stream.isTTY)return;c.cursorTo(this.stream,D,u)}cursorRelative(D=null,u=null){if(!this.stream.isTTY)return;this.dy=this.dy+u,c.moveCursor(this.stream,D,u)}cursorRelativeReset(){if(!this.stream.isTTY)return;c.moveCursor(this.stream,0,-this.dy),c.cursorTo(this.stream,0,null),this.dy=0}clearRight(){if(!this.stream.isTTY)return;c.clearLine(this.stream,1)}clearLine(){if(!this.stream.isTTY)return;c.clearLine(this.stream,0)}clearBottom(){if(!this.stream.isTTY)return;c.clearScreenDown(this.stream)}newline(){this.stream.write(` +`),this.dy++}write(D,u=!1){if(this.linewrap===!0&&u===!1)this.stream.write(D.substr(0,this.getWidth()));else this.stream.write(D)}lineWrapping(D){if(!this.stream.isTTY)return;if(this.linewrap=D,D)this.stream.write("\x1B[?7h");else this.stream.write("\x1B[?7l")}isTTY(){return this.stream.isTTY===!0}getWidth(){return this.stream.columns||(this.stream.isTTY?80:200)}}Bu.exports=Cu});var Zu=V((m3,Au)=>{Au.exports=({onlyFirst:D=!1}={})=>{let u=["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)","(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"].join("|");return new RegExp(u,D?void 0:"g")}});var $u=V((c3,Yu)=>{var VF=Zu();Yu.exports=(D)=>typeof D==="string"?D.replace(VF(),""):D});var Ku=V((d3,qD)=>{var Ju=(D)=>{if(Number.isNaN(D))return!1;if(D>=4352&&(D<=4447||D===9001||D===9002||11904<=D&&D<=12871&&D!==12351||12880<=D&&D<=19903||19968<=D&&D<=42182||43360<=D&&D<=43388||44032<=D&&D<=55203||63744<=D&&D<=64255||65040<=D&&D<=65049||65072<=D&&D<=65131||65281<=D&&D<=65376||65504<=D&&D<=65510||110592<=D&&D<=110593||127488<=D&&D<=127569||131072<=D&&D<=262141))return!0;return!1};qD.exports=Ju;qD.exports.default=Ju});var Gu=V((l3,Xu)=>{Xu.exports=function(){return/\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F|\uD83D\uDC68(?:\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68\uD83C\uDFFB|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFE])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D[\uDC66\uDC67])|[\u2695\u2696\u2708]\uFE0F|\uD83D[\uDC66\uDC67]|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|(?:\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708])\uFE0F|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C[\uDFFB-\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFB\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)\uD83C\uDFFB|\uD83E\uDDD1(?:\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])|\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1)|(?:\uD83E\uDDD1\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFE])|(?:\uD83E\uDDD1\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)(?:\uD83C[\uDFFB\uDFFC])|\uD83D\uDC69(?:\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFC-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|(?:\uD83E\uDDD1\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)(?:\uD83C[\uDFFB-\uDFFD])|\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83D\uDC69(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|(?:(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)\uFE0F|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF])\u200D[\u2640\u2642]|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD6-\uDDDD])(?:(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|\u200D[\u2640\u2642])|\uD83C\uDFF4\u200D\u2620)\uFE0F|\uD83D\uDC69\u200D\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|\uD83D\uDC15\u200D\uD83E\uDDBA|\uD83D\uDC69\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC67|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF4\uD83C\uDDF2|\uD83C\uDDF6\uD83C\uDDE6|[#\*0-9]\uFE0F\u20E3|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83D\uDC69(?:\uD83C[\uDFFB-\uDFFF])|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270A-\u270D]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC70\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDCAA\uDD74\uDD7A\uDD90\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD36\uDDB5\uDDB6\uDDBB\uDDD2-\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5\uDEEB\uDEEC\uDEF4-\uDEFA\uDFE0-\uDFEB]|\uD83E[\uDD0D-\uDD3A\uDD3C-\uDD45\uDD47-\uDD71\uDD73-\uDD76\uDD7A-\uDDA2\uDDA5-\uDDAA\uDDAE-\uDDCA\uDDCD-\uDDFF\uDE70-\uDE73\uDE78-\uDE7A\uDE80-\uDE82\uDE90-\uDE95])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDED5\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEFA\uDFE0-\uDFEB]|\uD83E[\uDD0D-\uDD3A\uDD3C-\uDD45\uDD47-\uDD71\uDD73-\uDD76\uDD7A-\uDDA2\uDDA5-\uDDAA\uDDAE-\uDDCA\uDDCD-\uDDFF\uDE70-\uDE73\uDE78-\uDE7A\uDE80-\uDE82\uDE90-\uDE95])\uFE0F|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDC8F\uDC91\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD0F\uDD18-\uDD1F\uDD26\uDD30-\uDD39\uDD3C-\uDD3E\uDDB5\uDDB6\uDDB8\uDDB9\uDDBB\uDDCD-\uDDCF\uDDD1-\uDDDD])/g}});var Uu=V((p3,zD)=>{var NF=$u(),RF=Ku(),xF=Gu(),_u=(D)=>{if(typeof D!=="string"||D.length===0)return 0;if(D=NF(D),D.length===0)return 0;D=D.replace(xF()," ");let u=0;for(let F=0;F=127&&E<=159)continue;if(E>=768&&E<=879)continue;if(E>65535)F++;u+=RF(E)?2:1}return u};zD.exports=_u;zD.exports.default=_u});var LD=V((n3,Wu)=>{Wu.exports=function(u,F,E){if(F.autopadding!==!0)return u;function C(A,B){return(F.autopaddingChar+A).slice(-B)}switch(E){case"percentage":return C(u,3);default:return u}}});var VD=V((s3,ku)=>{ku.exports=function(u,F){let E=Math.round(u*F.barsize),C=F.barsize-E;return F.barCompleteString.substr(0,E)+F.barGlue+F.barIncompleteString.substr(0,C)}});var ND=V((a3,Hu)=>{Hu.exports=function(u,F,E){function C(B){if(E)return E*Math.round(B/E);else return B}function A(B){return(F.autopaddingChar+B).slice(-2)}if(u>3600)return A(Math.floor(u/3600))+"h"+A(C(u%3600/60))+"m";else if(u>60)return A(Math.floor(u/60))+"m"+A(C(u%60))+"s";else if(u>10)return A(C(u))+"s";else return A(u)+"s"}});var RD=V((r3,Qu)=>{var IF=Uu(),OF=LD(),MF=VD(),wF=ND();Qu.exports=function(u,F,E){let C=u.format,A=u.formatTime||wF,B=u.formatValue||OF,Z=u.formatBar||MF,Y=Math.floor(F.progress*100)+"",K=F.stopTime||Date.now(),G=Math.round((K-F.startTime)/1000),$=Object.assign({},E,{bar:Z(F.progress,u),percentage:B(Y,u,"percentage"),total:B(F.total,u,"total"),value:B(F.value,u,"value"),eta:B(F.eta,u,"eta"),eta_formatted:A(F.eta,u,5),duration:B(G,u,"duration"),duration_formatted:A(G,u,1)});C=C.replace(/\{(\w+)\}/g,function(X,_){if(typeof $[_]<"u")return $[_];return X});let U=Math.max(0,F.maxWidth-IF(C)-2),J=Math.floor(U/2);switch(u.align){case"right":C=U>0?" ".repeat(U)+C:C;break;case"center":C=J>0?" ".repeat(J)+C:C;break;case"left":default:break}return C}});var BD=V((i3,qu)=>{function q(D,u){if(typeof D>"u"||D===null)return u;else return D}qu.exports={parse:function(u,F){let E={},C=Object.assign({},F,u);return E.throttleTime=1000/q(C.fps,10),E.stream=q(C.stream,process.stderr),E.terminal=q(C.terminal,null),E.clearOnComplete=q(C.clearOnComplete,!1),E.stopOnComplete=q(C.stopOnComplete,!1),E.barsize=q(C.barsize,40),E.align=q(C.align,"left"),E.hideCursor=q(C.hideCursor,!1),E.linewrap=q(C.linewrap,!1),E.barGlue=q(C.barGlue,""),E.barCompleteChar=q(C.barCompleteChar,"="),E.barIncompleteChar=q(C.barIncompleteChar,"-"),E.format=q(C.format,"progress [{bar}] {percentage}% | ETA: {eta}s | {value}/{total}"),E.formatTime=q(C.formatTime,null),E.formatValue=q(C.formatValue,null),E.formatBar=q(C.formatBar,null),E.etaBufferLength=q(C.etaBuffer,10),E.etaAsynchronousUpdate=q(C.etaAsynchronousUpdate,!1),E.progressCalculationRelative=q(C.progressCalculationRelative,!1),E.synchronousUpdate=q(C.synchronousUpdate,!0),E.noTTYOutput=q(C.noTTYOutput,!1),E.notTTYSchedule=q(C.notTTYSchedule,2000),E.emptyOnZero=q(C.emptyOnZero,!1),E.forceRedraw=q(C.forceRedraw,!1),E.autopadding=q(C.autopadding,!1),E.gracefulExit=q(C.gracefulExit,!1),E},assignDerivedOptions:function(u){return u.barCompleteString=u.barCompleteChar.repeat(u.barsize+1),u.barIncompleteString=u.barIncompleteChar.repeat(u.barsize+1),u.autopaddingChar=u.autopadding?q(u.autopaddingChar," "):"",u}}});var xD=V((o3,Lu)=>{var zu=Eu(),jF=QD(),bF=RD(),SF=BD(),TF=f("events");Lu.exports=class extends TF{constructor(u){super();this.options=SF.assignDerivedOptions(u),this.terminal=this.options.terminal?this.options.terminal:new jF(this.options.stream),this.value=0,this.startValue=0,this.total=100,this.lastDrawnString=null,this.startTime=null,this.stopTime=null,this.lastRedraw=Date.now(),this.eta=new zu(this.options.etaBufferLength,0,0),this.payload={},this.isActive=!1,this.formatter=typeof this.options.format==="function"?this.options.format:bF}render(u=!1){let F={progress:this.getProgress(),eta:this.eta.getTime(),startTime:this.startTime,stopTime:this.stopTime,total:this.total,value:this.value,maxWidth:this.terminal.getWidth()};if(this.options.etaAsynchronousUpdate)this.updateETA();let E=this.formatter(this.options,F,this.payload);if(u||this.options.forceRedraw||this.options.noTTYOutput&&!this.terminal.isTTY()||this.lastDrawnString!=E)this.emit("redraw-pre"),this.terminal.cursorTo(0,null),this.terminal.write(E),this.terminal.clearRight(),this.lastDrawnString=E,this.lastRedraw=Date.now(),this.emit("redraw-post")}start(u,F,E){this.value=F||0,this.total=typeof u<"u"&&u>=0?u:100,this.startValue=F||0,this.payload=E||{},this.startTime=Date.now(),this.stopTime=null,this.lastDrawnString="",this.eta=new zu(this.options.etaBufferLength,this.startTime,this.value),this.isActive=!0,this.emit("start",u,F)}stop(){this.isActive=!1,this.stopTime=Date.now(),this.emit("stop",this.total,this.value)}update(u,F={}){if(typeof u==="number")this.value=u,this.eta.update(Date.now(),u,this.total);let E=(typeof u==="object"?u:F)||{};this.emit("update",this.total,this.value);for(let C in E)this.payload[C]=E[C];if(this.value>=this.getTotal()&&this.options.stopOnComplete)this.stop()}getProgress(){let u=this.value/this.total;if(this.options.progressCalculationRelative)u=(this.value-this.startValue)/(this.total-this.startValue);if(isNaN(u))u=this.options&&this.options.emptyOnZero?0:1;return u=Math.min(Math.max(u,0),1),u}increment(u=1,F={}){if(typeof u==="object")this.update(this.value+1,u);else this.update(this.value+u,F)}getTotal(){return this.total}setTotal(u){if(typeof u<"u"&&u>=0)this.total=u}updateETA(){this.eta.update(Date.now(),this.value,this.total)}}});var Nu=V((t3,Vu)=>{var yF=xD(),vF=BD();Vu.exports=class extends yF{constructor(u,F){super(vF.parse(u,F));if(this.timer=null,this.options.noTTYOutput&&this.terminal.isTTY()===!1)this.options.synchronousUpdate=!1;this.schedulingRate=this.terminal.isTTY()?this.options.throttleTime:this.options.notTTYSchedule,this.sigintCallback=null}render(){if(this.timer)clearTimeout(this.timer),this.timer=null;if(super.render(),this.options.noTTYOutput&&this.terminal.isTTY()===!1)this.terminal.newline();this.timer=setTimeout(this.render.bind(this),this.schedulingRate)}update(u,F){if(!this.timer)return;if(super.update(u,F),this.options.synchronousUpdate&&this.lastRedraw+this.options.throttleTime*2{var hF=QD(),PF=xD(),fF=BD(),gF=f("events");Ru.exports=class extends gF{constructor(u,F){super();this.bars=[],this.options=fF.parse(u,F),this.options.synchronousUpdate=!1,this.terminal=this.options.terminal?this.options.terminal:new hF(this.options.stream),this.timer=null,this.isActive=!1,this.schedulingRate=this.terminal.isTTY()?this.options.throttleTime:this.options.notTTYSchedule,this.loggingBuffer=[],this.sigintCallback=null}create(u,F,E,C={}){let A=new PF(Object.assign({},this.options,{terminal:this.terminal},C));if(this.bars.push(A),this.options.noTTYOutput===!1&&this.terminal.isTTY()===!1)return A;if(this.sigintCallback===null&&this.options.gracefulExit)this.sigintCallback=this.stop.bind(this),process.once("SIGINT",this.sigintCallback),process.once("SIGTERM",this.sigintCallback);if(!this.isActive){if(this.options.hideCursor===!0)this.terminal.cursor(!1);if(this.options.linewrap===!1)this.terminal.lineWrapping(!1);this.timer=setTimeout(this.update.bind(this),this.schedulingRate)}return this.isActive=!0,A.start(u,F,E),this.emit("start"),A}remove(u){let F=this.bars.indexOf(u);if(F<0)return!1;return this.bars.splice(F,1),this.update(),this.terminal.newline(),this.terminal.clearBottom(),!0}update(){if(this.timer)clearTimeout(this.timer),this.timer=null;if(this.emit("update-pre"),this.terminal.cursorRelativeReset(),this.emit("redraw-pre"),this.loggingBuffer.length>0){this.terminal.clearLine();while(this.loggingBuffer.length>0)this.terminal.write(this.loggingBuffer.shift(),!0)}for(let u=0;u0)this.terminal.newline();this.bars[u].render()}if(this.emit("redraw-post"),this.options.noTTYOutput&&this.terminal.isTTY()===!1)this.terminal.newline(),this.terminal.newline();if(this.timer=setTimeout(this.update.bind(this),this.schedulingRate),this.emit("update-post"),this.options.stopOnComplete&&!this.bars.find((u)=>u.isActive))this.stop()}stop(){if(clearTimeout(this.timer),this.timer=null,this.sigintCallback)process.removeListener("SIGINT",this.sigintCallback),process.removeListener("SIGTERM",this.sigintCallback),this.sigintCallback=null;if(this.isActive=!1,this.options.hideCursor===!0)this.terminal.cursor(!0);if(this.options.linewrap===!1)this.terminal.lineWrapping(!0);if(this.terminal.cursorRelativeReset(),this.emit("stop-pre-clear"),this.options.clearOnComplete)this.terminal.clearBottom();else{for(let u=0;u0)this.terminal.newline();this.bars[u].render(),this.bars[u].stop()}this.terminal.newline()}this.emit("stop")}log(u){this.loggingBuffer.push(u)}}});var Ou=V((D8,Iu)=>{Iu.exports={format:"progress [{bar}] {percentage}% | ETA: {eta}s | {value}/{total}",barCompleteChar:"=",barIncompleteChar:"-"}});var wu=V((u8,Mu)=>{Mu.exports={format:" {bar} {percentage}% | ETA: {eta}s | {value}/{total}",barCompleteChar:"█",barIncompleteChar:"░"}});var bu=V((F8,ju)=>{ju.exports={format:" \x1B[90m{bar}\x1B[0m {percentage}% | ETA: {eta}s | {value}/{total}",barCompleteChar:"█",barIncompleteChar:"░"}});var Tu=V((E8,Su)=>{Su.exports={format:" {bar}■ {percentage}% | ETA: {eta}s | {value}/{total}",barCompleteChar:"■",barIncompleteChar:" "}});var vu=V((C8,yu)=>{var mF=Ou(),cF=wu(),dF=bu(),lF=Tu();yu.exports={legacy:mF,shades_classic:cF,shades_grey:dF,rect:lF}});var fu=V((B8,Pu)=>{var hu=Nu(),pF=xu(),nF=vu(),sF=RD(),aF=LD(),rF=VD(),iF=ND();Pu.exports={Bar:hu,SingleBar:hu,MultiBar:pF,Presets:nF,Format:{Formatter:sF,BarFormat:rF,ValueFormat:aF,TimeFormat:iF}}});import{join as CD,basename as tD,extname as eD}from"node:path";import{randomUUID as zF}from"node:crypto";import{rm as Du}from"node:fs/promises";import{spawn as p}from"node:child_process";import{appendFile as EF}from"node:fs/promises";var KD=null;function XD(D){KD=D}async function v(D){if(KD)try{await EF(KD,D,"utf-8")}catch(u){}}async function n(){return new Promise((D)=>{let u=p("ffmpeg",["-version"]);u.on("error",()=>D(!1)),u.on("close",(F)=>D(F===0))})}async function s(){return new Promise((D)=>{let u=p("MP4Box",["-version"]);u.on("error",()=>D(!1)),u.on("close",(F)=>D(F===0))})}async function a(){return new Promise((D)=>{let u=p("ffmpeg",["-hide_banner","-encoders"]),F="";u.stdout.on("data",(E)=>{F+=E.toString()}),u.on("error",()=>D(!1)),u.on("close",(E)=>{if(E!==0)D(!1);else D(F.includes("h264_nvenc")||F.includes("hevc_nvenc"))})})}async function r(){return new Promise((D)=>{let u=p("ffmpeg",["-hide_banner","-encoders"]),F="";u.stdout.on("data",(E)=>{F+=E.toString()}),u.on("error",()=>D({available:!1})),u.on("close",(E)=>{if(E!==0)D({available:!1});else if(F.includes("av1_nvenc"))D({available:!0,encoder:"av1_nvenc"});else if(F.includes("av1_qsv"))D({available:!0,encoder:"av1_qsv"});else if(F.includes("av1_amf"))D({available:!0,encoder:"av1_amf"});else D({available:!1})})})}async function g(D,u,F){let C=` +=== FFmpeg Command [${new Date().toISOString()}] === +ffmpeg ${D.join(" ")} +`;return await v(C),new Promise((A,B)=>{let Z=p("ffmpeg",D),Y="";Z.stderr.on("data",(K)=>{let G=K.toString();if(Y+=G,u&&F){let $=G.match(/time=(\d{2}):(\d{2}):(\d{2}\.\d{2})/);if($){let U=parseInt($[1]),J=parseInt($[2]),X=parseFloat($[3]),_=U*3600+J*60+X,W=Math.min(100,_/F*100);u(W)}}}),Z.on("error",(K)=>{v(`ERROR: ${K.message} +`),B(Error(`FFmpeg error: ${K.message}`))}),Z.on("close",(K)=>{if(K===0)v(`SUCCESS: Exit code ${K} +`),A();else v(`FAILED: Exit code ${K} +${Y} +`),B(Error(`FFmpeg failed with exit code ${K} +${Y}`))})})}async function GD(D){let F=` +=== MP4Box Command [${new Date().toISOString()}] === +MP4Box ${D.join(" ")} +`;return await v(F),new Promise((E,C)=>{let A=p("MP4Box",D),B="",Z="";A.stdout.on("data",(Y)=>{B+=Y.toString()}),A.stderr.on("data",(Y)=>{Z+=Y.toString()}),A.on("error",(Y)=>{v(`ERROR: ${Y.message} +`),C(Error(`MP4Box error: ${Y.message}`))}),A.on("close",(Y)=>{if(Y===0)v(`SUCCESS: Exit code ${Y} +`),E();else{let K=Z||B;v(`FAILED: Exit code ${Y} +${K} +`),C(Error(`MP4Box failed with exit code ${Y} +${K}`))}})})}import{spawn as CF}from"node:child_process";async function i(D){return new Promise((u,F)=>{let E=CF("ffprobe",["-v","error","-show_entries","stream=width,height,duration,r_frame_rate,codec_name,codec_type,bit_rate","-show_entries","format=duration","-of","json",D]),C="";E.stdout.on("data",(A)=>{C+=A.toString()}),E.on("error",(A)=>{F(Error(`ffprobe error: ${A.message}`))}),E.on("close",(A)=>{if(A!==0){F(Error(`ffprobe failed with exit code ${A}`));return}try{let B=JSON.parse(C),Z=B.streams.find((X)=>X.codec_type==="video"),Y=B.streams.find((X)=>X.codec_type==="audio"&&X.bit_rate),K=B.format;if(!Z){F(Error("No video stream found in input file"));return}let G=30;if(Z.r_frame_rate){let[X,_]=Z.r_frame_rate.split("/").map(Number);if(X&&_&&_!==0)G=X/_}let $=parseFloat(Z.duration||K.duration||"0"),U=Y?.bit_rate?Math.round(parseInt(Y.bit_rate)/1000):void 0,J=Z.bit_rate?Math.round(parseInt(Z.bit_rate)/1000):void 0;u({width:Z.width,height:Z.height,duration:$,fps:G,codec:Z.codec_name,audioBitrate:U,videoBitrate:J})}catch(B){F(Error(`Failed to parse ffprobe output: ${B}`))}})})}function uD(D,u=256){if(!D)return`${u}k`;let F=Math.min(D,u);if(F<=64)return"64k";if(F<=96)return"96k";if(F<=128)return"128k";if(F<=192)return"192k";return"256k"}function FD(D){let u=Math.floor(D/3600),F=Math.floor(D%3600/60),E=D%60;return`${String(u).padStart(2,"0")}:${String(F).padStart(2,"0")}:${E.toFixed(3).padStart(6,"0")}`}import{mkdir as BF,access as AF,constants as ZF}from"node:fs/promises";async function m(D){try{await AF(D,ZF.F_OK)}catch{await BF(D,{recursive:!0})}}function YF(D,u){let F=D*u;if(F<=230400)return 0.08;if(F<=409920)return 0.075;if(F<=921600)return 0.07;if(F<=2073600)return 0.065;if(F<=3686400)return 0.06;return 0.055}function y(D,u,F=30,E){let C=YF(D,u),A=Math.round(D*u*F*C/1000);if(E&&A>E)A=E;return`${A}k`}var _D=[{name:"360p",width:640,height:360,videoBitrate:y(640,360,30),audioBitrate:"192k"},{name:"480p",width:854,height:480,videoBitrate:y(854,480,30),audioBitrate:"192k"},{name:"720p",width:1280,height:720,videoBitrate:y(1280,720,30),audioBitrate:"192k"},{name:"1080p",width:1920,height:1080,videoBitrate:y(1920,1080,30),audioBitrate:"256k"},{name:"1440p",width:2560,height:1440,videoBitrate:y(2560,1440,30),audioBitrate:"256k"},{name:"2160p",width:3840,height:2160,videoBitrate:y(3840,2160,30),audioBitrate:"256k"}];function UD(D,u,F=30,E){let C=[],A=_D.filter((B)=>{return B.width<=D&&B.height<=u});for(let B of A)C.push({...B,videoBitrate:y(B.width,B.height,30,E),fps:30});return C}function $F(D,u,F){return{...D,name:`${D.name}-${u}`,videoBitrate:y(D.width,D.height,u,F),fps:u}}function mD(D){let F=D.trim().match(/^(\d+)p?(?:[@-](\d+))?$/i);if(!F)return null;let E=F[1]+"p",C=F[2]?parseInt(F[2]):30;return{resolution:E,fps:C}}function cD(D,u=30,F){let E=_D.find((C)=>C.name===D);if(!E)return null;if(u===30)return{...E,videoBitrate:y(E.width,E.height,30,F),fps:30};return $F(E,u,F)}function JF(D,u,F,E){let C=mD(D);if(!C)return{error:`Invalid profile format: ${D}. Use format like: 360, 720@60, 1080-60`};let A=cD(C.resolution,C.fps);if(!A)return{error:`Unknown resolution: ${C.resolution}. Available: 360, 480, 720, 1080, 1440, 2160`};if(A.width>u||A.height>F)return{error:`Source resolution (${u}x${F}) is lower than ${D} (${A.width}x${A.height})`};let B=120,Z=C.fps,Y;if(C.fps>E)Z=Math.min(E,B),Y=`Requested ${C.fps} FPS in ${D}, but source is ${E} FPS. Using ${Z} FPS instead`;else if(C.fps>B)Z=B,Y=`Requested ${C.fps} FPS in ${D} exceeds maximum ${B} FPS. Using ${Z} FPS instead`;return Y?{warning:Y,adjustedFps:Z}:{}}function dD(D,u,F,E,C){let A=[],B=[],Z=[];for(let Y of D){let K=JF(Y,u,F,E);if(K.error){B.push(K.error);continue}if(K.warning)Z.push(K.warning);let G=mD(Y);if(!G)continue;let $=K.adjustedFps!==void 0?K.adjustedFps:G.fps,U=cD(G.resolution,$,C);if(U)A.push(U)}return{profiles:A,errors:B,warnings:Z}}import{join as h}from"node:path";import{readdir as KF,unlink as lD,rmdir as XF,writeFile as pD}from"node:fs/promises";async function nD(D,u,F="00:00:01"){let E=h(u,"poster.jpg"),C=/^\d+(\.\d+)?$/.test(F)?F:F;return await g(["-ss",C,"-i",D,"-vframes","1","-q:v","2","-y",E]),E}async function sD(D,u,F,E){let{width:C,height:A,interval:B,columns:Z}=E,Y=h(u,".thumbnails_temp");await m(Y),await pD(h(Y,".keep"),"");let K=h(Y,"thumb_%04d.jpg");await g(["-i",D,"-vf",`fps=1/${B},scale=${C}:${A}`,"-q:v","5",K]);let $=(await KF(Y)).filter((R)=>R.startsWith("thumb_")&&R.endsWith(".jpg")).sort();if($.length===0)throw Error("No thumbnails generated");let U=$.length,J=Math.ceil(U/Z),X=h(u,"thumbnails.jpg"),_=`tile=${Z}x${J}`;await g(["-i",K,"-filter_complex",_,"-q:v","5",X]);let W=h(u,"thumbnails.vtt"),k=GF(U,B,C,A,Z,"thumbnails.jpg");await pD(W,k);for(let R of $)await lD(h(Y,R));return await lD(h(Y,".keep")),await XF(Y),{spritePath:X,vttPath:W}}function GF(D,u,F,E,C,A){let B=`WEBVTT -`;for(let q=0;q ${p(K)} -`,B+=`${A}#xywh=${G},${Z},${F},${C} +`;for(let Z=0;Z ${FD(K)} +`,B+=`${A}#xywh=${U},${J},${F},${E} -`}return B}import{join as su}from"node:path";async function TD(u,D,F,C,E,A,B,q,Y,K,Q,L){let G=su(D,`video_${K}_${F.name}.mp4`),Z=["-y","-i",u,"-c:v",C];if(C==="h264_nvenc")Z.push("-rc:v","vbr"),Z.push("-preset",E),Z.push("-2pass","0");else if(C==="av1_nvenc")Z.push("-rc:v","vbr"),Z.push("-preset",E),Z.push("-2pass","0");else if(C==="av1_qsv")Z.push("-preset",E),Z.push("-global_quality","23");else if(C==="av1_amf")Z.push("-quality","balanced"),Z.push("-rc","vbr_latency");else if(C==="libsvtav1")Z.push("-preset",E),Z.push("-svtav1-params","tune=0:enable-overlays=1");else Z.push("-preset",E);let X=K==="av1"?0.6:1,J=Math.round(parseInt(F.videoBitrate)*X),W=`${J}k`;Z.push("-b:v",W,"-maxrate",W,"-bufsize",`${J*2}k`);let I=Math.round(q*B);Z.push("-g",String(I),"-keyint_min",String(I),"-sc_threshold","0");let U=[`scale=${F.width}:${F.height}`];if(Q){if(Q.deinterlace)U.push("yadif");if(Q.denoise)U.push("hqdn3d");if(Q.customFilters)U.push(...Q.customFilters)}Z.push("-vf",U.join(","));let N=parseInt(F.audioBitrate)||256,V=n(Y,N);if(Z.push("-c:a","aac","-b:a",V),Q?.audioNormalize)Z.push("-af","loudnorm");return Z.push("-f","mp4",G),await w(Z,L,A),G}async function bD(u,D,F,C,E,A,B,q,Y,K,Q,L,G,Z){let X=new Map;if(K&&F.length>1)for(let J=0;JTD(u,D,N,C,E,A,B,q,Y,L,G,(V)=>{if(Z)Z(N.name,V)}));(await Promise.all(I)).forEach((N,V)=>{let x=W[V];X.set(x.name,N)})}else for(let J of F){let W=await TD(u,D,J,C,E,A,B,q,Y,L,G,(I)=>{if(Z)Z(J.name,I)});X.set(J.name,W)}return X}import{join as O}from"node:path";async function vD(u,D,F,C,E){let A=O(D,"manifest.mpd"),B=["-dash",String(C*1000),"-frag",String(C*1000),"-rap","-segment-timeline","-segment-name","$RepresentationID$_$Number$","-out",A],q=!0;for(let[Y,K]of u.entries())for(let Q of F){let L=K.get(Q.name);if(!L)throw Error(`MP4 file not found for profile: ${Q.name}, codec: ${Y}`);let G=E==="dual"?`${Q.name}-${Y}`:Q.name;if(B.push(`${L}#video:id=${G}`),q)B.push(`${L}#audio:id=audio`),q=!1}return await uD(B),await ru(D,F,E),await iu(A,F,E),A}async function ru(u,D,F){let{readdir:C,rename:E,mkdir:A}=await import("node:fs/promises"),B=[];if(F==="h264"||F==="dual")B.push("h264");if(F==="av1"||F==="dual")B.push("av1");let q=[];for(let Q of B)for(let L of D){let G=F==="dual"?`${L.name}-${Q}`:L.name;q.push(G);let Z=O(u,G);await A(Z,{recursive:!0})}let Y=O(u,"audio");await A(Y,{recursive:!0});let K=await C(u);for(let Q of K){if(Q==="manifest.mpd")continue;if(Q.startsWith("audio_")||Q==="audio_init.m4s"){let L=O(u,Q),G=O(Y,Q);await E(L,G);continue}for(let L of q)if(Q.startsWith(`${L}_`)){let G=O(u,Q),Z=O(u,L,Q);await E(G,Z);break}}}async function iu(u,D,F){let{readFile:C,writeFile:E}=await import("node:fs/promises"),A=await C(u,"utf-8");A=A.replace(/media="\$RepresentationID\$_\$Number\$\.m4s"/g,'media="$RepresentationID$/$RepresentationID$_$Number$.m4s"'),A=A.replace(/initialization="\$RepresentationID\$_\.mp4"/g,'initialization="$RepresentationID$/$RepresentationID$_.mp4"'),await E(u,A,"utf-8")}async function ED(u){let{input:D,outputDir:F,segmentDuration:C=2,profiles:E,customProfiles:A,codec:B="dual",useNvenc:q,generateThumbnails:Y=!0,thumbnailConfig:K={},generatePoster:Q=!0,posterTimecode:L="00:00:01",parallel:G=!0,onProgress:Z}=u,X=hD("/tmp",`dash-converter-${eu()}`);await f(X);try{return await DF(D,F,X,C,E,A,B,q,Y,K,Q,L,G,Z)}finally{try{await PD(X,{recursive:!0,force:!0})}catch(J){console.warn(`Warning: Failed to cleanup temp directory: ${X}`)}}}async function DF(u,D,F,C,E,A,B,q,Y,K,Q,L,G,Z){if(!await T())throw Error("FFmpeg is not installed or not in PATH");if(!await b())throw Error("MP4Box is not installed or not in PATH. Install gpac package.");let X=(R,_,m,e)=>{if(Z)Z({stage:R,percent:_,message:m,currentProfile:e})};X("analyzing",0,"Analyzing input video...");let J=await P(u),W=q!==!1?await v():!1,I=q===!0?!0:q===!1?!1:W;if(q===!0&&!W)throw Error("NVENC requested but not available. Check NVIDIA drivers and GPU support.");let U;if(A&&A.length>0){let R=jD(A,J.width,J.height,J.fps,J.videoBitrate);if(R.errors.length>0){console.warn(` -⚠️ Profile warnings:`);for(let _ of R.errors)console.warn(` - ${_}`);console.warn("")}if(U=R.profiles,U.length===0)throw Error("No valid profiles found in custom list. Check warnings above.")}else if(E)U=E;else U=CD(J.width,J.height,J.fps,J.videoBitrate);if(U.length===0)throw Error("No suitable profiles found for input video resolution");let N=ou(u,tu(u)),V=hD(D,N);try{await PD(V,{recursive:!0,force:!0})}catch(R){}await f(V);let x=[];if(B==="h264"||B==="dual"){let R=I?"h264_nvenc":"libx264",_=I?"p4":"medium";x.push({type:"h264",codec:R,preset:_})}if(B==="av1"||B==="dual"){let R=await h(),_=R.available?R.encoder:"libsvtav1",m=R.available?_==="av1_nvenc"?"p4":"medium":"8";x.push({type:"av1",codec:_,preset:m})}let Nu=x.map((R)=>R.type.toUpperCase()).join(" + ");X("analyzing",20,`Using ${Nu} encoding (${I?"GPU":"CPU"})`,void 0);let ju=I?3:2,t=new Map;for(let R=0;R{let yF=U.findIndex((Su)=>Su.name===c),VD=25+HD*40,zD=WD/100*(40*Ou/U.length);if(X("encoding",VD+zD,`Encoding ${_.toUpperCase()} ${c}...`,`${_}-${c}`),Z)Z({stage:"encoding",percent:VD+zD,currentProfile:`${_}-${c}`,profilePercent:WD,message:`Encoding ${_.toUpperCase()} ${c}...`})});t.set(_,yu)}X("encoding",65,"Stage 1 complete: All codecs and profiles encoded"),X("encoding",70,"Stage 2: Creating DASH with MP4Box...");let wu=await vD(t,V,U,C,B),UD=[];for(let R of t.values())UD.push(...Array.from(R.values()));X("encoding",80,"Stage 2 complete: DASH created");let kD,_D;if(Y){X("thumbnails",80,"Generating thumbnail sprites...");let R={width:K.width||160,height:K.height||90,interval:K.interval||1,columns:K.columns||10},_=await SD(u,V,J.duration,R);kD=_.spritePath,_D=_.vttPath,X("thumbnails",90,"Thumbnails generated")}let ID;if(Q)X("thumbnails",92,"Generating poster image..."),ID=await yD(u,V,L),X("thumbnails",95,"Poster generated");return X("manifest",95,"Finalizing manifest..."),X("complete",100,"Conversion complete!"),{manifestPath:wu,videoPaths:UD,thumbnailSpritePath:kD,thumbnailVttPath:_D,posterPath:ID,duration:J.duration,profiles:U,usedNvenc:I,codecType:B}}var GD=DD(Wu(),1);import{statSync as jF}from"node:fs";var H=process.argv.slice(2),i,o,g="dual",RD=[];for(let u=0;uC.trim()).filter((C)=>C.length>0)}else if(H[u]==="-p"||H[u]==="--poster")o=H[u+1],u++;else if(H[u]==="-c"||H[u]==="--codec"){let D=H[u+1];if(D==="av1"||D==="h264"||D==="dual")g=D;else console.error(`❌ Invalid codec: ${D}. Valid options: av1, h264, dual`),process.exit(1);u++}else if(!H[u].startsWith("-"))RD.push(H[u]);var d=RD[0],Vu=RD[1]||".";if(!d)console.error("❌ Usage: dvc-cli [output-dir] [-r resolutions] [-c codec] [-p poster-timecode]"),console.error(` -Options:`),console.error(" -r, --resolutions Video resolutions (e.g., 360,480,720 or 720@60,1080@60)"),console.error(" -c, --codec Video codec: av1, h264, or dual (default: dual)"),console.error(" -p, --poster Poster timecode (e.g., 00:00:05 or 10)"),console.error(` -Examples:`),console.error(" dvc-cli video.mp4"),console.error(" dvc-cli video.mp4 ./output"),console.error(" dvc-cli video.mp4 -r 360,480,720"),console.error(" dvc-cli video.mp4 -c av1"),console.error(" dvc-cli video.mp4 -c h264"),console.error(" dvc-cli video.mp4 -c dual"),console.error(" dvc-cli video.mp4 -r 720@60,1080@60,2160@60 -c av1"),console.error(" dvc-cli video.mp4 -p 00:00:05"),console.error(" dvc-cli video.mp4 ./output -r 720,1080 -c dual -p 10"),process.exit(1);console.log(`\uD83D\uDD0D Checking system... -`);var zu=await T(),xu=await v(),$D=await h(),Mu=await b();console.log(`FFmpeg: ${zu?"✅":"❌"}`);console.log(`NVENC (H.264): ${xu?"✅ (GPU acceleration)":"⚠️ (CPU only)"}`);if($D.available)console.log(`AV1 Encoder: ✅ ${$D.encoder} (GPU acceleration)`);else console.log("AV1 Encoder: ⚠️ (not available, will use CPU fallback)");console.log(`MP4Box: ${Mu?"✅":"❌"} -`);if(!zu)console.error("❌ FFmpeg not found. Please install FFmpeg first."),process.exit(1);if(!Mu)console.error("❌ MP4Box not found. Please install: sudo pacman -S gpac"),process.exit(1);if((g==="av1"||g==="dual")&&!$D.available)console.error("⚠️ Warning: AV1 encoding requested but no hardware AV1 encoder found."),console.error(" CPU-based AV1 encoding (libsvtav1) will be VERY slow."),console.error(` Consider using --codec h264 for faster encoding. -`);console.log(`\uD83D\uDCCA Analyzing video... -`);var z=await P(d),wF=jF(d),OF=(wF.size/1048576).toFixed(2);console.log("\uD83D\uDCF9 Video Information:");console.log(` File: ${d}`);console.log(` Size: ${OF} MB`);console.log(` Resolution: ${z.width}x${z.height}`);console.log(` FPS: ${z.fps.toFixed(2)}`);console.log(` Duration: ${Math.floor(z.duration/60)}m ${Math.floor(z.duration%60)}s`);console.log(` Codec: ${z.codec}`);if(z.videoBitrate)console.log(` Video Bitrate: ${(z.videoBitrate/1000).toFixed(2)} Mbps`);if(z.audioBitrate)console.log(` Audio Bitrate: ${z.audioBitrate} kbps`);console.log(` -\uD83D\uDCC1 Output: ${Vu}`);console.log(`\uD83C\uDFAC Codec: ${g}${g==="dual"?" (AV1 + H.264 for maximum compatibility)":""}`);if(i)console.log(`\uD83C\uDFAF Custom profiles: ${i.join(", ")}`);if(o)console.log(`\uD83D\uDDBC️ Poster timecode: ${o}`);console.log(` +`}return B}import{join as _F}from"node:path";function UF(D,u,F){if(F)if(u==="h264")return 32;else return 42;else if(u==="h264"){if(D<=360)return 25;if(D<=480)return 24;if(D<=720)return 23;if(D<=1080)return 22;if(D<=1440)return 21;return 20}else{if(D<=360)return 40;if(D<=480)return 38;if(D<=720)return 35;if(D<=1080)return 32;if(D<=1440)return 30;return 28}}async function aD(D,u,F,E,C,A,B,Z,Y,K,G,$){let U=_F(u,`video_${Y}_${F.name}.mp4`),J=["-y","-i",D,"-c:v",E],X=E.includes("nvenc")||E.includes("qsv")||E.includes("amf"),_;if(X&&K?.cq!==void 0)_=K.cq;else if(!X&&K?.crf!==void 0)_=K.crf;else _=UF(F.height,Y,X);if(E==="h264_nvenc")J.push("-rc:v","vbr"),J.push("-cq",String(_)),J.push("-preset",C),J.push("-2pass","0");else if(E==="av1_nvenc")J.push("-rc:v","vbr"),J.push("-cq",String(_)),J.push("-preset",C),J.push("-2pass","0");else if(E==="av1_qsv")J.push("-preset",C),J.push("-global_quality",String(_));else if(E==="av1_amf")J.push("-quality","balanced"),J.push("-rc","cqp"),J.push("-qp_i",String(_)),J.push("-qp_p",String(_));else if(E==="libsvtav1")J.push("-crf",String(_)),J.push("-preset",C),J.push("-svtav1-params","tune=0:enable-overlays=1");else if(E==="libx264")J.push("-crf",String(_)),J.push("-preset",C);else J.push("-preset",C);let W=Y==="av1"?0.6:1,k=Math.round(parseInt(F.videoBitrate)*W*1.5);J.push("-maxrate",`${k}k`),J.push("-bufsize",`${k*2}k`);let R=F.fps||30,N=Math.round(R*B);J.push("-g",String(N),"-keyint_min",String(N),"-sc_threshold","0");let H=[`scale=${F.width}:${F.height}`];if(G){if(G.deinterlace)H.push("yadif");if(G.denoise)H.push("hqdn3d");if(G.customFilters)H.push(...G.customFilters)}J.push("-vf",H.join(","));let T=parseInt(F.audioBitrate)||256,I=uD(Z,T);if(J.push("-c:a","aac","-b:a",I),G?.audioNormalize)J.push("-af","loudnorm");return J.push("-f","mp4",U),await g(J,$,A),U}async function rD(D,u,F,E,C,A,B,Z,Y,K,G,$,U,J){let X=new Map;if(Y&&F.length>1)for(let _=0;_aD(D,u,N,E,C,A,B,Z,G,$,U,(H)=>{if(J)J(N.name,H)}));(await Promise.all(k)).forEach((N,H)=>{let T=W[H];X.set(T.name,N)})}else for(let _ of F){let W=await aD(D,u,_,E,C,A,B,Z,G,$,U,(k)=>{if(J)J(_.name,k)});X.set(_.name,W)}return X}import{join as x}from"node:path";import{readFile as WF,writeFile as ED,readdir as WD,rename as kD,mkdir as iD}from"node:fs/promises";async function kF(D){let{readFile:u,writeFile:F}=await import("node:fs/promises"),C=(await u(D,"utf-8")).split(` +`),A=[],B=0;while(B")){let X=C[B];if(X.includes(""))J=!1}else if(X.includes("0&&U.length>0)A.push(Z),G.forEach((X)=>A.push(X)),$.forEach((X)=>A.push(X)),A.push(" "),A.push(Z),G.forEach((X)=>A.push(X)),U.forEach((X)=>A.push(X)),A.push(" ");else{A.push(Z);for(let X=Y+1;XH.endsWith(".m4s")).sort((H,T)=>{let I=parseInt(H.match(/_(\d+)\.m4s$/)?.[1]||"0"),O=parseInt(T.match(/_(\d+)\.m4s$/)?.[1]||"0");return I-O}),W=X.find((H)=>H.endsWith("_.mp4"));if(!W||_.length===0)continue;let k=`#EXTM3U +`;k+=`#EXT-X-VERSION:6 +`,k+=`#EXT-X-TARGETDURATION:${Math.ceil(F)} +`,k+=`#EXT-X-MEDIA-SEQUENCE:1 +`,k+=`#EXT-X-INDEPENDENT-SEGMENTS +`,k+=`#EXT-X-MAP:URI="${W}" +`;for(let H of _)k+=`#EXTINF:${F}, +`,k+=`${H} +`;k+=`#EXT-X-ENDLIST +`;let R=x(J,"playlist.m3u8");await ED(R,k,"utf-8");let N=parseInt($.videoBitrate)*1000;A.push({path:`${U}/playlist.m3u8`,bandwidth:N,resolution:`${$.width}x${$.height}`,fps:$.fps||30})}let B=x(D,"audio"),Z=await WD(B),Y=Z.filter(($)=>$.endsWith(".m4s")).sort(($,U)=>{let J=parseInt($.match(/_(\d+)\.m4s$/)?.[1]||"0"),X=parseInt(U.match(/_(\d+)\.m4s$/)?.[1]||"0");return J-X}),K=Z.find(($)=>$.endsWith("_.mp4"));if(K&&Y.length>0){let $=`#EXTM3U +`;$+=`#EXT-X-VERSION:6 +`,$+=`#EXT-X-TARGETDURATION:${Math.ceil(F)} +`,$+=`#EXT-X-MEDIA-SEQUENCE:1 +`,$+=`#EXT-X-INDEPENDENT-SEGMENTS +`,$+=`#EXT-X-MAP:URI="${K}" +`;for(let U of Y)$+=`#EXTINF:${F}, +`,$+=`${U} +`;$+=`#EXT-X-ENDLIST +`,await ED(x(B,"playlist.m3u8"),$,"utf-8")}let G=`#EXTM3U +`;G+=`#EXT-X-VERSION:6 +`,G+=`#EXT-X-INDEPENDENT-SEGMENTS + +`,G+=`#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="audio",AUTOSELECT=YES,URI="audio/playlist.m3u8",CHANNELS="2" + +`;for(let $ of A)G+=`#EXT-X-STREAM-INF:BANDWIDTH=${$.bandwidth},CODECS="avc1.4D4020,mp4a.40.2",RESOLUTION=${$.resolution},FRAME-RATE=${$.fps},AUDIO="audio" +`,G+=`${$.path} + +`;return await ED(C,G,"utf-8"),C}async function HD(D){let{input:u,outputDir:F,segmentDuration:E=2,profiles:C,customProfiles:A,codec:B="dual",format:Z="both",useNvenc:Y,quality:K,generateThumbnails:G=!0,thumbnailConfig:$={},generatePoster:U=!0,posterTimecode:J="00:00:01",parallel:X=!0,onProgress:_}=D,W=CD("/tmp",`dash-converter-${zF()}`);await m(W);let k=tD(u,eD(u)),R=CD(F,k);await m(R);let N=CD(R,"conversion.log");XD(N);let{writeFile:H}=await import("node:fs/promises"),T=`=========================================== +DASH Conversion Log +Started: ${new Date().toISOString()} +Input: ${u} +Output: ${R} +Codec: ${B} +Format: ${Z} +=========================================== +`;await H(N,T,"utf-8");try{return await LF(u,F,W,E,C,A,B,Z,Y,K,G,$,U,J,X,_)}finally{let{appendFile:I}=await import("node:fs/promises");try{await I(N,` +Completed: ${new Date().toISOString()} +`,"utf-8")}catch(O){}try{await Du(W,{recursive:!0,force:!0})}catch(O){console.warn(`Warning: Failed to cleanup temp directory: ${W}`)}}}async function LF(D,u,F,E,C,A,B,Z,Y,K,G,$,U,J,X,_){if(!await n())throw Error("FFmpeg is not installed or not in PATH");if(!await s())throw Error("MP4Box is not installed or not in PATH. Install gpac package.");let W=(z,L,o,JD)=>{if(_)_({stage:z,percent:L,message:o,currentProfile:JD})};W("analyzing",0,"Analyzing input video...");let k=await i(D),R=Y!==!1?await a():!1,N=Y===!0?!0:Y===!1?!1:R;if(Y===!0&&!R)throw Error("NVENC requested but not available. Check NVIDIA drivers and GPU support.");let H;if(A&&A.length>0){let z=dD(A,k.width,k.height,k.fps,k.videoBitrate);if(z.errors.length>0){console.warn(` +❌ Profile errors:`);for(let L of z.errors)console.warn(` - ${L}`);console.warn("")}if(z.warnings.length>0){console.warn(` +⚠️ Profile warnings:`);for(let L of z.warnings)console.warn(` - ${L}`);console.warn("")}if(H=z.profiles,H.length===0)throw Error("No valid profiles found in custom list. Check errors above.")}else if(C)H=C;else H=UD(k.width,k.height,k.fps,k.videoBitrate);if(H.length===0)throw Error("No suitable profiles found for input video resolution");let T=tD(D,eD(D)),I=CD(u,T);try{await Du(I,{recursive:!0,force:!0})}catch(z){}await m(I);let O=[];if(B==="h264"||B==="dual"){let z=N?"h264_nvenc":"libx264",L=N?"p4":"medium";O.push({type:"h264",codec:z,preset:L})}if(B==="av1"||B==="dual"){let z=await r(),L=z.available?z.encoder:"libsvtav1",o=z.available?L==="av1_nvenc"?"p4":"medium":"8";O.push({type:"av1",codec:L,preset:o})}let lu=O.map((z)=>z.type.toUpperCase()).join(" + ");W("analyzing",20,`Using ${lu} encoding (${N?"GPU":"CPU"})`,void 0);let pu=N?3:2,$D=new Map;for(let z=0;z{let D3=H.findIndex((ou)=>ou.name===t),PD=25+vD*40,fD=hD/100*(40*au/H.length);if(W("encoding",PD+fD,`Encoding ${L.toUpperCase()} ${t}...`,`${L}-${t}`),_)_({stage:"encoding",percent:PD+fD,currentProfile:`${L}-${t}`,profilePercent:hD,message:`Encoding ${L.toUpperCase()} ${t}...`})});$D.set(L,iu)}W("encoding",65,"Stage 1 complete: All codecs and profiles encoded"),W("encoding",70,"Stage 2: Creating segments and manifests...");let{manifestPath:nu,hlsManifestPath:su}=await oD($D,I,H,E,B,Z),bD=[];for(let z of $D.values())bD.push(...Array.from(z.values()));W("encoding",80,"Stage 2 complete: All formats packaged");let SD,TD;if(G){W("thumbnails",80,"Generating thumbnail sprites...");let z={width:$.width||160,height:$.height||90,interval:$.interval||1,columns:$.columns||10},L=await sD(D,I,k.duration,z);SD=L.spritePath,TD=L.vttPath,W("thumbnails",90,"Thumbnails generated")}let yD;if(U)W("thumbnails",92,"Generating poster image..."),yD=await nD(D,I,J),W("thumbnails",95,"Poster generated");return W("manifest",95,"Finalizing..."),W("complete",100,"Conversion complete!"),{manifestPath:nu,hlsManifestPath:su,videoPaths:bD,thumbnailSpritePath:SD,thumbnailVttPath:TD,posterPath:yD,duration:k.duration,profiles:H,usedNvenc:N,codecType:B,format:Z}}var MD=e(fu(),1);import{statSync as oF}from"node:fs";var Q=process.argv.slice(2),ZD,YD,l="dual",d="both",jD=[],M,w,j,b;for(let D=0;DE.trim()).filter((E)=>E.length>0)}else if(Q[D]==="-p"||Q[D]==="--poster")YD=Q[D+1],D++;else if(Q[D]==="-c"||Q[D]==="--codec"){let u=Q[D+1];if(u==="av1"||u==="h264"||u==="dual")l=u;else console.error(`❌ Invalid codec: ${u}. Valid options: av1, h264, dual`),process.exit(1);D++}else if(Q[D]==="-f"||Q[D]==="--format"){let u=Q[D+1];if(u==="dash"||u==="hls"||u==="both")d=u;else console.error(`❌ Invalid format: ${u}. Valid options: dash, hls, both`),process.exit(1);D++}else if(Q[D]==="--h264-cq"){if(M=parseInt(Q[D+1]),isNaN(M)||M<0||M>51)console.error(`❌ Invalid H.264 CQ value: ${Q[D+1]}. Must be 0-51`),process.exit(1);D++}else if(Q[D]==="--h264-crf"){if(w=parseInt(Q[D+1]),isNaN(w)||w<0||w>51)console.error(`❌ Invalid H.264 CRF value: ${Q[D+1]}. Must be 0-51`),process.exit(1);D++}else if(Q[D]==="--av1-cq"){if(j=parseInt(Q[D+1]),isNaN(j)||j<0||j>51)console.error(`❌ Invalid AV1 CQ value: ${Q[D+1]}. Must be 0-51`),process.exit(1);D++}else if(Q[D]==="--av1-crf"){if(b=parseInt(Q[D+1]),isNaN(b)||b<0||b>63)console.error(`❌ Invalid AV1 CRF value: ${Q[D+1]}. Must be 0-63`),process.exit(1);D++}else if(!Q[D].startsWith("-"))jD.push(Q[D]);var DD=jD[0],gu=jD[1]||".";if(!DD)console.error("❌ Usage: dvc-cli [output-dir] [options]"),console.error(` +Options:`),console.error(" -r, --resolutions Video resolutions (e.g., 360,480,720 or 720@60,1080@60)"),console.error(" -c, --codec Video codec: av1, h264, or dual (default: dual)"),console.error(" -f, --format Streaming format: dash, hls, or both (default: both)"),console.error(" -p, --poster Poster timecode (e.g., 00:00:05 or 10)"),console.error(` +Quality Options (override defaults):`),console.error(" --h264-cq H.264 GPU CQ value (0-51, lower = better, default: auto)"),console.error(" --h264-crf H.264 CPU CRF value (0-51, lower = better, default: auto)"),console.error(" --av1-cq AV1 GPU CQ value (0-51, lower = better, default: auto)"),console.error(" --av1-crf AV1 CPU CRF value (0-63, lower = better, default: auto)"),console.error(` +Examples:`),console.error(" dvc-cli video.mp4"),console.error(" dvc-cli video.mp4 ./output"),console.error(" dvc-cli video.mp4 -r 360,480,720"),console.error(" dvc-cli video.mp4 -c av1 --av1-cq 40"),console.error(" dvc-cli video.mp4 -c dual --h264-cq 30 --av1-cq 39"),console.error(" dvc-cli video.mp4 -f hls"),console.error(" dvc-cli video.mp4 -c dual -f both"),console.error(" dvc-cli video.mp4 -r 720@60,1080@60,2160@60 -c av1 -f dash"),console.error(" dvc-cli video.mp4 -p 00:00:05"),console.error(" dvc-cli video.mp4 ./output -r 720,1080 -c dual -f both -p 10 --h264-cq 28 --av1-cq 37"),process.exit(1);console.log(`\uD83D\uDD0D Checking system... +`);var mu=await n(),cu=await a(),wD=await r(),du=await s();console.log(`FFmpeg: ${mu?"✅":"❌"}`);console.log(`NVENC (H.264): ${cu?"✅ (GPU acceleration)":"⚠️ (CPU only)"}`);if(wD.available)console.log(`AV1 Encoder: ✅ ${wD.encoder} (GPU acceleration)`);else console.log("AV1 Encoder: ⚠️ (not available, will use CPU fallback)");console.log(`MP4Box: ${du?"✅":"❌"} +`);if(!mu)console.error("❌ FFmpeg not found. Please install FFmpeg first."),process.exit(1);if(!du)console.error("❌ MP4Box not found. Please install: sudo pacman -S gpac"),process.exit(1);if((l==="av1"||l==="dual")&&!wD.available)console.error("⚠️ Warning: AV1 encoding requested but no hardware AV1 encoder found."),console.error(" CPU-based AV1 encoding (libsvtav1) will be VERY slow."),console.error(` Consider using --codec h264 for faster encoding. +`);if((d==="hls"||d==="both")&&l==="av1")console.error("❌ Error: HLS format requires H.264 codec for Safari/iOS compatibility."),console.error(` Please use --codec h264 or --codec dual with --format hls +`),process.exit(1);console.log(`\uD83D\uDCCA Analyzing video... +`);var S=await i(DD),tF=oF(DD),eF=(tF.size/1048576).toFixed(2);console.log("\uD83D\uDCF9 Video Information:");console.log(` File: ${DD}`);console.log(` Size: ${eF} MB`);console.log(` Resolution: ${S.width}x${S.height}`);console.log(` FPS: ${S.fps.toFixed(2)}`);console.log(` Duration: ${Math.floor(S.duration/60)}m ${Math.floor(S.duration%60)}s`);console.log(` Codec: ${S.codec}`);if(S.videoBitrate)console.log(` Video Bitrate: ${(S.videoBitrate/1000).toFixed(2)} Mbps`);if(S.audioBitrate)console.log(` Audio Bitrate: ${S.audioBitrate} kbps`);console.log(` +\uD83D\uDCC1 Output: ${gu}`);console.log(`\uD83C\uDFAC Codec: ${l}${l==="dual"?" (AV1 + H.264 for maximum compatibility)":""}`);console.log(`\uD83D\uDCFA Format: ${d}${d==="both"?" (DASH + HLS for maximum compatibility)":d==="hls"?" (H.264 only for Safari/iOS)":""}`);if(ZD)console.log(`\uD83C\uDFAF Custom profiles: ${ZD.join(", ")}`);if(YD)console.log(`\uD83D\uDDBC️ Poster timecode: ${YD}`);var P;if(M!==void 0||w!==void 0||j!==void 0||b!==void 0){if(P={},M!==void 0||w!==void 0){if(P.h264={},M!==void 0)P.h264.cq=M;if(w!==void 0)P.h264.crf=w;console.log(`\uD83C\uDF9A️ H.264 Quality: ${M!==void 0?`CQ ${M}`:""}${w!==void 0?` CRF ${w}`:""}`)}if(j!==void 0||b!==void 0){if(P.av1={},j!==void 0)P.av1.cq=j;if(b!==void 0)P.av1.crf=b;console.log(`\uD83C\uDF9A️ AV1 Quality: ${j!==void 0?`CQ ${j}`:""}${b!==void 0?` CRF ${b}`:""}`)}}console.log(` \uD83D\uDE80 Starting conversion... -`);var r=new GD.default.MultiBar({format:"{stage} | {bar} | {percentage}% | {name}",barCompleteChar:"█",barIncompleteChar:"░",hideCursor:!0,clearOnComplete:!1,stopOnComplete:!0},GD.default.Presets.shades_classic),KD={},LD=null;try{let u=await ED({input:d,outputDir:Vu,customProfiles:i,posterTimecode:o,codec:g,segmentDuration:2,useNvenc:xu,generateThumbnails:!0,generatePoster:!0,parallel:!0,onProgress:(D)=>{let F=D.stage==="encoding"?"Encoding":D.stage==="thumbnails"?"Thumbnails":D.stage==="manifest"?"Manifest":D.stage==="analyzing"?"Analyzing":"Complete";if(D.stage==="encoding"&&D.currentProfile){if(!KD[D.currentProfile])KD[D.currentProfile]=r.create(100,0,{stage:"Encode",name:D.currentProfile});let C=D.profilePercent??D.percent;KD[D.currentProfile].update(C,{stage:"Encode",name:D.currentProfile})}if(!LD)LD=r.create(100,0,{stage:F,name:"Overall"});LD.update(D.percent,{stage:F,name:D.message||"Overall"})}});if(r.stop(),console.log(` +`);var AD=new MD.default.MultiBar({format:"{stage} | {bar} | {percentage}% | {name}",barCompleteChar:"█",barIncompleteChar:"░",hideCursor:!0,clearOnComplete:!1,stopOnComplete:!0},MD.default.Presets.shades_classic),ID={},OD=null;try{let D=await HD({input:DD,outputDir:gu,customProfiles:ZD,posterTimecode:YD,codec:l,format:d,segmentDuration:2,useNvenc:cu,quality:P,generateThumbnails:!0,generatePoster:!0,parallel:!0,onProgress:(u)=>{let F=u.stage==="encoding"?"Encoding":u.stage==="thumbnails"?"Thumbnails":u.stage==="manifest"?"Manifest":u.stage==="analyzing"?"Analyzing":"Complete";if(u.stage==="encoding"&&u.currentProfile){if(!ID[u.currentProfile])ID[u.currentProfile]=AD.create(100,0,{stage:"Encode",name:u.currentProfile});let E=u.profilePercent??u.percent;ID[u.currentProfile].update(E,{stage:"Encode",name:u.currentProfile})}if(!OD)OD=AD.create(100,0,{stage:F,name:"Overall"});OD.update(u.percent,{stage:F,name:u.message||"Overall"})}});if(AD.stop(),console.log(` ✅ Conversion completed successfully! -`),console.log("\uD83D\uDCCA Results:"),console.log(` Manifest: ${u.manifestPath}`),console.log(` Duration: ${u.duration.toFixed(2)}s`),console.log(` Profiles: ${u.profiles.map((D)=>D.name).join(", ")}`),console.log(` Codec: ${u.codecType}${u.codecType==="dual"?" (AV1 + H.264)":""}`),console.log(` Encoder: ${u.usedNvenc?"⚡ GPU accelerated":"\uD83D\uDD27 CPU"}`),u.posterPath)console.log(` Poster: ${u.posterPath}`);if(u.thumbnailSpritePath)console.log(` Thumbnails: ${u.thumbnailSpritePath}`),console.log(` VTT file: ${u.thumbnailVttPath}`);console.log(` -\uD83C\uDF89 Done! You can now use the manifest file in your video player.`)}catch(u){r.stop(),console.error(` +`),console.log("\uD83D\uDCCA Results:"),D.manifestPath)console.log(` DASH Manifest: ${D.manifestPath}`);if(D.hlsManifestPath)console.log(` HLS Manifest: ${D.hlsManifestPath}`);if(console.log(` Duration: ${D.duration.toFixed(2)}s`),console.log(` Profiles: ${D.profiles.map((u)=>u.name).join(", ")}`),console.log(` Format: ${D.format}`),console.log(` Codec: ${D.codecType}${D.codecType==="dual"?" (AV1 + H.264)":""}`),console.log(` Encoder: ${D.usedNvenc?"⚡ GPU accelerated":"\uD83D\uDD27 CPU"}`),D.posterPath)console.log(` Poster: ${D.posterPath}`);if(D.thumbnailSpritePath)console.log(` Thumbnails: ${D.thumbnailSpritePath}`),console.log(` VTT file: ${D.thumbnailVttPath}`);console.log(` +\uD83C\uDF89 Done! You can now use the manifest file(s) in your video player.`)}catch(D){AD.stop(),console.error(` -❌ Error during conversion:`),console.error(u),process.exit(1)} +❌ Error during conversion:`),console.error(D),process.exit(1)} diff --git a/docs/VIDEO_QUALITY_TESTING.md b/docs/VIDEO_QUALITY_TESTING.md new file mode 100644 index 0000000..cfa3eda --- /dev/null +++ b/docs/VIDEO_QUALITY_TESTING.md @@ -0,0 +1,558 @@ +# Тестирование качества видео и сравнение кодеков + +Руководство по анализу качества видео, сравнению кодеков и измерению эффективности сжатия. + +## 📋 Содержание + +1. [Метрики качества](#метрики-качества) +2. [Протестированные кодеки](#протестированные-кодеки) +3. [Результаты тестирования](#результаты-тестирования) +4. [Команды для тестирования](#команды-для-тестирования) +5. [Интерпретация результатов](#интерпретация-результатов) +6. [Рекомендации](#рекомендации) + +--- + +## Метрики качества + +### PSNR (Peak Signal-to-Noise Ratio) + +**Что измеряет:** Отношение сигнал/шум в децибелах (dB). Показывает математическое отличие между оригиналом и сжатым видео. + +**Интерпретация:** +- `> 45 dB` - Отличное качество (практически неразличимо) +- `40-45 dB` - Очень хорошее качество +- `35-40 dB` - Хорошее качество +- `30-35 dB` - Приемлемое качество +- `< 30 dB` - Плохое качество (видимые артефакты) + +**Формула расчета:** +``` +PSNR = 10 × log₁₀(MAX²/MSE) +``` +где MAX - максимальное значение пикселя (255 для 8-bit), MSE - средняя квадратичная ошибка. + +### SSIM (Structural Similarity Index) + +**Что измеряет:** Структурное сходство изображений. Более точно отражает восприятие человеческим глазом. + +**Интерпретация:** +- `1.0` - Идентичные изображения (100%) +- `> 0.99` - Отличное качество (99%+ схожести) +- `0.95-0.99` - Хорошее качество +- `0.90-0.95` - Приемлемое качество +- `< 0.90` - Заметная потеря качества + +**Преимущества SSIM:** +- Учитывает яркость, контраст и структуру +- Лучше коррелирует с субъективной оценкой качества +- Более устойчива к локальным искажениям + +--- + +## Протестированные кодеки + +### 1. H.264 / AVC + +**Описание:** Широко распространенный кодек, поддерживается всеми устройствами. + +**Энкодеры:** +- `libx264` - CPU энкодер (отличное качество/размер) +- `h264_nvenc` - NVIDIA GPU энкодер (быстрее, но менее эффективен) + +**Параметры качества:** +- CRF: 0-51 (меньше = лучше качество) +- Рекомендуемый диапазон: 18-28 +- Пресеты: ultrafast, fast, medium, slow, slower, veryslow + +### 2. VP9 + +**Описание:** Открытый кодек от Google, часть WebM. На 20-50% эффективнее H.264. + +**Энкодеры:** +- `libvpx-vp9` - CPU энкодер +- `vp9_vaapi` - аппаратное ускорение (Intel/AMD) + +**Параметры качества:** +- CRF: 0-63 (меньше = лучше качество) +- Рекомендуемый диапазон: 28-35 +- cpu-used: 0-5 (меньше = лучше качество, медленнее) + +### 3. AV1 + +**Описание:** Современный кодек, следующее поколение после VP9. На 30-50% эффективнее H.264. + +**Энкодеры:** +- `libsvtav1` - CPU энкодер (быстрый, хорошее качество) +- `libaom-av1` - CPU энкодер (лучшее качество, очень медленный) +- `av1_nvenc` - NVIDIA GPU энкодер (быстро, но менее эффективен) +- `av1_amf` - AMD GPU энкодер +- `av1_qsv` - Intel GPU энкодер + +**Параметры качества:** +- CRF (libsvtav1): 0-63 (меньше = лучше качество) +- CQ (av1_nvenc): 0-51 (меньше = лучше качество) +- Рекомендуемый диапазон: 30-40 + +--- + +## Результаты тестирования + +### Тестовое видео + +**Параметры:** +- Файл: `tenexia.mp4` +- Разрешение: 1920×1080 (Full HD) +- FPS: 25 +- Длительность: 135 секунд (2:15) +- Оригинальный размер: 167 MB +- Оригинальный кодек: H.264 (битрейт ~10 Mbps) + +### Сводная таблица результатов + +| Кодек | Энкодер | Параметр | Размер | PSNR | SSIM | Сжатие | Скорость | +|-------|---------|----------|--------|------|------|--------|----------| +| **Оригинал** | - | - | 167 MB | - | - | 1.0x | - | +| **VP9** | libvpx-vp9 | CRF 32 | 13 MB | 47.42 dB | 0.9917 | 12.8x | ~5-10 мин | +| **AV1** | libsvtav1 | CRF 35 | 9.5 MB | 48.01 dB | 0.9921 | 17.6x | ~10-15 мин | +| **AV1** | av1_nvenc | CQ 32 | 20 MB | N/A | N/A | 8.3x | ~10 сек | +| **AV1** | av1_nvenc | CQ 40 | 9.3 MB | 47.13 dB | 0.9914 | 18.0x | ~10 сек | +| **AV1** | av1_nvenc | CQ 45 | 7.1 MB | 45.49 dB | 0.9899 | 23.5x | ~10 сек | +| **H.264** | libx264 | CRF 28 | 9.7 MB | 44.85 dB | 0.9904 | 17.2x | ~3-5 мин | +| **H.264** | h264_nvenc | CQ 28 | 20 MB | 47.88 dB | 0.9922 | 8.4x | ~10 сек | +| **H.264** | h264_nvenc | CQ 32 | 12 MB | N/A | N/A | 14.0x | ~10 сек | +| **H.264** | h264_nvenc | CQ 35 | 7.9 MB | 44.48 dB | 0.9891 | 21.1x | ~10 сек | + +### Победители по категориям + +🥇 **Лучшее качество:** AV1 CPU (CRF 35) - PSNR 48.01 dB, SSIM 0.9921 + +🥇 **Лучшее сжатие при сохранении качества:** AV1 GPU (CQ 40) - 9.3 MB, PSNR 47.13 dB + +🥇 **Максимальное сжатие:** AV1 GPU (CQ 45) - 7.1 MB, PSNR 45.49 dB (всё ещё отличное) + +⚡ **Лучший баланс скорость/качество:** AV1 GPU (CQ 40) - быстро + малый размер + хорошее качество + +--- + +## Команды для тестирования + +### 1. Анализ исходного видео + +#### Получить метаданные с помощью ffprobe + +```bash +# Полная информация в JSON формате +ffprobe -v error -show_format -show_streams -print_format json input.mp4 + +# Краткая информация о видео +ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,r_frame_rate,bit_rate -of default=noprint_wrappers=1 input.mp4 + +# Информация об аудио +ffprobe -v error -select_streams a:0 -show_entries stream=codec_name,sample_rate,bit_rate,channels -of default=noprint_wrappers=1 input.mp4 + +# Размер файла и битрейт +ffprobe -v error -show_entries format=size,duration,bit_rate -of default=noprint_wrappers=1:nokey=1 input.mp4 +``` + +### 2. Конвертация видео + +#### VP9 (CPU) + +```bash +# Базовая конвертация +ffmpeg -i input.mp4 \ + -c:v libvpx-vp9 \ + -crf 32 \ + -b:v 0 \ + -row-mt 1 \ + -cpu-used 2 \ + -c:a libopus \ + -b:a 128k \ + output_vp9.webm + +# Параметры: +# -crf 32 - качество (18-40, меньше = лучше) +# -b:v 0 - режим постоянного качества +# -row-mt 1 - многопоточность +# -cpu-used 2 - скорость кодирования (0-5) +``` + +#### AV1 (CPU) - libsvtav1 + +```bash +# Рекомендуемая конфигурация +ffmpeg -i input.mp4 \ + -c:v libsvtav1 \ + -crf 35 \ + -preset 6 \ + -svtav1-params tune=0 \ + -c:a libopus \ + -b:a 128k \ + output_av1_cpu.mp4 + +# Параметры: +# -crf 35 - качество (0-63, меньше = лучше) +# -preset 6 - скорость (0-13, 6 = средняя) +# -svtav1-params tune=0 - оптимизация под PSNR +``` + +#### AV1 (GPU) - NVIDIA + +```bash +# Оптимальный баланс качество/размер +ffmpeg -i input.mp4 \ + -c:v av1_nvenc \ + -preset p7 \ + -cq 40 \ + -b:v 0 \ + -c:a libopus \ + -b:a 128k \ + output_av1_gpu.mp4 + +# Параметры: +# -preset p7 - качество пресета (p1-p7, p7 = лучшее) +# -cq 40 - constant quality (0-51, меньше = лучше) +# -b:v 0 - без ограничения битрейта + +# Максимальное сжатие (хорошее качество) +ffmpeg -i input.mp4 -c:v av1_nvenc -preset p7 -cq 45 -b:v 0 -c:a libopus -b:a 128k output_av1_small.mp4 +``` + +#### AV1 (GPU) - AMD + +```bash +ffmpeg -i input.mp4 \ + -c:v av1_amf \ + -quality quality \ + -qp_i 40 -qp_p 40 \ + -c:a libopus \ + -b:a 128k \ + output_av1_amd.mp4 +``` + +#### AV1 (GPU) - Intel + +```bash +ffmpeg -i input.mp4 \ + -c:v av1_qsv \ + -preset veryslow \ + -global_quality 40 \ + -c:a libopus \ + -b:a 128k \ + output_av1_intel.mp4 +``` + +#### H.264 (CPU) + +```bash +# Лучшее качество/размер +ffmpeg -i input.mp4 \ + -c:v libx264 \ + -crf 28 \ + -preset slow \ + -c:a aac \ + -b:a 128k \ + output_h264_cpu.mp4 + +# Параметры: +# -crf 28 - качество (18-28, меньше = лучше) +# -preset slow - компромисс скорость/качество +# (ultrafast, fast, medium, slow, slower, veryslow) +``` + +#### H.264 (GPU) - NVIDIA + +```bash +# Баланс качество/размер +ffmpeg -i input.mp4 \ + -c:v h264_nvenc \ + -preset p7 \ + -cq 33 \ + -b:v 0 \ + -c:a aac \ + -b:a 128k \ + output_h264_gpu.mp4 + +# Параметры: +# -preset p7 - качество (p1-p7) +# -cq 33 - constant quality (0-51) +``` + +### 3. Измерение качества + +#### PSNR (Peak Signal-to-Noise Ratio) + +```bash +# Базовый расчет PSNR +ffmpeg -i encoded.mp4 -i original.mp4 \ + -lavfi "[0:v][1:v]psnr" \ + -f null - 2>&1 | grep "PSNR" + +# С сохранением детальной статистики в файл +ffmpeg -i encoded.mp4 -i original.mp4 \ + -lavfi "[0:v][1:v]psnr=stats_file=psnr_stats.log" \ + -f null - + +# Просмотр статистики +head -5 psnr_stats.log && echo "..." && tail -5 psnr_stats.log +``` + +**Формат вывода:** +``` +PSNR y:46.02 u:53.92 v:53.54 average:47.42 min:41.20 max:52.27 +``` + +- `y` - яркость (luminance) +- `u`, `v` - цветовые каналы (chrominance) +- `average` - средний PSNR +- `min`, `max` - минимальный и максимальный PSNR по кадрам + +#### SSIM (Structural Similarity Index) + +```bash +# Базовый расчет SSIM +ffmpeg -i encoded.mp4 -i original.mp4 \ + -lavfi "[0:v][1:v]ssim" \ + -f null - 2>&1 | grep "SSIM" + +# С сохранением детальной статистики +ffmpeg -i encoded.mp4 -i original.mp4 \ + -lavfi "[0:v][1:v]ssim=stats_file=ssim_stats.log" \ + -f null - + +# Просмотр статистики +head -5 ssim_stats.log && echo "..." && tail -5 ssim_stats.log +``` + +**Формат вывода:** +``` +SSIM Y:0.9887 (19.46 dB) U:0.9979 (26.70 dB) V:0.9979 (26.75 dB) All:0.9917 (20.83 dB) +``` + +- Значения 0.0-1.0 (1.0 = идентичные изображения) +- dB - SSIM в децибелах (для удобства сравнения) + +#### VMAF (Video Multimethod Assessment Fusion) + +VMAF - современная метрика от Netflix, лучше всего коррелирует с человеческим восприятием. + +```bash +# Установка модели VMAF (один раз) +# Скачать модель с https://github.com/Netflix/vmaf/tree/master/model + +# Расчет VMAF +ffmpeg -i encoded.mp4 -i original.mp4 \ + -lavfi "[0:v][1:v]libvmaf=model_path=/path/to/vmaf_v0.6.1.json:log_path=vmaf.json" \ + -f null - + +# Интерпретация VMAF: +# 90-100 - Отличное качество +# 75-90 - Хорошее качество +# 50-75 - Приемлемое качество +# < 50 - Плохое качество +``` + +### 4. Полный скрипт для тестирования + +Создайте файл `test_codec.sh`: + +```bash +#!/bin/bash + +# Использование: ./test_codec.sh input.mp4 encoded.mp4 output_dir + +INPUT="$1" +ENCODED="$2" +OUTPUT_DIR="${3:-.}" + +mkdir -p "$OUTPUT_DIR" + +echo "=== Анализ размеров файлов ===" +echo "Оригинал:" +ls -lh "$INPUT" | awk '{print $5, $9}' +echo "Сжатый:" +ls -lh "$ENCODED" | awk '{print $5, $9}' +echo "" + +# Рассчет сжатия +ORIG_SIZE=$(stat -f%z "$INPUT" 2>/dev/null || stat -c%s "$INPUT") +ENC_SIZE=$(stat -f%z "$ENCODED" 2>/dev/null || stat -c%s "$ENCODED") +RATIO=$(echo "scale=2; $ORIG_SIZE / $ENC_SIZE" | bc) +echo "Сжатие: ${RATIO}x" +echo "" + +echo "=== Метаданные закодированного видео ===" +ffprobe -v error -show_format -show_streams -print_format json "$ENCODED" | \ + grep -E "(codec_name|width|height|bit_rate|size)" | head -10 +echo "" + +echo "=== Расчет PSNR ===" +ffmpeg -i "$ENCODED" -i "$INPUT" \ + -lavfi "[0:v][1:v]psnr=stats_file=$OUTPUT_DIR/psnr_stats.log" \ + -f null - 2>&1 | grep "PSNR" +echo "" + +echo "=== Расчет SSIM ===" +ffmpeg -i "$ENCODED" -i "$INPUT" \ + -lavfi "[0:v][1:v]ssim=stats_file=$OUTPUT_DIR/ssim_stats.log" \ + -f null - 2>&1 | grep "SSIM" +echo "" + +echo "Детальная статистика сохранена в $OUTPUT_DIR/" +``` + +**Использование:** + +```bash +chmod +x test_codec.sh +./test_codec.sh original.mp4 encoded.mp4 ./test_results +``` + +--- + +## Интерпретация результатов + +### Матрица принятия решений + +| Сценарий | Рекомендуемый кодек | Параметры | Ожидаемый результат | +|----------|---------------------|-----------|---------------------| +| Максимальное качество | AV1 CPU | CRF 30-35 | Лучшее качество, малый размер | +| Быстрое кодирование | AV1 GPU | CQ 38-42 | Быстро, хорошее качество | +| Совместимость | H.264 CPU | CRF 23-28 | Работает везде | +| Веб-стриминг | VP9 | CRF 30-35 | Хороший баланс | +| Архивирование | AV1 CPU | CRF 25-30 | Лучшее качество | + +### Соответствие метрик и визуального качества + +| PSNR | SSIM | Визуальная оценка | +|------|------|-------------------| +| > 45 dB | > 0.99 | Практически неотличимо от оригинала | +| 40-45 dB | 0.98-0.99 | Отличное качество, артефакты незаметны | +| 35-40 dB | 0.95-0.98 | Хорошее качество, артефакты видны при внимательном просмотре | +| 30-35 dB | 0.90-0.95 | Приемлемое качество, видимые артефакты | +| < 30 dB | < 0.90 | Плохое качество, явные артефакты | + +### Факторы, влияющие на результаты + +1. **Контент видео:** + - Статичные сцены сжимаются лучше + - Быстрое движение требует больше битрейта + - Детализированные текстуры сложнее сжать + - Темные сцены могут показывать бандинг + +2. **Разрешение:** + - Высокие разрешения требуют более высокого битрейта + - При одинаковом CRF, 4K будет весить больше чем 1080p + +3. **Частота кадров:** + - 60 FPS требует ~1.5-2x больше битрейта чем 30 FPS + - Высокий FPS важнее для игрового контента + +4. **Цветовое пространство:** + - HDR (10-bit) требует ~20-30% больше битрейта + - Широкий цветовой охват увеличивает размер + +--- + +## Рекомендации + +### Для продакшена + +**Adaptive Streaming (DASH/HLS):** +```bash +# Используйте несколько профилей качества +# 360p, 480p, 720p, 1080p, 1440p, 2160p + +# Dual codec для максимальной совместимости: +# - AV1 для современных браузеров +# - H.264 для старых устройств (iOS < 14) + +# Пример: dvc-cli уже реализует это +dvc-cli input.mp4 output/ -c dual -f both -r 360,720,1080 +``` + +### Для архивирования + +```bash +# Используйте AV1 CPU с низким CRF +ffmpeg -i input.mp4 \ + -c:v libsvtav1 -crf 25 -preset 4 \ + -c:a libopus -b:a 192k \ + archive.mp4 +``` + +### Для быстрой обработки + +```bash +# Используйте GPU кодирование +ffmpeg -i input.mp4 \ + -c:v av1_nvenc -preset p7 -cq 35 \ + -c:a aac -b:a 128k \ + quick_encode.mp4 +``` + +### Подбор оптимального CRF/CQ + +**Метод бинарного поиска:** + +1. Начните с среднего значения (CRF 28 для H.264, 32 для VP9, 35 для AV1) +2. Закодируйте короткий фрагмент (30-60 сек) +3. Проверьте качество (PSNR > 45, SSIM > 0.99 для отличного качества) +4. Если качество избыточное - увеличьте CRF на 2-3 +5. Если качество недостаточное - уменьшите CRF на 2-3 +6. Повторяйте до достижения баланса + +**Быстрый тест:** + +```bash +# Извлечь 60 секунд с 30-й секунды +ffmpeg -ss 30 -i input.mp4 -t 60 -c copy sample.mp4 + +# Протестировать разные CRF +for crf in 30 32 35 38 40; do + ffmpeg -i sample.mp4 -c:v libsvtav1 -crf $crf -preset 8 -c:a copy test_crf${crf}.mp4 + + # Измерить качество + ffmpeg -i test_crf${crf}.mp4 -i sample.mp4 -lavfi "[0:v][1:v]psnr" -f null - 2>&1 | grep "PSNR" + + # Размер файла + ls -lh test_crf${crf}.mp4 +done +``` + +--- + +## Дополнительные ресурсы + +### Инструменты + +- **FFmpeg** - https://ffmpeg.org/ +- **ffprobe** - анализ медиа файлов +- **MediaInfo** - GUI инструмент для анализа +- **Handbrake** - GUI для кодирования +- **ab-av1** - инструмент для подбора оптимальных параметров AV1 + +### Документация + +- [FFmpeg Encoding Guide](https://trac.ffmpeg.org/wiki/Encode) +- [x264 Settings](https://trac.ffmpeg.org/wiki/Encode/H.264) +- [VP9 Encoding Guide](https://trac.ffmpeg.org/wiki/Encode/VP9) +- [SVT-AV1 Documentation](https://gitlab.com/AOMediaCodec/SVT-AV1) +- [NVIDIA Video Codec SDK](https://developer.nvidia.com/video-codec-sdk) + +### Научные статьи + +- "The Netflix Tech Blog: Per-Title Encode Optimization" +- "VMAF: The Journey Continues" - Netflix +- "AV1 Performance vs x265 and libvpx" - Facebook Engineering + +--- + +## Changelog + +**2025-11-12** - Создан документ на основе реальных тестов +- Протестированы кодеки: VP9, AV1 (CPU/GPU), H.264 (CPU/GPU) +- Добавлены все команды для тестирования +- Добавлены результаты сравнения на видео 1920×1080, 135 сек + diff --git a/src/cli.ts b/src/cli.ts index fed6b99..a71ebfa 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -13,7 +13,7 @@ import { convertToDash, checkFFmpeg, checkNvenc, checkMP4Box, checkAV1Support, getVideoMetadata } from './index'; import cliProgress from 'cli-progress'; import { statSync } from 'node:fs'; -import type { CodecType, StreamingFormat } from './types'; +import type { CodecType, StreamingFormat, QualitySettings } from './types'; // Parse arguments const args = process.argv.slice(2); @@ -23,6 +23,12 @@ let codecType: CodecType = 'dual'; // Default to dual codec let formatType: StreamingFormat = 'both'; // Default to both formats (DASH + HLS) const positionalArgs: string[] = []; +// Quality settings +let h264CQ: number | undefined; +let h264CRF: number | undefined; +let av1CQ: number | undefined; +let av1CRF: number | undefined; + // First pass: extract flags and their values for (let i = 0; i < args.length; i++) { if (args[i] === '-r' || args[i] === '--resolutions') { @@ -64,6 +70,34 @@ for (let i = 0; i < args.length; i++) { process.exit(1); } i++; // Skip next arg + } else if (args[i] === '--h264-cq') { + h264CQ = parseInt(args[i + 1]); + if (isNaN(h264CQ) || h264CQ < 0 || h264CQ > 51) { + console.error(`❌ Invalid H.264 CQ value: ${args[i + 1]}. Must be 0-51`); + process.exit(1); + } + i++; // Skip next arg + } else if (args[i] === '--h264-crf') { + h264CRF = parseInt(args[i + 1]); + if (isNaN(h264CRF) || h264CRF < 0 || h264CRF > 51) { + console.error(`❌ Invalid H.264 CRF value: ${args[i + 1]}. Must be 0-51`); + process.exit(1); + } + i++; // Skip next arg + } else if (args[i] === '--av1-cq') { + av1CQ = parseInt(args[i + 1]); + if (isNaN(av1CQ) || av1CQ < 0 || av1CQ > 51) { + console.error(`❌ Invalid AV1 CQ value: ${args[i + 1]}. Must be 0-51`); + process.exit(1); + } + i++; // Skip next arg + } else if (args[i] === '--av1-crf') { + av1CRF = parseInt(args[i + 1]); + if (isNaN(av1CRF) || av1CRF < 0 || av1CRF > 63) { + console.error(`❌ Invalid AV1 CRF value: ${args[i + 1]}. Must be 0-63`); + process.exit(1); + } + i++; // Skip next arg } else if (!args[i].startsWith('-')) { // Positional argument positionalArgs.push(args[i]); @@ -75,23 +109,28 @@ const input = positionalArgs[0]; const outputDir = positionalArgs[1] || '.'; // Текущая директория по умолчанию if (!input) { - console.error('❌ Usage: dvc-cli [output-dir] [-r resolutions] [-c codec] [-f format] [-p poster-timecode]'); + console.error('❌ Usage: dvc-cli [output-dir] [options]'); console.error('\nOptions:'); console.error(' -r, --resolutions Video resolutions (e.g., 360,480,720 or 720@60,1080@60)'); console.error(' -c, --codec Video codec: av1, h264, or dual (default: dual)'); - console.error(' -f, --format Streaming format: dash, hls, or both (default: dash)'); + console.error(' -f, --format Streaming format: dash, hls, or both (default: both)'); console.error(' -p, --poster Poster timecode (e.g., 00:00:05 or 10)'); + console.error('\nQuality Options (override defaults):'); + console.error(' --h264-cq H.264 GPU CQ value (0-51, lower = better, default: auto)'); + console.error(' --h264-crf H.264 CPU CRF value (0-51, lower = better, default: auto)'); + console.error(' --av1-cq AV1 GPU CQ value (0-51, lower = better, default: auto)'); + console.error(' --av1-crf AV1 CPU CRF value (0-63, lower = better, default: auto)'); console.error('\nExamples:'); console.error(' dvc-cli video.mp4'); console.error(' dvc-cli video.mp4 ./output'); console.error(' dvc-cli video.mp4 -r 360,480,720'); - console.error(' dvc-cli video.mp4 -c av1'); + console.error(' dvc-cli video.mp4 -c av1 --av1-cq 40'); + console.error(' dvc-cli video.mp4 -c dual --h264-cq 30 --av1-cq 39'); console.error(' dvc-cli video.mp4 -f hls'); - console.error(' dvc-cli video.mp4 -f both'); console.error(' dvc-cli video.mp4 -c dual -f both'); console.error(' dvc-cli video.mp4 -r 720@60,1080@60,2160@60 -c av1 -f dash'); console.error(' dvc-cli video.mp4 -p 00:00:05'); - console.error(' dvc-cli video.mp4 ./output -r 720,1080 -c dual -f both -p 10'); + console.error(' dvc-cli video.mp4 ./output -r 720,1080 -c dual -f both -p 10 --h264-cq 28 --av1-cq 37'); process.exit(1); } @@ -163,6 +202,27 @@ if (customProfiles) { if (posterTimecode) { console.log(`🖼️ Poster timecode: ${posterTimecode}`); } + +// Build quality settings if any are specified +let quality: QualitySettings | undefined; +if (h264CQ !== undefined || h264CRF !== undefined || av1CQ !== undefined || av1CRF !== undefined) { + quality = {}; + + if (h264CQ !== undefined || h264CRF !== undefined) { + quality.h264 = {}; + if (h264CQ !== undefined) quality.h264.cq = h264CQ; + if (h264CRF !== undefined) quality.h264.crf = h264CRF; + console.log(`🎚️ H.264 Quality: ${h264CQ !== undefined ? `CQ ${h264CQ}` : ''}${h264CRF !== undefined ? ` CRF ${h264CRF}` : ''}`); + } + + if (av1CQ !== undefined || av1CRF !== undefined) { + quality.av1 = {}; + if (av1CQ !== undefined) quality.av1.cq = av1CQ; + if (av1CRF !== undefined) quality.av1.crf = av1CRF; + console.log(`🎚️ AV1 Quality: ${av1CQ !== undefined ? `CQ ${av1CQ}` : ''}${av1CRF !== undefined ? ` CRF ${av1CRF}` : ''}`); + } +} + console.log('\n🚀 Starting conversion...\n'); // Create multibar container @@ -189,6 +249,7 @@ try { format: formatType, segmentDuration: 2, useNvenc: hasNvenc, + quality, generateThumbnails: true, generatePoster: true, parallel: true, diff --git a/src/core/converter.ts b/src/core/converter.ts index feae6b2..0115685 100644 --- a/src/core/converter.ts +++ b/src/core/converter.ts @@ -16,7 +16,8 @@ import { checkNvenc, checkAV1Support, getVideoMetadata, - ensureDir + ensureDir, + setLogFile } from '../utils'; import { selectProfiles, createProfilesFromStrings } from '../config/profiles'; import { generateThumbnailSprite, generatePoster } from './thumbnails'; @@ -39,6 +40,7 @@ export async function convertToDash( codec = 'dual', format = 'both', useNvenc, + quality, generateThumbnails = true, thumbnailConfig = {}, generatePoster: shouldGeneratePoster = true, @@ -51,6 +53,27 @@ export async function convertToDash( const tempDir = join('/tmp', `dash-converter-${randomUUID()}`); await ensureDir(tempDir); + // Create video output directory and initialize logging + const videoName = basename(input, extname(input)); + const videoOutputDir = join(outputDir, videoName); + await ensureDir(videoOutputDir); + + // Initialize log file + const logFile = join(videoOutputDir, 'conversion.log'); + setLogFile(logFile); + + // Write log header + const { writeFile } = await import('node:fs/promises'); + const header = `=========================================== +DASH Conversion Log +Started: ${new Date().toISOString()} +Input: ${input} +Output: ${videoOutputDir} +Codec: ${codec} +Format: ${format} +===========================================\n`; + await writeFile(logFile, header, 'utf-8'); + try { return await convertToDashInternal( input, @@ -62,6 +85,7 @@ export async function convertToDash( codec, format, useNvenc, + quality, generateThumbnails, thumbnailConfig, shouldGeneratePoster, @@ -70,6 +94,14 @@ export async function convertToDash( onProgress ); } finally { + // Write completion to log + const { appendFile } = await import('node:fs/promises'); + try { + await appendFile(logFile, `\nCompleted: ${new Date().toISOString()}\n`, 'utf-8'); + } catch (err) { + // Ignore log write errors + } + // Cleanup temp directory try { await rm(tempDir, { recursive: true, force: true }); @@ -92,6 +124,7 @@ async function convertToDashInternal( codec: CodecType, format: StreamingFormat, useNvenc: boolean | undefined, + quality: DashConvertOptions['quality'], generateThumbnails: boolean, thumbnailConfig: ThumbnailConfig, generatePosterFlag: boolean, @@ -227,6 +260,9 @@ async function convertToDashInternal( reportProgress('encoding', 25 + codecProgress * 40, `Stage 1: Encoding ${type.toUpperCase()} (${profiles.length} profiles)...`); + // Get quality settings for this codec + const codecQuality = type === 'h264' ? quality?.h264 : quality?.av1; + const tempMP4Paths = await encodeProfilesToMP4( input, tempDir, @@ -239,6 +275,7 @@ async function convertToDashInternal( parallel, maxConcurrent, type, // Pass codec type to differentiate output files + codecQuality, // Pass quality settings (CQ/CRF) undefined, // optimizations - for future use (profileName, percent) => { const profileIndex = profiles.findIndex(p => p.name === profileName); diff --git a/src/core/encoding.ts b/src/core/encoding.ts index 34d975e..44e0ac1 100644 --- a/src/core/encoding.ts +++ b/src/core/encoding.ts @@ -1,6 +1,41 @@ import { join } from 'node:path'; import { execFFmpeg, selectAudioBitrate } from '../utils'; -import type { VideoProfile, VideoOptimizations } from '../types'; +import type { VideoProfile, VideoOptimizations, CodecQualitySettings } from '../types'; + +/** + * Get default CQ/CRF value based on resolution and codec + */ +function getDefaultQuality(height: number, codecType: 'h264' | 'av1', isGPU: boolean): number { + if (isGPU) { + // GPU encoders use CQ - ФИКСИРОВАННЫЕ ЗНАЧЕНИЯ ДЛЯ ТЕСТИРОВАНИЯ + if (codecType === 'h264') { + // H.264 NVENC CQ = 32 (для всех разрешений) + return 32; + } else { + // AV1 NVENC CQ = 42 (для всех разрешений) + return 42; + } + } else { + // CPU encoders use CRF + if (codecType === 'h264') { + // libx264 CRF (на ~3-5 ниже чем NVENC CQ) + if (height <= 360) return 25; + if (height <= 480) return 24; + if (height <= 720) return 23; + if (height <= 1080) return 22; + if (height <= 1440) return 21; + return 20; // 4K + } else { + // libsvtav1 CRF (шкала 0-63, на ~20% выше чем NVENC CQ) + if (height <= 360) return 40; + if (height <= 480) return 38; + if (height <= 720) return 35; + if (height <= 1080) return 32; + if (height <= 1440) return 30; + return 28; // 4K + } + } +} /** * Encode single profile to MP4 @@ -16,6 +51,7 @@ export async function encodeProfileToMP4( segmentDuration: number, sourceAudioBitrate: number | undefined, codecType: 'h264' | 'av1', + qualitySettings?: CodecQualitySettings, optimizations?: VideoOptimizations, onProgress?: (percent: number) => void ): Promise { @@ -27,45 +63,63 @@ export async function encodeProfileToMP4( '-c:v', videoCodec ]; - // Add codec-specific options + // Determine if using GPU or CPU encoder + const isGPU = videoCodec.includes('nvenc') || videoCodec.includes('qsv') || videoCodec.includes('amf'); + + // Determine quality value (CQ for GPU, CRF for CPU) + let qualityValue: number; + if (isGPU && qualitySettings?.cq !== undefined) { + qualityValue = qualitySettings.cq; + } else if (!isGPU && qualitySettings?.crf !== undefined) { + qualityValue = qualitySettings.crf; + } else { + // Use default quality based on resolution + qualityValue = getDefaultQuality(profile.height, codecType, isGPU); + } + + // Add codec-specific options with CQ/CRF if (videoCodec === 'h264_nvenc') { - // NVIDIA H.264 + // NVIDIA H.264 with CQ args.push('-rc:v', 'vbr'); + args.push('-cq', String(qualityValue)); args.push('-preset', preset); args.push('-2pass', '0'); } else if (videoCodec === 'av1_nvenc') { - // NVIDIA AV1 + // NVIDIA AV1 with CQ args.push('-rc:v', 'vbr'); + args.push('-cq', String(qualityValue)); args.push('-preset', preset); args.push('-2pass', '0'); } else if (videoCodec === 'av1_qsv') { // Intel QSV AV1 args.push('-preset', preset); - args.push('-global_quality', '23'); // Quality level for QSV + args.push('-global_quality', String(qualityValue)); } else if (videoCodec === 'av1_amf') { // AMD AMF AV1 args.push('-quality', 'balanced'); - args.push('-rc', 'vbr_latency'); + args.push('-rc', 'cqp'); + args.push('-qp_i', String(qualityValue)); + args.push('-qp_p', String(qualityValue)); } else if (videoCodec === 'libsvtav1') { - // CPU-based SVT-AV1 + // CPU-based SVT-AV1 with CRF + args.push('-crf', String(qualityValue)); args.push('-preset', preset); // 0-13, 8 is medium speed args.push('-svtav1-params', 'tune=0:enable-overlays=1'); + } else if (videoCodec === 'libx264') { + // CPU-based x264 with CRF + args.push('-crf', String(qualityValue)); + args.push('-preset', preset); } else { - // Default (libx264, libx265, etc.) + // Default fallback args.push('-preset', preset); } - // Video encoding parameters - // AV1 is ~40% more efficient than H.264 at same quality (Netflix/YouTube standard) + // Add maxrate as safety limit (optional but recommended for streaming) + // This prevents extreme bitrate spikes on complex scenes const bitrateMultiplier = codecType === 'av1' ? 0.6 : 1.0; - const targetBitrate = Math.round(parseInt(profile.videoBitrate) * bitrateMultiplier); - const bitrateString = `${targetBitrate}k`; - - args.push( - '-b:v', bitrateString, - '-maxrate', bitrateString, - '-bufsize', `${targetBitrate * 2}k` - ); + const maxBitrate = Math.round(parseInt(profile.videoBitrate) * bitrateMultiplier * 1.5); // +50% headroom + args.push('-maxrate', `${maxBitrate}k`); + args.push('-bufsize', `${maxBitrate * 2}k`); // Set GOP size for DASH segments // Keyframes must align with segment boundaries @@ -130,6 +184,7 @@ export async function encodeProfilesToMP4( parallel: boolean, maxConcurrent: number, codecType: 'h264' | 'av1', + qualitySettings?: CodecQualitySettings, optimizations?: VideoOptimizations, onProgress?: (profileName: string, percent: number) => void ): Promise> { @@ -150,6 +205,7 @@ export async function encodeProfilesToMP4( segmentDuration, sourceAudioBitrate, codecType, + qualitySettings, optimizations, (percent) => { if (onProgress) { @@ -178,6 +234,7 @@ export async function encodeProfilesToMP4( segmentDuration, sourceAudioBitrate, codecType, + qualitySettings, optimizations, (percent) => { if (onProgress) { diff --git a/src/core/manifest.ts b/src/core/manifest.ts new file mode 100644 index 0000000..1a2aaec --- /dev/null +++ b/src/core/manifest.ts @@ -0,0 +1,256 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import type { VideoProfile, CodecType } from '../types'; + +/** + * DASH MPD Manifest Generator + * Handles creation and manipulation of MPEG-DASH manifests + */ + +/** + * Validate and fix MPD manifest XML structure + * Ensures all Representation tags are properly closed + */ +export async function validateAndFixManifest(manifestPath: string): Promise { + let mpd = await readFile(manifestPath, 'utf-8'); + + // Fix 1: Remove double slashes in self-closing tags: "//> → "/> + mpd = mpd.replace(/\/\/>/g, '/>'); + + // Fix 2: Fix malformed self-closing tags with extra space: "/ /> → "/> + mpd = mpd.replace(/\/\s+\/>/g, '/>'); + + // Fix 3: Normalize Representation self-closing tags - remove extra spaces before /> + mpd = mpd.replace(/(]+)\s+\/>/g, '$1/>'); + + // Fix 4: Remove orphaned closing tags after self-closing Representation tags + mpd = mpd.replace(/]+)\/>\s*<\/Representation>/g, ''); + + // Fix 5: Convert self-closing Representation tags that have child elements to properly opened tags + mpd = mpd.replace( + /]+)\/>\s*(]*\/>)/g, + '\n $2\n ' + ); + + // Fix 6: Convert unclosed Representation tags to self-closing (if no children) + mpd = mpd.replace( + /]+)>\s*(?=<(?:Representation|\/AdaptationSet))/g, + '\n' + ); + + await writeFile(manifestPath, mpd, 'utf-8'); +} + +/** + * Update MPD manifest paths to reflect subdirectory structure + * Changes: $RepresentationID$_$Number$ → $RepresentationID$/$RepresentationID$_$Number$ + */ +export async function updateManifestPaths( + manifestPath: string, + profiles: VideoProfile[], + codecType: CodecType +): Promise { + let mpd = await readFile(manifestPath, 'utf-8'); + + // MP4Box uses $RepresentationID$ template variable + // Replace: media="$RepresentationID$_$Number$.m4s" + // With: media="$RepresentationID$/$RepresentationID$_$Number$.m4s" + + mpd = mpd.replace( + /media="\$RepresentationID\$_\$Number\$\.m4s"/g, + 'media="$RepresentationID$/$RepresentationID$_$Number$.m4s"' + ); + + // Replace: initialization="$RepresentationID$_.mp4" + // With: initialization="$RepresentationID$/$RepresentationID$_.mp4" + + mpd = mpd.replace( + /initialization="\$RepresentationID\$_\.mp4"/g, + 'initialization="$RepresentationID$/$RepresentationID$_.mp4"' + ); + + await writeFile(manifestPath, mpd, 'utf-8'); +} + +/** + * Separate H.264 and AV1 representations into different AdaptationSets + * This allows DASH players to prefer AV1 when supported, with H.264 fallback + */ +export async function separateCodecAdaptationSets(manifestPath: string): Promise { + let mpd = await readFile(manifestPath, 'utf-8'); + + // Simple string-based approach: look for mixed codec patterns + // Find patterns like: + while (i < lines.length && !lines[i].includes('')) { + const currentLine = lines[i]; + + if (currentLine.includes('')) { + inSegmentTemplate = false; + } + } else if (currentLine.includes(' 0 && av1Reps.length > 0) { + // Split into two AdaptationSets + + // H.264 AdaptationSet + result.push(line); // Opening tag + segmentTemplateLines.forEach(l => result.push(l)); + h264Reps.forEach(l => result.push(l)); + result.push(' '); + + // AV1 AdaptationSet + result.push(line); // Same opening tag + segmentTemplateLines.forEach(l => result.push(l)); + av1Reps.forEach(l => result.push(l)); + result.push(' '); + } else { + // No mixed codecs, keep original + result.push(line); + for (let j = adaptationSetStart + 1; j < i; j++) { + result.push(lines[j]); + } + result.push(lines[i]); // closing tag + } + + i++; + } else { + result.push(line); + i++; + } + } + + await writeFile(manifestPath, result.join('\n'), 'utf-8'); +} + +/** + * Generate MPD manifest from scratch (alternative to MP4Box) + * TODO: Implement full MPD generation without external tools + */ +export async function generateMPDManifest( + profiles: VideoProfile[], + codecType: CodecType, + duration: number, + segmentDuration: number +): Promise { + // TODO: Implement manual MPD generation + // This will be used when we want full control over manifest + throw new Error('Manual MPD generation not yet implemented. Use Bento4 or MP4Box for now.'); +} + +/** + * Update HLS master manifest to reflect subdirectory structure + */ +export async function updateHLSManifestPaths( + manifestPath: string, + profiles: VideoProfile[] +): Promise { + let m3u8 = await readFile(manifestPath, 'utf-8'); + + // MP4Box uses $RepresentationID$ template variable + // Replace: media="$RepresentationID$_$Number$.m4s" + // With: media="$RepresentationID$/$RepresentationID$_$Number$.m4s" + + m3u8 = m3u8.replace( + /media="\$RepresentationID\$_\$Number\$\.m4s"/g, + 'media="$RepresentationID$/$RepresentationID$_$Number$.m4s"' + ); + + // Replace: initialization="$RepresentationID$_.mp4" + // With: initialization="$RepresentationID$/$RepresentationID$_.mp4" + + m3u8 = m3u8.replace( + /initialization="\$RepresentationID\$_\.mp4"/g, + 'initialization="$RepresentationID$/$RepresentationID$_.mp4"' + ); + + await writeFile(manifestPath, m3u8, 'utf-8'); +} + +/** + * Generate HLS media playlist content + */ +export function generateHLSMediaPlaylist( + segmentFiles: string[], + initFile: string, + segmentDuration: number +): string { + let content = '#EXTM3U\n'; + content += `#EXT-X-VERSION:6\n`; + content += `#EXT-X-TARGETDURATION:${Math.ceil(segmentDuration)}\n`; + content += `#EXT-X-MEDIA-SEQUENCE:1\n`; + content += `#EXT-X-INDEPENDENT-SEGMENTS\n`; + content += `#EXT-X-MAP:URI="${initFile}"\n`; + + for (const segmentFile of segmentFiles) { + content += `#EXTINF:${segmentDuration},\n`; + content += `${segmentFile}\n`; + } + + content += `#EXT-X-ENDLIST\n`; + + return content; +} + +/** + * Generate HLS master playlist content + */ +export function generateHLSMasterPlaylist( + variants: Array<{ path: string; bandwidth: number; resolution: string; fps: number }>, + hasAudio: boolean +): string { + let content = '#EXTM3U\n'; + content += '#EXT-X-VERSION:6\n'; + content += '#EXT-X-INDEPENDENT-SEGMENTS\n\n'; + + // Add audio reference + if (hasAudio) { + content += `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="audio",AUTOSELECT=YES,URI="audio/playlist.m3u8",CHANNELS="2"\n\n`; + } + + // Add video variants + for (const variant of variants) { + content += `#EXT-X-STREAM-INF:BANDWIDTH=${variant.bandwidth},CODECS="avc1.4D4020,mp4a.40.2",RESOLUTION=${variant.resolution},FRAME-RATE=${variant.fps}`; + if (hasAudio) { + content += `,AUDIO="audio"`; + } + content += `\n`; + content += `${variant.path}\n\n`; + } + + return content; +} + diff --git a/src/core/packaging.ts b/src/core/packaging.ts index 2487a75..86f3f12 100644 --- a/src/core/packaging.ts +++ b/src/core/packaging.ts @@ -1,7 +1,15 @@ import { join } from 'node:path'; import { execMP4Box } from '../utils'; import type { VideoProfile, CodecType, StreamingFormat } from '../types'; -import { readFile, writeFile, readdir, rename, mkdir } from 'node:fs/promises'; +import { readdir, rename, mkdir, writeFile } from 'node:fs/promises'; +import { + validateAndFixManifest, + updateManifestPaths, + separateCodecAdaptationSets, + updateHLSManifestPaths, + generateHLSMediaPlaylist, + generateHLSMasterPlaylist +} from './manifest'; /** * Package MP4 files into DASH format using MP4Box @@ -52,6 +60,7 @@ export async function packageToDash( } // Execute MP4Box + // Note: We separate codecs into different AdaptationSets manually via separateCodecAdaptationSets() await execMP4Box(args); // MP4Box creates files in the same directory as output MPD @@ -61,6 +70,14 @@ export async function packageToDash( // Update MPD to reflect new file structure with subdirectories await updateManifestPaths(manifestPath, profiles, codecType); + // For dual-codec mode, separate H.264 and AV1 into different AdaptationSets + if (codecType === 'dual') { + await separateCodecAdaptationSets(manifestPath); + } + + // Validate and fix XML structure (ensure all tags are properly closed) + await validateAndFixManifest(manifestPath); + return manifestPath; } @@ -127,38 +144,6 @@ async function organizeSegments( } } -/** - * Update MPD manifest to reflect subdirectory structure - */ -async function updateManifestPaths( - manifestPath: string, - profiles: VideoProfile[], - codecType: CodecType -): Promise { - const { readFile, writeFile } = await import('node:fs/promises'); - - let mpd = await readFile(manifestPath, 'utf-8'); - - // MP4Box uses $RepresentationID$ template variable - // Replace: media="$RepresentationID$_$Number$.m4s" - // With: media="$RepresentationID$/$RepresentationID$_$Number$.m4s" - - mpd = mpd.replace( - /media="\$RepresentationID\$_\$Number\$\.m4s"/g, - 'media="$RepresentationID$/$RepresentationID$_$Number$.m4s"' - ); - - // Replace: initialization="$RepresentationID$_.mp4" - // With: initialization="$RepresentationID$/$RepresentationID$_.mp4" - - mpd = mpd.replace( - /initialization="\$RepresentationID\$_\.mp4"/g, - 'initialization="$RepresentationID$/$RepresentationID$_.mp4"' - ); - - await writeFile(manifestPath, mpd, 'utf-8'); -} - /** * Package MP4 files into HLS format using MP4Box * Stage 2: Light work - just packaging, no encoding @@ -220,7 +205,7 @@ export async function packageToHLS( await organizeSegmentsHLS(outputDir, profiles); // Update manifest to reflect new file structure with subdirectories - await updateManifestPathsHLS(manifestPath, profiles); + await updateHLSManifestPaths(manifestPath, profiles); return manifestPath; } @@ -277,35 +262,6 @@ async function organizeSegmentsHLS( } } -/** - * Update HLS master manifest to reflect subdirectory structure - */ -async function updateManifestPathsHLS( - manifestPath: string, - profiles: VideoProfile[] -): Promise { - let m3u8 = await readFile(manifestPath, 'utf-8'); - - // MP4Box uses $RepresentationID$ template variable - // Replace: media="$RepresentationID$_$Number$.m4s" - // With: media="$RepresentationID$/$RepresentationID$_$Number$.m4s" - - m3u8 = m3u8.replace( - /media="\$RepresentationID\$_\$Number\$\.m4s"/g, - 'media="$RepresentationID$/$RepresentationID$_$Number$.m4s"' - ); - - // Replace: initialization="$RepresentationID$_.mp4" - // With: initialization="$RepresentationID$/$RepresentationID$_.mp4" - - m3u8 = m3u8.replace( - /initialization="\$RepresentationID\$_\.mp4"/g, - 'initialization="$RepresentationID$/$RepresentationID$_.mp4"' - ); - - await writeFile(manifestPath, m3u8, 'utf-8'); -} - /** * Unified packaging: creates segments once and generates both DASH and HLS manifests * No duplication - segments stored in {profile}-{codec}/ folders @@ -319,58 +275,17 @@ export async function packageToFormats( format: StreamingFormat ): Promise<{ manifestPath?: string; hlsManifestPath?: string }> { - // Step 1: Generate segments using MP4Box (DASH mode) - const tempManifestPath = join(outputDir, '.temp_manifest.mpd'); - - const args = [ - '-dash', String(segmentDuration * 1000), - '-frag', String(segmentDuration * 1000), - '-rap', - '-segment-timeline', - '-segment-name', '$RepresentationID$_$Number$', - '-out', tempManifestPath - ]; - - // Add all MP4 files - let firstFile = true; - - for (const [codecType, mp4Files] of codecMP4Files.entries()) { - for (const profile of profiles) { - const mp4Path = mp4Files.get(profile.name); - if (!mp4Path) { - throw new Error(`MP4 file not found for profile: ${profile.name}, codec: ${codecType}`); - } - - const representationId = codec === 'dual' ? `${profile.name}-${codecType}` : profile.name; - - args.push(`${mp4Path}#video:id=${representationId}`); - - if (firstFile) { - args.push(`${mp4Path}#audio:id=audio`); - firstFile = false; - } - } - } - - // Execute MP4Box to create segments - await execMP4Box(args); - - // Step 2: Organize segments into {profile}-{codec}/ folders - await organizeSegmentsUnified(outputDir, profiles, codec); - - // Step 3: Generate manifests based on format let manifestPath: string | undefined; let hlsManifestPath: string | undefined; + // Step 1: Generate DASH segments and manifest using MP4Box if (format === 'dash' || format === 'both') { - // Move and update DASH manifest - manifestPath = join(outputDir, 'manifest.mpd'); - await rename(tempManifestPath, manifestPath); - await updateDashManifestPaths(manifestPath, profiles, codec); + manifestPath = await packageToDash(codecMP4Files, outputDir, profiles, segmentDuration, codec); } + // Step 2: Generate HLS playlists from existing segments if (format === 'hls' || format === 'both') { - // Generate HLS playlists + // HLS generation from segments hlsManifestPath = await generateHLSPlaylists( outputDir, profiles, @@ -379,101 +294,9 @@ export async function packageToFormats( ); } - // Clean up temp manifest if not used - try { - const { unlink } = await import('node:fs/promises'); - await unlink(tempManifestPath); - } catch { - // Already moved or doesn't exist - } - return { manifestPath, hlsManifestPath }; } -/** - * Organize segments into unified structure: {profile}-{codec}/ - */ -async function organizeSegmentsUnified( - outputDir: string, - profiles: VideoProfile[], - codecType: CodecType -): Promise { - const representationIds: string[] = []; - - // Determine which codecs are used - const codecs: Array<'h264' | 'av1'> = []; - if (codecType === 'h264' || codecType === 'dual') codecs.push('h264'); - if (codecType === 'av1' || codecType === 'dual') codecs.push('av1'); - - // Create directories for each profile-codec combination - for (const codecName of codecs) { - for (const profile of profiles) { - const repId = codecType === 'dual' ? `${profile.name}-${codecName}` : profile.name; - representationIds.push(repId); - - const profileDir = join(outputDir, repId); - await mkdir(profileDir, { recursive: true }); - } - } - - // Create audio directory - const audioDir = join(outputDir, 'audio'); - await mkdir(audioDir, { recursive: true }); - - // Get all files in output directory - const files = await readdir(outputDir); - - // Move segment files to their respective directories - for (const file of files) { - // Skip manifests and directories - if (file.endsWith('.mpd') || file.endsWith('.m3u8') || !file.includes('_')) { - continue; - } - - // Move audio files - if (file.startsWith('audio_')) { - const oldPath = join(outputDir, file); - const newPath = join(audioDir, file); - await rename(oldPath, newPath); - continue; - } - - // Move video segment files - for (const repId of representationIds) { - if (file.startsWith(`${repId}_`)) { - const oldPath = join(outputDir, file); - const newPath = join(outputDir, repId, file); - await rename(oldPath, newPath); - break; - } - } - } -} - -/** - * Update DASH manifest paths to point to {profile}-{codec}/ folders - */ -async function updateDashManifestPaths( - manifestPath: string, - profiles: VideoProfile[], - codecType: CodecType -): Promise { - let mpd = await readFile(manifestPath, 'utf-8'); - - // Update paths: $RepresentationID$_$Number$.m4s → $RepresentationID$/$RepresentationID$_$Number$.m4s - mpd = mpd.replace( - /media="\$RepresentationID\$_\$Number\$\.m4s"/g, - 'media="$RepresentationID$/$RepresentationID$_$Number$.m4s"' - ); - - mpd = mpd.replace( - /initialization="\$RepresentationID\$_\.mp4"/g, - 'initialization="$RepresentationID$/$RepresentationID$_.mp4"' - ); - - await writeFile(manifestPath, mpd, 'utf-8'); -} - /** * Generate HLS playlists (media playlists in folders + master in root) */ @@ -507,20 +330,8 @@ async function generateHLSPlaylists( continue; // Skip if no segments found } - // Generate media playlist content - let playlistContent = '#EXTM3U\n'; - playlistContent += `#EXT-X-VERSION:6\n`; - playlistContent += `#EXT-X-TARGETDURATION:${Math.ceil(segmentDuration)}\n`; - playlistContent += `#EXT-X-MEDIA-SEQUENCE:1\n`; - playlistContent += `#EXT-X-INDEPENDENT-SEGMENTS\n`; - playlistContent += `#EXT-X-MAP:URI="${initFile}"\n`; - - for (const segmentFile of segmentFiles) { - playlistContent += `#EXTINF:${segmentDuration},\n`; - playlistContent += `${segmentFile}\n`; - } - - playlistContent += `#EXT-X-ENDLIST\n`; + // Generate media playlist content using manifest module + const playlistContent = generateHLSMediaPlaylist(segmentFiles, initFile, segmentDuration); // Write media playlist const playlistPath = join(profilePath, 'playlist.m3u8'); @@ -550,37 +361,12 @@ async function generateHLSPlaylists( const audioInit = audioFiles.find(f => f.endsWith('_.mp4')); if (audioInit && audioSegments.length > 0) { - let audioPlaylistContent = '#EXTM3U\n'; - audioPlaylistContent += `#EXT-X-VERSION:6\n`; - audioPlaylistContent += `#EXT-X-TARGETDURATION:${Math.ceil(segmentDuration)}\n`; - audioPlaylistContent += `#EXT-X-MEDIA-SEQUENCE:1\n`; - audioPlaylistContent += `#EXT-X-INDEPENDENT-SEGMENTS\n`; - audioPlaylistContent += `#EXT-X-MAP:URI="${audioInit}"\n`; - - for (const segmentFile of audioSegments) { - audioPlaylistContent += `#EXTINF:${segmentDuration},\n`; - audioPlaylistContent += `${segmentFile}\n`; - } - - audioPlaylistContent += `#EXT-X-ENDLIST\n`; - + const audioPlaylistContent = generateHLSMediaPlaylist(audioSegments, audioInit, segmentDuration); await writeFile(join(audioDir, 'playlist.m3u8'), audioPlaylistContent, 'utf-8'); } - // Generate master playlist - let masterContent = '#EXTM3U\n'; - masterContent += '#EXT-X-VERSION:6\n'; - masterContent += '#EXT-X-INDEPENDENT-SEGMENTS\n\n'; - - // Add audio reference - masterContent += `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="audio",AUTOSELECT=YES,URI="audio/playlist.m3u8",CHANNELS="2"\n\n`; - - // Add video variants - for (const variant of variants) { - masterContent += `#EXT-X-STREAM-INF:BANDWIDTH=${variant.bandwidth},CODECS="avc1.4D4020,mp4a.40.2",RESOLUTION=${variant.resolution},FRAME-RATE=${variant.fps},AUDIO="audio"\n`; - masterContent += `${variant.path}\n\n`; - } - + // Generate master playlist using manifest module + const masterContent = generateHLSMasterPlaylist(variants, audioInit !== undefined && audioSegments.length > 0); await writeFile(masterPlaylistPath, masterContent, 'utf-8'); return masterPlaylistPath; diff --git a/src/types/index.ts b/src/types/index.ts index 7d850ce..273872d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -8,6 +8,28 @@ export type CodecType = 'av1' | 'h264' | 'dual'; */ export type StreamingFormat = 'dash' | 'hls' | 'both'; +/** + * Quality settings for a codec + */ +export interface CodecQualitySettings { + /** CQ (Constant Quality) for GPU encoders (0-51, lower = better quality) */ + cq?: number; + + /** CRF (Constant Rate Factor) for CPU encoders (0-51 for h264, 0-63 for av1, lower = better quality) */ + crf?: number; +} + +/** + * Quality settings for video encoding + */ +export interface QualitySettings { + /** Quality settings for H.264 codec */ + h264?: CodecQualitySettings; + + /** Quality settings for AV1 codec */ + av1?: CodecQualitySettings; +} + /** * Configuration options for DASH conversion */ @@ -36,6 +58,9 @@ export interface DashConvertOptions { /** Enable NVENC hardware acceleration (auto-detect if undefined) */ useNvenc?: boolean; + /** Quality settings for video encoding (CQ/CRF values) */ + quality?: QualitySettings; + /** Generate thumbnail sprite (default: true) */ generateThumbnails?: boolean; diff --git a/src/utils/index.ts b/src/utils/index.ts index 1082958..042a69e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,7 +5,8 @@ export { checkNvenc, checkAV1Support, execFFmpeg, - execMP4Box + execMP4Box, + setLogFile } from './system'; // Video utilities diff --git a/src/utils/system.ts b/src/utils/system.ts index c1118b7..29ccee4 100644 --- a/src/utils/system.ts +++ b/src/utils/system.ts @@ -1,4 +1,28 @@ import { spawn } from 'node:child_process'; +import { appendFile } from 'node:fs/promises'; + +// Global variable for log file path +let currentLogFile: string | null = null; + +/** + * Set log file path for FFmpeg and MP4Box output + */ +export function setLogFile(logPath: string): void { + currentLogFile = logPath; +} + +/** + * Append log entry to file + */ +async function appendLog(entry: string): Promise { + if (currentLogFile) { + try { + await appendFile(currentLogFile, entry, 'utf-8'); + } catch (err) { + // Silently ignore log errors to not break conversion + } + } +} /** * Check if FFmpeg is available @@ -22,6 +46,17 @@ export async function checkMP4Box(): Promise { }); } +/** + * Check if Bento4 mp4dash is available + */ +export async function checkBento4(): Promise { + return new Promise((resolve) => { + const proc = spawn('mp4dash', ['--version']); + proc.on('error', () => resolve(false)); + proc.on('close', (code) => resolve(code === 0)); + }); +} + /** * Check if NVENC is available */ @@ -89,6 +124,10 @@ export async function execFFmpeg( onProgress?: (percent: number) => void, duration?: number ): Promise { + const timestamp = new Date().toISOString(); + const commandLog = `\n=== FFmpeg Command [${timestamp}] ===\nffmpeg ${args.join(' ')}\n`; + await appendLog(commandLog); + return new Promise((resolve, reject) => { const proc = spawn('ffmpeg', args); @@ -113,13 +152,20 @@ export async function execFFmpeg( }); proc.on('error', (err) => { + appendLog(`ERROR: ${err.message}\n`); reject(new Error(`FFmpeg error: ${err.message}`)); }); proc.on('close', (code) => { if (code === 0) { + // Log last 10 lines of output for successful runs + const lines = stderrData.split('\n').filter(l => l.trim()); + const lastLines = lines.slice(-10).join('\n'); + appendLog(`SUCCESS: Exit code ${code}\n--- Last 10 lines of output ---\n${lastLines}\n`); resolve(); } else { + // Log full output on failure + appendLog(`FAILED: Exit code ${code}\n--- Full error output ---\n${stderrData}\n`); reject(new Error(`FFmpeg failed with exit code ${code}\n${stderrData}`)); } }); @@ -130,6 +176,10 @@ export async function execFFmpeg( * Execute MP4Box command */ export async function execMP4Box(args: string[]): Promise { + const timestamp = new Date().toISOString(); + const commandLog = `\n=== MP4Box Command [${timestamp}] ===\nMP4Box ${args.join(' ')}\n`; + await appendLog(commandLog); + return new Promise((resolve, reject) => { const proc = spawn('MP4Box', args); @@ -145,14 +195,22 @@ export async function execMP4Box(args: string[]): Promise { }); proc.on('error', (err) => { + appendLog(`ERROR: ${err.message}\n`); reject(new Error(`MP4Box error: ${err.message}`)); }); proc.on('close', (code) => { if (code === 0) { + // Log output summary for successful runs + const output = stdoutData || stderrData; + const lines = output.split('\n').filter(l => l.trim()); + const lastLines = lines.slice(-10).join('\n'); + appendLog(`SUCCESS: Exit code ${code}\n--- Last 10 lines of output ---\n${lastLines}\n`); resolve(); } else { + // Log full output on failure const output = stderrData || stdoutData; + appendLog(`FAILED: Exit code ${code}\n--- Full error output ---\n${output}\n`); reject(new Error(`MP4Box failed with exit code ${code}\n${output}`)); } });