added probe and refactoring

This commit is contained in:
Sami Salkosuo 2020-04-22 11:06:38 +03:00
parent b1df41c8b0
commit ce44d17ebc
9 changed files with 212 additions and 134 deletions

View File

@ -12,23 +12,20 @@ FFMPEG API is provided as Docker image for easy consumption.
== Endpoints
* `POST /convert/audio/to/mp3` - Convert audio file in request body to mp3
* `POST /convert/video/to/mp4` - Convert video file in request body to mp4
* `POST /convert/image/to/jpg` - Convert image file to jpg
* `GET /` - API Readme
== Usage
=== Convert
Convert audio/video/image files using the API.
* `curl -F "file=@input.wav" 127.0.0.1:3000/convert/audio/to/mp3 > output.mp3`
* `curl -F "file=@input.m4a" 127.0.0.1:3000/convert/audio/to/mp3 > output.mp3`
* `curl -F "file=@input.mov" 127.0.0.1:3000/convert/video/to/mp4 > output.mp4`
* `curl -F "file=@input.mp4" 127.0.0.1:3000/convert/videp/to/mp4 > output.mp4`
* `curl -F "file=@input.tiff" 127.0.0.1:3000/convert/image/to/jpg > output.jpg`
* `curl -F "file=@input.png" 127.0.0.1:3000/convert/image/to/jpg > output.jpg`
* `GET /` - API Readme.
* `GET /endpoints` - Service endpoints as JSON.
* `POST /convert/audio/to/mp3` - Convert audio file in request body to mp3. Returns mp3-file.
* `POST /convert/audio/to/wav` - Convert audio file in request body to wav. Returns wav-file.
* `POST /convert/video/to/mp4` - Convert video file in request body to mp4. Returns mp4-file.
* `POST /convert/image/to/jpg` - Convert image file to jpg. Returns jpg-file.
* `POST /video/extract/audio` - Extract audio track from POSTed video file. Returns audio track as 1-channel wav-file.
** Query param: `mono=no` - Returns audio track, all channels.
* `POST /video/extract/images` - Extract images from POSTed video file as PNG. Default FPS is 1. Returns JSON that includes download links to extracted images.
** Query param: `compress=zip|gzip` - Returns extracted images as _zip_ or _tar.gz_ (gzip).
** Query param: `fps=2` - Extract images using specified FPS.
* `GET /video/extract/download/:filename` - Downloads extracted image file and deletes it from server.
** Query param: `delete=no` - does not delete file.
* `POST /probe` - Probe media file, return JSON metadata.
== Docker image
@ -38,32 +35,85 @@ Convert audio/video/image files using the API.
* Build Docker image:
** `docker build -t ffmpeg-api .`
* Run image in foreground:
** `docker run -it --rm -p 3000:3000 ffmpeg-api`
** `docker run -it --rm --name ffmpeg-api -p 3000:3000 ffmpeg-api`
* Run image in background:
** `docker run -d -name ffmpeg-api -p 3000:3000 ffmpeg-api`
=== Use existing
* Run image in foreground:
** `docker run -it --rm -p 3000:3000 kazhar/ffmpeg-api`
** `docker run -it --rm --name ffmpeg-api -p 3000:3000 kazhar/ffmpeg-api`
* Run image in background:
** `docker run -d --name ffmpeg-api -p 3000:3000 kazhar/ffmpeg-api`
=== Environment variables
Default log level is INFO. Set log level using environment variable, _LOG_LEVEL_.
* Default log level is _info_. Set log level using environment variable, _LOG_LEVEL_.
** Set log level to debug:
** `docker run -it --rm -p 3000:3000 -e LOG_LEVEL=debug kazhar/ffmpeg-api`
* Default maximum file size of uploaded files is 512MB. Use environment variable _FILE_SIZE_LIMIT_BYTES_ to change it:
** Set max file size to 1MB:
** `docker run -it --rm -p 3000:3000 -e FILE_SIZE_LIMIT_BYTES=1048576 kazhar/ffmpeg-api`
* All uploaded and converted files are deleted when they've been downloaded. Use environment variable _KEEP_ALL_FILES_ to keep all files inside the container /tmp-directory:
** `docker run -it --rm -p 3000:3000 -e KEEP_ALL_FILES=true kazhar/ffmpeg-api`
- Set log level to debug:
- `docker run -it --rm -p 3000:3000 -e LOG_LEVEL=debug kazhar/ffmpeg-api`
Default maximum file size of uploaded files is 512MB. Use environment variable _FILE_SIZE_LIMIT_BYTES_ to change it:
== Usage
- Set max file size to 1MB:
- `docker run -it --rm -p 3000:3000 -e FILE_SIZE_LIMIT_BYTES=1048576 kazhar/ffmpeg-api`
Input file to FFMPEG API can be anything that ffmpeg supports. See https://www.ffmpeg.org/general.html#Supported-File-Formats_002c-Codecs-or-Features[ffmpeg docs for supported formats].
=== Convert
Convert audio/video/image files using the API.
* `curl -F "file=@input.wav" 127.0.0.1:3000/convert/audio/to/mp3 > output.mp3`
* `curl -F "file=@input.m4a" 127.0.0.1:3000/convert/audio/to/wav > output.wav`
* `curl -F "file=@input.mov" 127.0.0.1:3000/convert/video/to/mp4 > output.mp4`
* `curl -F "file=@input.mp4" 127.0.0.1:3000/convert/videp/to/mp4 > output.mp4`
* `curl -F "file=@input.tiff" 127.0.0.1:3000/convert/image/to/jpg > output.jpg`
* `curl -F "file=@input.png" 127.0.0.1:3000/convert/image/to/jpg > output.jpg`
=== Extract images
Extract images from video using the API.
* `curl -F "file=@input.mov" 127.0.0.1:3000/video/extract/images`
** Returns JSON that lists image download URLs for each extracted image.
** Default FPS is 1.
** Images are in PNG-format.
** See sample: link:./samples/extracted_images.json[extracted_images.json].
* `curl 127.0.0.1:3000/video/extract/download/ba0f565c-0001.png`
** Downloads exracted image and deletes it from server.
* `curl 127.0.0.1:3000/video/extract/download/ba0f565c-0001.png?delete=no`
** Downloads exracted image but does not deletes it from server.
* `curl -F "file=@input.mov" 127.0.0.1:3000/video/extract/images?compress=zip > images.zip`
** Returns ZIP package of all extracted images.
* `curl -F "file=@input.mov" 127.0.0.1:3000/video/extract/images?compress=gzip > images.tar.gz`
** Returns GZIP (tar.gz) package of all extracted images.
* `curl -F "file=@input.mov" 127.0.0.1:3000/video/extract/images?fps=0.5`
** Sets FPS to extract images. FPS=0.5 is every two seconds, FPS=4 is four images per seconds, etc.
=== Extract audio
Extract audio track from video using the API.
* `curl -F "file=@input.mov" 127.0.0.1:3000/video/extract/audio`
** Returns 1-channel WAV-file of video's audio track.
* `curl -F "file=@input.mov" 127.0.0.1:3000/video/extract/audio?mono=no`
** Returns WAV-file of video's audio track, with all the channels as in input video.
=== Probe
Probe audio/video/image files using the API.
* `curl -F "file=@input.mov" 127.0.0.1:3000/probe`
** Returns JSON metadata of media file.
** The same JSON metadata as in ffprobe command: `ffprobe -of json -show_streams -show_format input.mov`.
** See sample of MOV-video metadata: link:./samples/probe_metadata.json[probe_metadata.json].
== Background
Originally developed by https://github.com/surebert[Paul Visco].
Changes include updated Node.js version, Docker image based on Alpine, logging and others.
Changes include new functionality, updated Node.js version, Docker image based on Alpine, logging and other major refactoring.

