convert.agi 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. /*
  2. FFmpeg Factory - Convert
  3. Runs an ffmpeg conversion and writes progress to a JSON file.
  4. The same endpoint also cancels a running conversion (action=cancel), so the
  5. frontend only needs the task ID it already knows to stop a job.
  6. Expected POST parameters:
  7. action - optional; "cancel" stops the running conversion of taskId.
  8. Any other value (or omitted) starts a new conversion.
  9. src - virtual path of the input file
  10. outputExt - desired output file extension (e.g. "mp3", "mp4")
  11. convType - "audio" | "video" | "image" | "generic"
  12. taskId - unique task ID generated by the frontend
  13. options - JSON string with format-specific options:
  14. audio: { sample_rate: 44100 }
  15. video: { resolution: "720p", compression: 23 }
  16. image: { scale: 0.5, compression: 80 }
  17. generic: {}
  18. */
  19. requirelib("filelib");
  20. requirelib("ffmpeg");
  21. // Ensure task folder exists
  22. var taskDir = "tmp:/ffmpeg_factory";
  23. if (!filelib.fileExists(taskDir)) {
  24. filelib.mkdir(taskDir);
  25. }
  26. function taskFileOf(id) { return taskDir + "/" + id + ".task.json"; }
  27. function progressFileOf(id) { return taskDir + "/" + id + ".progress.json"; }
  28. // Read a task file back into an object, or null when it is missing / unreadable
  29. function readTaskObject(id) {
  30. var taskFile = taskFileOf(id);
  31. if (!filelib.fileExists(taskFile)) {
  32. return null;
  33. }
  34. var content = filelib.readFile(taskFile);
  35. if (content === false || content === "") {
  36. return null;
  37. }
  38. try {
  39. return JSON.parse(content);
  40. } catch (e) {
  41. return null;
  42. }
  43. }
  44. var isCancelRequest = (typeof action !== "undefined" && action === "cancel");
  45. var haveTaskId = (typeof taskId !== "undefined" && taskId !== "" && taskId !== "undefined");
  46. if (isCancelRequest) {
  47. // ── Cancel an already running conversion ──
  48. if (!haveTaskId) {
  49. sendJSONResp(JSON.stringify({ error: "Missing taskId" }));
  50. } else {
  51. // Stop the ffmpeg process; false means it already finished on its own
  52. var stopped = ffmpeg.cancel(progressFileOf(taskId));
  53. // Mark the task as cancelled. The still-running convert request re-reads
  54. // this file before writing its own result, so the cancelled state wins.
  55. var pendingTask = readTaskObject(taskId);
  56. if (pendingTask !== null) {
  57. pendingTask.status = "cancelled";
  58. pendingTask.error = "Cancelled by user";
  59. filelib.writeFile(taskFileOf(taskId), JSON.stringify(pendingTask));
  60. }
  61. sendJSONResp(JSON.stringify({
  62. success: true,
  63. taskId: taskId,
  64. stopped: stopped
  65. }));
  66. }
  67. } else if (typeof src === "undefined" || typeof outputExt === "undefined" ||
  68. typeof convType === "undefined" || !haveTaskId) {
  69. // ── Validate required parameters ──
  70. sendJSONResp(JSON.stringify({ error: "Missing required parameters" }));
  71. } else {
  72. // ── Start a new conversion ──
  73. // Parse options
  74. var opts = {};
  75. if (typeof options !== "undefined" && options !== "" && options !== "undefined") {
  76. try { opts = JSON.parse(options); } catch (e) {}
  77. }
  78. // Determine output virtual path (same directory as the source, new extension)
  79. var lastSlash = src.lastIndexOf("/");
  80. var inputDir = src.substring(0, lastSlash);
  81. var inputFile = src.substring(lastSlash + 1);
  82. var lastDot = inputFile.lastIndexOf(".");
  83. var baseName = (lastDot > 0) ? inputFile.substring(0, lastDot) : inputFile;
  84. var outputVpath = inputDir + "/" + baseName + "." + outputExt;
  85. // Progress file virtual path (written by the Go ffmpeg functions every ~500 ms).
  86. // It doubles as the cancellation key of this conversion.
  87. var progressVpath = progressFileOf(taskId);
  88. // Write initial task file so the frontend can resume the session if the tab is closed
  89. var taskObj = {
  90. id: taskId,
  91. input_vpath: src,
  92. output_vpath: outputVpath,
  93. conv_type: convType,
  94. options: opts,
  95. status: "running",
  96. error: "",
  97. created_at: Date.now()
  98. };
  99. filelib.writeFile(taskFileOf(taskId), JSON.stringify(taskObj));
  100. // --- Run the conversion ---
  101. var success = false;
  102. var errMsg = "";
  103. try {
  104. if (convType === "audio") {
  105. var sr = (opts.sample_rate && parseInt(opts.sample_rate) > 0) ? parseInt(opts.sample_rate) : 0;
  106. success = ffmpeg.audioConvert(src, outputVpath, sr, progressVpath);
  107. } else if (convType === "video") {
  108. var res = (opts.resolution && opts.resolution !== "undefined") ? opts.resolution : "";
  109. var crf = (opts.compression) ? parseInt(opts.compression) : 0;
  110. success = ffmpeg.videoConvert(src, outputVpath, res, crf, progressVpath);
  111. } else if (convType === "image") {
  112. var scale = (opts.scale) ? parseFloat(opts.scale) : 1.0;
  113. var qual = (opts.compression) ? parseInt(opts.compression) : 0;
  114. success = ffmpeg.imageConvert(src, outputVpath, scale, qual, progressVpath);
  115. } else {
  116. // generic / cross-media (e.g. mp4 → gif)
  117. success = ffmpeg.convertWithProgress(src, outputVpath, progressVpath);
  118. }
  119. } catch (e) {
  120. errMsg = e.toString();
  121. success = false;
  122. }
  123. // A cancel request may have come in while ffmpeg was running. The killed
  124. // process looks like a normal failure here, so check the task file first.
  125. var finalStatus = success ? "completed" : "failed";
  126. var latestTask = readTaskObject(taskId);
  127. var wasCancelled = (latestTask !== null && latestTask.status === "cancelled");
  128. if (wasCancelled) {
  129. finalStatus = "cancelled";
  130. errMsg = "Cancelled by user";
  131. }
  132. if (filelib.fileExists(taskFileOf(taskId))) {
  133. // Update task file with final status
  134. taskObj.status = finalStatus;
  135. taskObj.error = errMsg;
  136. filelib.writeFile(taskFileOf(taskId), JSON.stringify(taskObj));
  137. } else {
  138. // The task was dismissed while ffmpeg was still running: do not recreate
  139. // its files, and drop the progress file the conversion just rewrote.
  140. if (filelib.fileExists(progressVpath)) {
  141. filelib.deleteFile(progressVpath);
  142. }
  143. }
  144. sendJSONResp(JSON.stringify({
  145. success: success && !wasCancelled,
  146. cancelled: wasCancelled,
  147. taskId: taskId,
  148. output: outputVpath,
  149. error: errMsg
  150. }));
  151. }