Created
December 12, 2020 07:37
-
-
Save anuragteapot/56e60d364611d7cf8267ec31ecb3827c to your computer and use it in GitHub Desktop.
Video Processing helper code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import FfmpegCommand from "fluent-ffmpeg"; | |
const spawn = require("child_process").spawn; | |
const exec = require("child_process").exec; | |
// const execFile = require("child_process").execFile; | |
import * as path from "path"; | |
import * as fs from "fs-extra"; | |
import moment from "moment"; | |
import { HttpStatus, Util, Logs } from "./../index"; | |
/** | |
* @class VideoProcessing | |
*/ | |
export default class VideoProcessing { | |
[x: string]: any; | |
/** | |
* @constructor | |
* | |
* @param {String} [filePath] | |
* @param {String} [destinationPath] | |
*/ | |
constructor(filePath, destinationPath) { | |
this.fileInfo = null; | |
this.filePath = filePath; | |
this.fileName = path.basename(filePath); | |
this.libraryPath = path.join("bento4", "bin"); | |
this.destinationPath = destinationPath; | |
this.QUALITY = Util.QUALITY; | |
this.FfmpegCommand = FfmpegCommand; | |
} | |
hhmmss(secs: any): string { | |
let minutes: any = Math.floor(secs / 60); | |
secs = secs % 60; | |
let hours: any = Math.floor(minutes / 60); | |
minutes = minutes % 60; | |
if (hours < 10) { | |
hours = "0" + hours; | |
} | |
if (minutes < 10) { | |
minutes = "0" + minutes; | |
} | |
if (secs < 10) { | |
secs = "0" + secs; | |
} | |
return `${hours}:${minutes}:${secs}`; | |
} | |
async init(): Promise<this> { | |
return new Promise(async (resolve, reject) => { | |
try { | |
this.fileInfo = await this.getVideoInfo(); | |
return resolve(this); | |
} catch (err) { | |
return reject(err); | |
} | |
}); | |
} | |
/** | |
* | |
* @param {string} command | |
* | |
*/ | |
async generateThumbnails(scale: number = 400, startSecond: number) { | |
const input = this.filePath; | |
const output = path.join( | |
this.destinationPath, | |
"trancoded", | |
"thumbnails", | |
`${this.fileName.split(".")[0]}_thumbnail_${scale}_${startSecond}.png` | |
); | |
const ss = this.hhmmss(startSecond); | |
return new Promise(async (resolve, reject) => { | |
try { | |
if (fs.existsSync(output)) { | |
return resolve(output); | |
} | |
if (!fs.existsSync(input)) { | |
return reject(new Error("[CUSTOM_ERROR] Input file not found.")); | |
} | |
await fs.ensureDir( | |
path.join(this.destinationPath, "trancoded", "thumbnails") | |
); | |
const ffmpeg = spawn("ffmpeg", [ | |
"-ss", | |
ss, | |
"-i", | |
input, | |
"-vframes", | |
"1", | |
"-vf", | |
`scale=${scale}:-2`, | |
output, | |
]); | |
ffmpeg.on("error", (error) => { | |
reject(error); | |
}); | |
ffmpeg.on("close", () => { | |
resolve(output); | |
}); | |
} catch (err) { | |
return reject(err); | |
} | |
}); | |
} | |
async getThumbnails(scale = 400, number = 4) { | |
return new Promise(async (resolve, reject) => { | |
try { | |
let duration = 0; | |
if (this.fileInfo.format) { | |
duration = parseInt(this.fileInfo.format.duration, 10); | |
} | |
let ans = []; | |
for (let i = 0; i < number; i++) { | |
let time = 0; | |
if (i > 0) { | |
time = Math.floor(Math.random() * duration - 1) + 1; | |
} | |
const data = await this.generateThumbnails(scale, time); | |
ans.push(data); | |
} | |
resolve(ans); | |
} catch (err) { | |
reject(err); | |
} | |
}); | |
} | |
/** | |
* | |
* @param {string} command | |
* | |
*/ | |
async generateGif(scale = "300") { | |
if (this.fileInfo.format) { | |
const duration = parseInt(this.fileInfo.format.duration, 10); | |
if (duration < 20) { | |
return new Error("[CUSTOM_ERROR] Length is too small to generate gif"); | |
} | |
} | |
const input = this.filePath; | |
const output = path.join( | |
this.destinationPath, | |
"trancoded", | |
"gif", | |
`${this.fileName.split(".")[0]}_gif_${scale}.gif` | |
); | |
return new Promise(async (resolve, reject) => { | |
try { | |
if (fs.existsSync(output)) { | |
return resolve(output); | |
} | |
if (!fs.existsSync(input)) { | |
return reject(new Error("[CUSTOM_ERROR] Input file not found.")); | |
} | |
await fs.ensureDir(path.join(this.destinationPath, "trancoded", "gif")); | |
const ffmpeg = spawn("ffmpeg", [ | |
"-ss", | |
"00:00:10", | |
"-i", | |
input, | |
"-to", | |
"10", | |
"-r", | |
"10", | |
"-vf", | |
`scale=${scale}:-2`, | |
output, | |
]); | |
ffmpeg.on("error", (error) => { | |
reject(error); | |
}); | |
ffmpeg.on("close", () => { | |
resolve(output); | |
}); | |
} catch (err) { | |
return reject(err); | |
} | |
}); | |
} | |
/** | |
* | |
*/ | |
async getVideoInfo() { | |
const input = this.filePath; | |
const command = `ffprobe -of json -show_streams -show_format ${input}`; | |
return new Promise(async (resolve, reject) => { | |
exec(command, (error, stdout) => { | |
if (error) { | |
reject(error); | |
} else { | |
resolve(JSON.parse(stdout)); | |
} | |
}); | |
}); | |
} | |
/** | |
* | |
*/ | |
async mp4Fragment() { | |
const input = this.filePath; | |
const output = path.join( | |
this.destinationPath, | |
"trancoded", | |
"fragment", | |
`${this.fileName.split(".")[0]}_fragmented.mp4` | |
); | |
const command = `${path.join( | |
this.libraryPath, | |
"mp4fragment" | |
)} ${input} ${output}`; | |
return new Promise(async (resolve, reject) => { | |
exec(command, (error, stdout) => { | |
if (error) { | |
reject(error); | |
} else { | |
resolve(JSON.parse(stdout)); | |
} | |
}); | |
}); | |
} | |
/** | |
* Method to check isRotated | |
* | |
* @method isRotated | |
* | |
* @public | |
*/ | |
isRotated() { | |
let rotate = false; | |
if (this.fileInfo && this.fileInfo.streams) { | |
if (this.fileInfo.streams[0]) { | |
if ( | |
this.fileInfo.streams[0].tags.rotate || | |
this.fileInfo.streams[0].tags.rotate == 90 || | |
this.fileInfo.streams[0].side_data_list || | |
this.fileInfo.streams[0].width < this.fileInfo.streams[0].height | |
) { | |
rotate = true; | |
} | |
} | |
} | |
return rotate; | |
} | |
/** | |
* Method to different format | |
* | |
* @method resizeVideo | |
* | |
* @param {Number} quality | |
* | |
* @public | |
*/ | |
async resizeVideo(quality) { | |
if (!quality || !this.QUALITY[quality]) { | |
return new Error("NOT_SUPPPORTED"); | |
} | |
const qData = this.QUALITY[quality]; | |
const input = this.filePath; | |
const output = path.join( | |
this.destinationPath, | |
"trancoded", | |
"quality", | |
quality.toString(), | |
`${this.fileName.split(".")[0]}_${quality.toString()}.mp4` | |
); | |
return new Promise(async (resolve, reject) => { | |
try { | |
if (fs.existsSync(output)) { | |
return resolve(output); | |
} | |
if (!fs.existsSync(input)) { | |
return reject(new Error("[CUSTOM_ERROR] Input file not found.")); | |
} | |
await fs.ensureDir( | |
path.join( | |
this.destinationPath, | |
"trancoded", | |
"quality", | |
quality.toString() | |
) | |
); | |
const rot = this.isRotated(); | |
const ffmpeg = spawn("ffmpeg", [ | |
"-i", | |
input, | |
"-preset", | |
"slow", | |
// "-codec:a", | |
// "libxaac", | |
// "-aspect", | |
// "16:9", | |
"-codec:v", | |
"libx264", | |
"-b:a", | |
"128k", | |
"-threads", | |
"0", | |
"-pix_fmt", | |
"yuv420p", | |
"-b:v", | |
qData["bv"], | |
"-minrate", | |
qData["minrate"], | |
"-maxrate", | |
qData["maxrate"], | |
"-bufsize", | |
qData["bufsize"], | |
"-vf", | |
`scale=${qData["width"]}:${qData["height"]}${ | |
rot | |
? `:force_original_aspect_ratio=decrease,pad=${qData["width"]}:${qData["height"]}:(ow-iw)/2:(oh-ih)/2` | |
: "" | |
} `, | |
output, | |
]); | |
if (process.env.DEBUG_ENV) { | |
ffmpeg.stderr.on("data", (data) => { | |
Logs(`[VIDEO PROCESSING] ${data.toString("utf8")}`); | |
}); | |
} | |
ffmpeg.on("error", (error) => { | |
reject(error); | |
}); | |
ffmpeg.on("close", () => { | |
resolve(output); | |
}); | |
} catch (err) { | |
return reject(err); | |
} | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment