From 7b693d59dfce2ece45a368ba6c4693cfcd9ffa88 Mon Sep 17 00:00:00 2001 From: "CHIN\\a5566" Date: Tue, 5 Mar 2024 16:26:23 +0800 Subject: [PATCH] feat: implement convert study to video #14 gitlab --- .../controller/convert-image/convert-image.js | 6 +- .../service/convert-image.service.js | 22 +- .../convert-image/service/video-factory.js | 268 ++++++++++++++++++ package-lock.json | 191 ++++++++++++- package.json | 2 + 5 files changed, 477 insertions(+), 12 deletions(-) create mode 100644 api/dicom-web/controller/convert-image/service/video-factory.js diff --git a/api/dicom-web/controller/convert-image/convert-image.js b/api/dicom-web/controller/convert-image/convert-image.js index 5c3a8b2a..c61486f8 100644 --- a/api/dicom-web/controller/convert-image/convert-image.js +++ b/api/dicom-web/controller/convert-image/convert-image.js @@ -1,4 +1,6 @@ const { Controller } = require("@root/api/controller.class"); +const { ConvertImageService } = require("./service/convert-image.service"); +const fs = require("fs"); class ConvertImageController extends Controller { constructor(req, res) { @@ -6,7 +8,9 @@ class ConvertImageController extends Controller { } async mainProcess() { - return this.response.send({message: "hello world"}); + let convertImageService = new ConvertImageService(this.request, this.response); + let videoFile = await convertImageService.convert(); + return fs.createReadStream(videoFile).pipe(this.response); } } diff --git a/api/dicom-web/controller/convert-image/service/convert-image.service.js b/api/dicom-web/controller/convert-image/service/convert-image.service.js index a638f909..aef778bd 100644 --- a/api/dicom-web/controller/convert-image/service/convert-image.service.js +++ b/api/dicom-web/controller/convert-image/service/convert-image.service.js @@ -1,6 +1,26 @@ +const { StudyVideoFactory } = require("./video-factory"); + class ConvertImageService { constructor(req, res) { + /** @type { import("express").Request } */ this.request = req; + /** @type { import("express").Response } */ this.response = res; } -} \ No newline at end of file + + async convert() { + const videoMimeType = [ + "video/mp4", + "video/x-msvideo", + "video/mpeg", + "video/H265" + ]; + + if (videoMimeType.includes(this.request.headers.accept)) { + let videoFactory = new StudyVideoFactory(this.request.headers.accept, this.request.params); + return await videoFactory.convert(); + } + } +} + +module.exports.ConvertImageService = ConvertImageService; \ No newline at end of file diff --git a/api/dicom-web/controller/convert-image/service/video-factory.js b/api/dicom-web/controller/convert-image/service/video-factory.js new file mode 100644 index 00000000..f9b6625a --- /dev/null +++ b/api/dicom-web/controller/convert-image/service/video-factory.js @@ -0,0 +1,268 @@ +const { DicomWebServiceError } = require("@error/dicom-web-service"); +const { StudyImagePathFactory } = require("../../WADO-RS/service/WADO-RS.service"); +const { Dcm2JpgExecutor$Dcm2JpgOptions } = require("@java-wrapper/org/github/chinlinlee/dcm2jpg/Dcm2JpgExecutor$Dcm2JpgOptions"); +const { InstanceModel } = require("@models/sql/models/instance.model"); +const { Dcm2JpgExecutor } = require("@java-wrapper/org/github/chinlinlee/dcm2jpg/Dcm2JpgExecutor"); +const path = require("path"); +const mkdirp = require("mkdirp"); +const fsP = require("fs/promises"); +const { v4: uuidV4 } = require("uuid"); +const os = require("os"); +const ffmpegInstance = require("ffmpeg-static"); +const ffmpeg = require("fluent-ffmpeg"); +const { EventEmitter } = require("events"); +ffmpeg.setFfmpegPath(ffmpegInstance); + +class VideoFactory { + constructor(mimeType, uids) { + this.mimeType = mimeType; + this.uids = uids; + this.imagePathFactory = undefined; + this.imageTempFolder = path.join(__dirname, "../../../../../tempUploadFiles", this.uids.studyUID, uuidV4()); + } + + /** + * @param {string} folder the folder location that contains frames jpeg images + * @param {string[]} jpegs the jpeg images' file location + */ + async convert(folder, jpegs) { + let filename = await this.getFfmpegInputData(jpegs); + if (this.mimeType === "video/mp4") { + let converter = new Mp4Converter(); + return await converter.convert(folder, filename); + } else if (this.mimeType === "video/x-msvideo") { + let converter = new AviConverter(); + return await converter.convert(folder, filename); + } else if (this.mimeType === "video/mpeg") { + let converter = new MpegConverter(); + return await converter.convert(folder, filename); + } else if (this.mimeType === "video/H265") { + let converter = new H265Converter(); + return await converter.convert(folder, filename); + } + } + + async getFfmpegInputData(images) { + let ffmpegInputData = images.map(jpeg => `file 'file:${jpeg}'`).join("\n"); + let filename = path.join(__dirname, "../../../../../tempUploadFiles", `${uuidV4()}.txt`); + await fsP.writeFile(filename, ffmpegInputData); + return filename; + } + + async getImagePaths() { + await this.imagePathFactory.getImagePaths(); + let checkAllImageExistResult = await this.imagePathFactory.checkAllImageExist(); + if (!checkAllImageExistResult.status) { + throw new DicomWebServiceError("404", checkAllImageExistResult.message, 400); + } + + return this.imagePathFactory.imagePaths; + } + + async getInstancesFrames() { + let imagePaths = await this.getImagePaths(); + let instancesFrames = await Promise.all(imagePaths.map(async imagePath => await InstanceModel.getInstanceFrame(imagePath))); + instancesFrames.sort((a, b) => a["x00200013"] - b["x00200013"]); + return instancesFrames; + } + + /** + * TODO: 也許需要重構成 wado-rs/uri 通用的產圖 class + * @param {import("@root/utils/typeDef/dicomImage").InstanceFrameObj} frame + * @param {string} folder + * @returns + */ + async generateJpegImage(frame, folder) { + let jsDcm2Jpegs = []; + + let imageFrameNumber = frame["00280008"]?.["Value"]?.[0] || 1; + mkdirp.sync(folder, "0755"); + imageFrameNumber = parseInt(imageFrameNumber); + let jpegBaseFilename = path.resolve( + path.join(folder, frame["instanceUID"]) + ); + let jpegs = []; + for (let i = 1; i <= imageFrameNumber; i++) { + + let jpegFilename = `${jpegBaseFilename}.${i - 1}.jpg`; + jpegs.push(jpegFilename); + + let dcm2jpgOpt = new Dcm2JpgExecutor$Dcm2JpgOptions(); + let windowCenter = frame["00281050"]?.["Value"]?.[0]; + let windowWidth = frame["00281051"]?.["Value"]?.[0]; + + if (windowCenter && windowWidth) { + dcm2jpgOpt.windowCenter = windowCenter; + dcm2jpgOpt.windowWidth = windowWidth; + } + + dcm2jpgOpt.frameNumber = i; + + jsDcm2Jpegs.push({ + jsDcm2JpegOption: dcm2jpgOpt, + jpegFilename: jpegFilename + }); + + + if (i % os.cpus().length === 0) { + await Promise.allSettled( + jsDcm2Jpegs.map(async (j) => + Dcm2JpgExecutor.convertDcmToJpgFromFilename(frame["instancePath"], j.jpegFilename, j.jsDcm2JpegOption) + ) + ); + jsDcm2Jpegs = []; + } + } + + await Promise.allSettled( + jsDcm2Jpegs.map(async (j) => + Dcm2JpgExecutor.convertDcmToJpgFromFilename(frame["instancePath"], j.jpegFilename, j.jsDcm2JpegOption) + ) + ); + + return jpegs; + } +} + +class FfmpegVideoConverter { + constructor() { + this.removeTempFolderEvent = new EventEmitter(); + this.removeTempFolderEvent.on("start", (folder) => { + fsP.rm(folder, { recursive: true }); + }); + } + + async convert() { + throw new Error("Method not implemented."); + } +} + +class Mp4Converter extends FfmpegVideoConverter { + constructor() { + super(); + } + async convert(folder, ffmpegInputData) { + return new Promise((resolve, reject) => { + ffmpeg(ffmpegInputData) + .inputOption("-f concat") + .inputOption("-safe 0") + .inputFPS(60) + .videoCodec("libx264") + .outputOption("-crf 25") + .save(`${folder}.mp4`) + .on("error", (err) => { + fsP.unlink(ffmpegInputData); + this.removeTempFolderEvent.emit("start", folder); + return reject(err.message); + }) + .on("end", () => { + fsP.unlink(ffmpegInputData); + this.removeTempFolderEvent.emit("start", folder); + return resolve(`${folder}.mp4`); + }); + }); + } +} + +class AviConverter extends FfmpegVideoConverter { + constructor() { + super(); + } + + async convert(folder, ffmpegInputData) { + return new Promise((resolve, reject) => { + ffmpeg(ffmpegInputData) + .inputOption("-f concat") + .inputOption("-safe 0") + .inputFPS(60) + .outputOption("-crf 25") + .save(`${folder}.avi`) + .on("error", (err) => { + fsP.unlink(ffmpegInputData); + this.removeTempFolderEvent.emit("start", folder); + return reject(err.message); + }) + .on("end", () => { + fsP.unlink(ffmpegInputData); + this.removeTempFolderEvent.emit("start", folder); + return resolve(`${folder}.avi`); + }); + }); + } +} + +class MpegConverter extends FfmpegVideoConverter { + constructor() { + super(); + } + + async convert(folder, ffmpegInputData) { + return new Promise((resolve, reject) => { + ffmpeg(ffmpegInputData) + .inputOption("-f concat") + .inputOption("-safe 0") + .inputFPS(60) + .outputOption("-crf 25") + .save(`${folder}.mpeg`) + .on("error", (err) => { + fsP.unlink(ffmpegInputData); + this.removeTempFolderEvent.emit("start", folder); + return reject(err.message); + }) + .on("end", () => { + fsP.unlink(ffmpegInputData); + this.removeTempFolderEvent.emit("start", folder); + return resolve(`${folder}.mpeg`); + }); + }); + } +} + +class H265Converter extends FfmpegVideoConverter { + constructor() { + super(); + } + + async convert(folder, ffmpegInputData) { + return new Promise((resolve, reject) => { + ffmpeg(ffmpegInputData) + .inputOption("-f concat") + .inputOption("-safe 0") + .inputFPS(60) + .videoCodec("libx265") + .outputOption("-crf 25") + .save(`${folder}.h265`) + .on("error", (err) => { + fsP.unlink(ffmpegInputData); + this.removeTempFolderEvent.emit("start", folder); + return reject(err.message); + }) + .on("end", () => { + fsP.unlink(ffmpegInputData); + this.removeTempFolderEvent.emit("start", folder); + return resolve(`${folder}.h265`); + }); + }); + } +} + +class StudyVideoFactory extends VideoFactory { + constructor(mimeType, uids) { + super(mimeType, uids); + this.imagePathFactory = new StudyImagePathFactory(this.uids); + } + + async convert() { + let instancesFrames = await this.getInstancesFrames(); + let allJpegs = []; + for (let instanceFramesObj of instancesFrames) { + let jpegs = await this.generateJpegImage(instanceFramesObj, this.imageTempFolder); + allJpegs.push(...jpegs); + } + + return await super.convert(this.imageTempFolder, allJpegs); + } + +} + +module.exports.StudyVideoFactory = StudyVideoFactory; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6bcf9c31..f37715ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,9 @@ "env-var": "^7.3.1", "express": "^4.18.2", "express-session": "^1.17.2", + "ffmpeg-static": "^5.2.0", "flat": "^5.0.2", + "fluent-ffmpeg": "^2.1.2", "formidable": "^2.0.1", "iconv-lite": "^0.6.3", "image-size": "^1.1.1", @@ -757,6 +759,20 @@ "node": ">=6.9.0" } }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", @@ -1774,6 +1790,11 @@ "node": "*" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/async-mutex": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", @@ -1975,8 +1996,7 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "node_modules/buffer-writer": { "version": "2.0.0", @@ -2048,6 +2068,11 @@ "node": ">=6" } }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, "node_modules/chai": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", @@ -2320,7 +2345,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "dev": true, "engines": [ "node >= 6.0" ], @@ -3038,6 +3062,14 @@ "once": "^1.4.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, "node_modules/env-var": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/env-var/-/env-var-7.3.1.tgz", @@ -3448,6 +3480,21 @@ "pend": "~1.2.0" } }, + "node_modules/ffmpeg-static": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.2.0.tgz", + "integrity": "sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==", + "hasInstallScript": true, + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -3584,6 +3631,18 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", + "integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==", + "dependencies": { + "async": ">=0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/follow-redirects": { "version": "1.15.3", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", @@ -4124,6 +4183,19 @@ "node": ">= 0.8" } }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/http-response-object/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -6075,6 +6147,11 @@ "node": ">=6" } }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, "node_modules/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -6473,6 +6550,14 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7756,8 +7841,7 @@ "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/uglify-js": { "version": "3.17.4", @@ -7897,6 +7981,17 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -8715,6 +8810,17 @@ "js-tokens": "^4.0.0" } }, + "@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "requires": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + } + }, "@eslint/eslintrc": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", @@ -9537,6 +9643,11 @@ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "async-mutex": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", @@ -9689,8 +9800,7 @@ "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "buffer-writer": { "version": "2.0.0", @@ -9743,6 +9853,11 @@ } } }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, "chai": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", @@ -9962,7 +10077,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "dev": true, "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -10504,6 +10618,11 @@ "once": "^1.4.0" } }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" + }, "env-var": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/env-var/-/env-var-7.3.1.tgz", @@ -10828,6 +10947,17 @@ "pend": "~1.2.0" } }, + "ffmpeg-static": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.2.0.tgz", + "integrity": "sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==", + "requires": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + } + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -10935,6 +11065,15 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" }, + "fluent-ffmpeg": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", + "integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==", + "requires": { + "async": ">=0.2.9", + "which": "^1.1.1" + } + }, "follow-redirects": { "version": "1.15.3", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", @@ -11329,6 +11468,21 @@ "toidentifier": "1.0.1" } }, + "http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "requires": { + "@types/node": "^10.0.3" + }, + "dependencies": { + "@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + } + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -12725,6 +12879,11 @@ "callsites": "^3.0.0" } }, + "parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -13022,6 +13181,11 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -13994,8 +14158,7 @@ "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "uglify-js": { "version": "3.17.4", @@ -14101,6 +14264,14 @@ "webidl-conversions": "^3.0.0" } }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, "wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", diff --git a/package.json b/package.json index 69a2139c..c3128fdd 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,9 @@ "env-var": "^7.3.1", "express": "^4.18.2", "express-session": "^1.17.2", + "ffmpeg-static": "^5.2.0", "flat": "^5.0.2", + "fluent-ffmpeg": "^2.1.2", "formidable": "^2.0.1", "iconv-lite": "^0.6.3", "image-size": "^1.1.1",