View File

@ -26,28 +26,27 @@ app.use(compression());
var upload = require('./routes/uploadfile.js');
app.use(upload);
//test route for development
var test = require('./routes/test.js');
app.use('/test', test);
//routes to convert audio/video/image files to mp3/mp4/jpg
var convert = require('./routes/convert.js');
app.use('/convert', convert);
//routes to extract images or audio from video
var extract = require('./routes/extract.js');
app.use('/video/extract', extract);
//routes to probe file info
var probe = require('./routes/probe.js');
app.use('/probe', probe);
require('express-readme')(app, {
filename: 'index.md',
routes: ['/'],
});
const server = app.listen(constants.serverPort, function() {
let host = server.address().address;
let port = server.address().port;
logger.info('listening http://'+host+':'+port)
logger.info('Server started and listening http://'+host+':'+port)
});
server.on('connection', function(socket) {
@ -58,9 +57,9 @@ server.on('connection', function(socket) {
});
app.get('/endpoints', function(req, res) {
let code = 200;
res.writeHead(code, {'content-type' : 'text/plain'});
res.end("Endpoints:\n\n"+JSON.stringify(all_routes(app),null,2)+'\n');
res.status(200).send(all_routes(app));
//res.writeHead(200, {'content-type' : 'text/plain'});
//res.end("Endpoints:\n\n"+JSON.stringify(all_routes(app),null,2)+'\n');
});
app.use(function(req, res, next) {
@ -76,6 +75,3 @@ app.use(function(err, req, res, next){
res.end(`${err.message}\n`);
});
logger.debug(JSON.stringify(all_routes(app)));

View File

@ -2,3 +2,4 @@
exports.fileSizeLimit = parseInt(process.env.FILE_SIZE_LIMIT_BYTES || "536870912"); //536870912 = 512MB
exports.defaultFFMPEGProcessPriority=10;
exports.serverPort = 3000;//port to listen, NOTE: if using Docker/Kubernetes this port may not be the one clients are using
exports.keepAllFiles = process.env.KEEP_ALL_FILES || "false"; //if true, do not delete any uploaded/generated files

View File

@ -4,10 +4,6 @@
"description": "API for FFMPEG, media conversion utility",
"main": "app.js",
"bin" : { "ffmpegapi" : "./app.js" },
"author": {
"name": "Paul Visco",
"email": "paul.visco@gmail.com"
},
"license": "ISC",
"dependencies": {
"busboy": "~0.3.1",

View File

@ -1,33 +1,40 @@
var express = require('express')
const fs = require('fs');
const ffmpeg = require('fluent-ffmpeg');
const constants = require('../constants.js');
const logger = require('../utils/logger.js')
const utils = require('../utils/utils.js')
var router = express.Router()
const logger = require('../utils/logger.js')
//routes for /convert
//adds conversion type and format to res.locals. to be used in final post function
router.post('/audio/to/mp3', function (req, res,next) {
res.locals.conversion="audio"
res.locals.format="mp3"
res.locals.conversion="audio";
res.locals.format="mp3";
return convert(req,res,next);
});
router.post('/audio/to/wav', function (req, res,next) {
res.locals.conversion="audio";
res.locals.format="wav";
return convert(req,res,next);
});
router.post('/video/to/mp4', function (req, res,next) {
res.locals.conversion="video"
res.locals.format="mp4"
res.locals.conversion="video";
res.locals.format="mp4";
return convert(req,res,next);
});
router.post('/image/to/jpg', function (req, res,next) {
res.locals.conversion="image"
res.locals.format="jpg"
res.locals.conversion="image";
res.locals.format="jpg";
return convert(req,res,next);
});
@ -36,22 +43,24 @@ function convert(req,res,next) {
let format = res.locals.format;
let conversion = res.locals.conversion;
logger.debug(`path: ${req.path}, conversion: ${conversion}, format: ${format}`);
if (conversion == undefined || format == undefined)
{
res.status(400).send("Invalid convert URL. Use one of: /convert/image/to/jpg, /convert/audio/to/mp3 or /convert/video/to/mp4.\n");
return;
}
let ffmpegParams ={
extension: format
};
if (conversion == "image")
{
ffmpegParams.outputOptions= ['-pix_fmt yuv422p']
ffmpegParams.outputOptions= ['-pix_fmt yuv422p'];
}
if (conversion == "audio")
{
ffmpegParams.outputOptions=['-codec:a libmp3lame' ]
if (format === "mp3")
{
ffmpegParams.outputOptions=['-codec:a libmp3lame' ];
}
if (format === "wav")
{
ffmpegParams.outputOptions=['-codec:a pcm_s16le' ];
}
}
if (conversion == "video")
{
@ -88,16 +97,7 @@ function convert(req,res,next) {
})
.on('end', function() {
utils.deleteFile(savedFile);
logger.debug(`starting download to client ${savedFile}`);
res.download(outputFile, null, function(err) {
if (err) {
logger.error(`download ${err}`);
}
else {
utils.deleteFile(`${outputFile}`);
}
});
return utils.downloadFile(outputFile,null,req,res,next);
})
.save(outputFile);

View File

@ -5,10 +5,10 @@ const uniqueFilename = require('unique-filename');
var archiver = require('archiver');
const constants = require('../constants.js');
const logger = require('../utils/logger.js');
const utils = require('../utils/utils.js');
var router = express.Router()
const logger = require('../utils/logger.js')
const utils = require('../utils/utils.js')
var router = express.Router();
//routes for /video/extract
@ -29,24 +29,8 @@ router.post('/images', function (req, res,next) {
router.get('/download/:filename', function (req, res,next) {
//download extracted image
let filename = req.params.filename;
let deleteFile = req.query.delete || "true";
let file = `/tmp/${filename}`
logger.debug(`starting download to client ${file}`);
res.download(file, filename, function(err) {
if (err) {
logger.error(`download ${err}`);
}
else
{
//delete file if no delete=no query parameter
if (deleteFile === "true" || deleteFile === "yes")
{
utils.deleteFile(file);
}
}
});
return utils.downloadFile(file,null,req,res,next);
});
// extract audio or images from video
@ -68,9 +52,18 @@ function extract(req,res,next) {
if (extract === "audio"){
format = "wav"
ffmpegParams.outputOptions=[
`-ac 1` ,
'-vn',
`-f ${format}`
];
let monoAudio = req.query.mono || "yes";
if (monoAudio === "yes" || monoAudio === "true")
{
logger.debug("extracting audio, 1 channel only")
ffmpegParams.outputOptions.push('-ac 1')
}
else{
logger.debug("extracting audio, all channels")
}
}
ffmpegParams.extension = format;
@ -82,7 +75,7 @@ function extract(req,res,next) {
var uniqueFileNamePrefix = outputFile.replace("/tmp/","");
logger.debug(`uniqueFileNamePrefix ${uniqueFileNamePrefix}`);
//ffmpeg processing... converting file...
//ffmpeg processing...
var ffmpegCommand = ffmpeg(savedFile);
ffmpegCommand = ffmpegCommand
.renice(constants.defaultFFMPEGProcessPriority)
@ -94,6 +87,21 @@ function extract(req,res,next) {
res.end(JSON.stringify({error: `${err}`}));
})
//extract audio track from video as wav
if (extract === "audio"){
let wavFile = `${outputFile}.${format}`;
ffmpegCommand
.on('end', function() {
logger.debug(`ffmpeg process ended`);
utils.deleteFile(savedFile)
return utils.downloadFile(wavFile,null,req,res,next);
})
.save(wavFile);
}
//extract png images from video
if (extract === "images"){
ffmpegCommand
.output(`${outputFile}-%04d.png`)
@ -154,20 +162,8 @@ function extract(req,res,next) {
utils.deleteFile(file);
}
//return tar.gz
logger.debug(`starting download to client ${compressFilePath}`);
res.download(compressFilePath, compressFileName, function(err) {
if (err) {
logger.error(`download gzip error: ${err}`);
return next(err);
}
else
{
logger.debug(`download complete ${compressFilePath}`);
utils.deleteFile(compressFilePath);
}
});
//return compressed file
return utils.downloadFile(compressFilePath,compressFileName,req,res,next);
});
// Wait for streams to complete
@ -181,7 +177,7 @@ function extract(req,res,next) {
logger.debug(`output files in /tmp`);
var responseJson = {};
responseJson["totalfiles"] = files.length;
responseJson["description"] = "Extracted image files and URLs to download them. By default, downloading image also deletes the image from server. Note port in the URL may be different if server is running on Docker/Kubernetes.";
responseJson["description"] = `Extracted image files and URLs to download them. By default, downloading image also deletes the image from server. Note that port ${constants.serverPort} in the URL may not be the same as the real port, especially if server is running on Docker/Kubernetes.`;
var filesArray=[];
for (var i=0; i < files.length; i++) {
var file = files[i];
@ -197,13 +193,9 @@ function extract(req,res,next) {
}
})
.run();
// .save(outputFile);
}
}
module.exports = router

33
src/routes/probe.js Normal file
View File

@ -0,0 +1,33 @@
var express = require('express')
const ffmpeg = require('fluent-ffmpeg');
const logger = require('../utils/logger.js');
const utils = require('../utils/utils.js');
var router = express.Router();
//probe input file and return metadata
router.post('/', function (req, res,next) {
let savedFile = res.locals.savedFile;
logger.debug(`Probing ${savedFile}`);
//ffmpeg processing...
var ffmpegCommand = ffmpeg(savedFile)
ffmpegCommand.ffprobe(function(err, metadata) {
if (err)
{
next(err);
}
else
{
utils.deleteFile(savedFile);
res.status(200).send(metadata);
}
});
});
module.exports = router

View File

@ -1,22 +0,0 @@
var express = require('express')
var router = express.Router()
const logger = require('../utils/logger.js')
//route to handle file upload in all POST requests
// convert audio or video or image to mp3 or mp4 or jpg
router.post("/",function (req, res,next) {
logger.debug("path: " + req.path);
logger.debug("req.params: ");
for (const key in req.params) {
logger.debug(`${key}: ${req.params[key]}`);
}
logger.debug("res.locals.savedFile: " + res.locals.savedFile);
res.status(200).send("Test OK.");
});
module.exports = router;

View File

@ -1,12 +1,44 @@
const fs = require('fs');
const logger = require('./logger.js')
const constants = require('../constants.js');
function deleteFile (filepath) {
fs.unlinkSync(filepath);
logger.debug(`deleted ${filepath}`);
if (constants.keepAllFiles === "false")
{
fs.unlinkSync(filepath);
logger.debug(`deleted ${filepath}`);
}
else
{
logger.debug(`NOT deleted ${filepath}`);
}
}
function downloadFile (filepath,filename,req,res,next) {
logger.debug(`starting download to client. file: ${filepath}`);
res.download(filepath, filename, function(err) {
if (err) {
logger.error(`download error: ${err}`);
return next(err);
}
else
{
logger.debug(`download complete ${filepath}`);
let doDelete = req.query.delete || "true";
//delete file if doDelete is true
if (doDelete === "true" || doDelete === "yes")
{
deleteFile(filepath);
}
}
});
}
module.exports = {
deleteFile
deleteFile,
downloadFile
}