adding initial project which created web API to convert
files to jpg, mp3, and mp4
This commit is contained in:
commit
c36d1e14fb
7
.eslintrc.js
Normal file
7
.eslintrc.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
"extends": "google",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 6
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
*.avi
|
||||||
|
*.mp3
|
||||||
|
*.m4a
|
||||||
|
*.zip
|
||||||
|
uploads/*
|
||||||
|
|
||||||
|
.dockerignore
|
||||||
|
.ebextensions
|
||||||
|
nbproject
|
||||||
|
public
|
||||||
|
package-lock.json
|
||||||
|
input
|
||||||
|
|
||||||
|
node-ffmpeg.zip
|
||||||
|
|
||||||
|
/node_modules
|
||||||
|
# Elastic Beanstalk Files
|
||||||
|
.elasticbeanstalk/*
|
||||||
|
!.elasticbeanstalk/*.cfg.yml
|
||||||
|
!.elasticbeanstalk/*.global.yml
|
||||||
|
/nbproject/private/
|
61
Dockerfile
Normal file
61
Dockerfile
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
FROM jrottenberg/ffmpeg:centos
|
||||||
|
|
||||||
|
MAINTAINER Paul Visco <paul.visco@gmail.com>
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
#
|
||||||
|
# A Docker image to convert audio and video for web using web API
|
||||||
|
#
|
||||||
|
# with
|
||||||
|
# - Latest FFMPEG (built)
|
||||||
|
# - NodeJS
|
||||||
|
# - fluent-ffmpeg
|
||||||
|
#
|
||||||
|
# For more on Fluent-FFMPEG, see
|
||||||
|
#
|
||||||
|
# https://github.com/fluent-ffmpeg/node-fluent-ffmpeg
|
||||||
|
#
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
# Add the following two dependencies for nodejs
|
||||||
|
RUN yum install -y git
|
||||||
|
RUN yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
|
||||||
|
RUN yum install -y nodejs npm --enablerepo=epel
|
||||||
|
|
||||||
|
WORKDIR /usr/local/src
|
||||||
|
|
||||||
|
# Custom Builds go here
|
||||||
|
RUN npm install -g fluent-ffmpeg
|
||||||
|
|
||||||
|
# Remove all tmpfile and cleanup
|
||||||
|
# =================================
|
||||||
|
WORKDIR /usr/local/
|
||||||
|
RUN rm -rf /usr/local/src
|
||||||
|
RUN yum clean all
|
||||||
|
RUN rm -rf /var/cache/yum
|
||||||
|
|
||||||
|
# =================================
|
||||||
|
|
||||||
|
# Setup a working directory to allow for
|
||||||
|
# docker run --rm -ti -v ${PWD}:/work ...
|
||||||
|
# =======================================
|
||||||
|
WORKDIR /work
|
||||||
|
|
||||||
|
# Make sure Node.js is installed
|
||||||
|
RUN node -v
|
||||||
|
RUN npm -v
|
||||||
|
|
||||||
|
#Create app dir
|
||||||
|
RUN mkdir -p /usr/src/app
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
#Install Dependencies
|
||||||
|
COPY package.json /usr/src/app
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
#Bundle app source
|
||||||
|
COPY . /usr/src/app
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
ENTRYPOINT []
|
||||||
|
CMD [ "node", "app.js" ]
|
70
README.md
Normal file
70
README.md
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# ffmpeg web service
|
||||||
|
|
||||||
|
An web service for converting audio/video files using Nodejs, Express and FFMPEG
|
||||||
|
|
||||||
|
Based off of jrottenberg/ffmpeg
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
> POST /mp3 - Convert audio file in request body to mp3
|
||||||
|
|
||||||
|
> POST /mp4 - Convert video file in request body to mp4
|
||||||
|
|
||||||
|
> POST /jpg - Convert image file to jpg
|
||||||
|
|
||||||
|
> GET /, /readme - Web Service Readme
|
||||||
|
|
||||||
|
### /mp3, /m4a
|
||||||
|
|
||||||
|
Curl Ex:
|
||||||
|
|
||||||
|
> curl -F "file=@input.wav" 127.0.0.1:3000/mp3 > output.mp3
|
||||||
|
> curl -F "file=@input.m4a" 127.0.0.1:3000/mp3 > output.mp3
|
||||||
|
> curl -F "file=@input.mov" 127.0.0.1:3000/mp4 > output.mp4
|
||||||
|
> curl -F "file=@input.mp4" 127.0.0.1:3000/mp4 > output.mp4
|
||||||
|
> curl -F "file=@input.tiff" 127.0.0.1:3000/jpg > output.jpg
|
||||||
|
> curl -F "file=@input.png" 127.0.0.1:3000/jpg > output.jpg
|
||||||
|
|
||||||
|
## Configuration and New Endpoints
|
||||||
|
You can change the ffmpeg conversion settings or add new endpoints by editing
|
||||||
|
the /app/endpoints.js file
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Requires local Node and FFMPEG installation.
|
||||||
|
|
||||||
|
1) Install FFMPEG https://ffmpeg.org/download.html
|
||||||
|
|
||||||
|
2) Install node https://nodejs.org/en/download/
|
||||||
|
Using homebrew:
|
||||||
|
> $ brew install node
|
||||||
|
|
||||||
|
## Dev - Running Local Node.js Web Service
|
||||||
|
|
||||||
|
Navigate to project directory and:
|
||||||
|
|
||||||
|
Install dependencies:
|
||||||
|
> $ npm install
|
||||||
|
|
||||||
|
Start app:
|
||||||
|
> $ node app.js
|
||||||
|
|
||||||
|
Check for errors with ESLint:
|
||||||
|
> $ ./node_modules/.bin/eslint .
|
||||||
|
|
||||||
|
## Running Local Docker Container
|
||||||
|
|
||||||
|
Build Docker Image from Dockerfile with a set image tag. ex: docker-ffpmeg
|
||||||
|
> $ docker build -t surebert/docker-ffpmeg .
|
||||||
|
|
||||||
|
Launch Docker Container from Docker Image, exposing port 9025 on localhost only
|
||||||
|
|
||||||
|
> docker run -d \
|
||||||
|
--name ffmpeg-service \
|
||||||
|
--restart=always \
|
||||||
|
-v /storage/tmpfs:/usr/src/app/uploads \
|
||||||
|
-p 127.0.0.1:9025:3000 \
|
||||||
|
surebert/docker-ffpmeg
|
||||||
|
|
||||||
|
Launch Docker Container from Docker Image, exposing port 9026 on all IPs
|
||||||
|
> docker run -p 9026:3000 -d surebert/docker-ffpmeg
|
168
app.js
Normal file
168
app.js
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
const Busboy = require('busboy');
|
||||||
|
const compression = require('compression');
|
||||||
|
const ffmpeg = require('fluent-ffmpeg');
|
||||||
|
const uniqueFilename = require('unique-filename');
|
||||||
|
const consts = require(__dirname + '/app/constants.js');
|
||||||
|
const endpoints = require(__dirname + '/app/endpoints.js');
|
||||||
|
const winston = require('winston');
|
||||||
|
|
||||||
|
app.use(compression());
|
||||||
|
winston.remove(winston.transports.Console);
|
||||||
|
winston.add(winston.transports.Console, {'timestamp': true});
|
||||||
|
|
||||||
|
for (let prop in endpoints.types) {
|
||||||
|
if (endpoints.types.hasOwnProperty(prop)) {
|
||||||
|
let ffmpegParams = endpoints.types[prop];
|
||||||
|
let bytes = 0;
|
||||||
|
app.post('/' + prop, function(req, res) {
|
||||||
|
let hitLimit = false;
|
||||||
|
let fileName = '';
|
||||||
|
let savedFile = uniqueFilename(__dirname + '/uploads/');
|
||||||
|
let busboy = new Busboy({
|
||||||
|
headers: req.headers,
|
||||||
|
limits: {
|
||||||
|
files: 1,
|
||||||
|
fileSize: consts.fileSizeLimit,
|
||||||
|
}});
|
||||||
|
busboy.on('filesLimit', function() {
|
||||||
|
winston.error(JSON.stringify({
|
||||||
|
type: 'filesLimit',
|
||||||
|
message: 'Upload file size limit hit',
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
busboy.on('file', function(
|
||||||
|
fieldname,
|
||||||
|
file,
|
||||||
|
filename,
|
||||||
|
encoding,
|
||||||
|
mimetype
|
||||||
|
) {
|
||||||
|
file.on('limit', function(file) {
|
||||||
|
hitLimit = true;
|
||||||
|
let err = {file: filename, error: 'exceeds max size limit'};
|
||||||
|
err = JSON.stringify(err);
|
||||||
|
winston.error(err);
|
||||||
|
res.writeHead(500, {'Connection': 'close'});
|
||||||
|
res.end(err);
|
||||||
|
});
|
||||||
|
let log = {
|
||||||
|
file: filename,
|
||||||
|
encoding: encoding,
|
||||||
|
mimetype: mimetype,
|
||||||
|
};
|
||||||
|
winston.info(JSON.stringify(log));
|
||||||
|
file.on('data', function(data) {
|
||||||
|
bytes += data.length;
|
||||||
|
});
|
||||||
|
file.on('end', function(data) {
|
||||||
|
log.bytes = bytes;
|
||||||
|
winston.info(JSON.stringify(log));
|
||||||
|
});
|
||||||
|
|
||||||
|
fileName = filename;
|
||||||
|
winston.info(JSON.stringify({
|
||||||
|
action: 'Uploading',
|
||||||
|
name: fileName,
|
||||||
|
}));
|
||||||
|
let written = file.pipe(fs.createWriteStream(savedFile));
|
||||||
|
|
||||||
|
if (written) {
|
||||||
|
winston.info(JSON.stringify({
|
||||||
|
action: 'saved',
|
||||||
|
path: savedFile,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
busboy.on('finish', function() {
|
||||||
|
if (hitLimit) {
|
||||||
|
fs.unlinkSync(savedFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
winston.info(JSON.stringify({
|
||||||
|
action: 'upload complete',
|
||||||
|
name: fileName,
|
||||||
|
}));
|
||||||
|
let outputFile = savedFile + '.' + ffmpegParams.extension;
|
||||||
|
winston.info(JSON.stringify({
|
||||||
|
action: 'begin conversion',
|
||||||
|
from: savedFile,
|
||||||
|
to: outputFile,
|
||||||
|
}));
|
||||||
|
let ffmpegConvertCommand = ffmpeg(savedFile);
|
||||||
|
ffmpegConvertCommand
|
||||||
|
.renice(15)
|
||||||
|
.outputOptions(ffmpegParams.outputOptions)
|
||||||
|
.on('error', function(err) {
|
||||||
|
let log = JSON.stringify({
|
||||||
|
type: 'ffmpeg',
|
||||||
|
message: err,
|
||||||
|
});
|
||||||
|
winston.error(log);
|
||||||
|
fs.unlinkSync(savedFile);
|
||||||
|
res.writeHead(500, {'Connection': 'close'});
|
||||||
|
res.end(log);
|
||||||
|
})
|
||||||
|
.on('end', function() {
|
||||||
|
fs.unlinkSync(savedFile);
|
||||||
|
winston.info(JSON.stringify({
|
||||||
|
action: 'starting download to client',
|
||||||
|
file: savedFile,
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.download(outputFile, null, function(err) {
|
||||||
|
if (err) {
|
||||||
|
winston.error(JSON.stringify({
|
||||||
|
type: 'download',
|
||||||
|
message: err,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
winston.info(JSON.stringify({
|
||||||
|
action: 'deleting',
|
||||||
|
file: outputFile,
|
||||||
|
}));
|
||||||
|
if (fs.unlinkSync(outputFile)) {
|
||||||
|
winston.info(JSON.stringify({
|
||||||
|
action: 'deleted',
|
||||||
|
file: outputFile,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.save(outputFile);
|
||||||
|
});
|
||||||
|
return req.pipe(busboy);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require('express-readme')(app, {
|
||||||
|
filename: 'README.md',
|
||||||
|
routes: ['/', '/readme'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = app.listen(consts.port, function() {
|
||||||
|
let host = server.address().address;
|
||||||
|
let port = server.address().port;
|
||||||
|
winston.info(JSON.stringify({
|
||||||
|
action: 'listening',
|
||||||
|
url: 'http://'+host+':'+port,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('connection', function(socket) {
|
||||||
|
winston.info(JSON.stringify({
|
||||||
|
action: 'new connection',
|
||||||
|
timeout: consts.timeout,
|
||||||
|
}));
|
||||||
|
socket.setTimeout(consts.timeout);
|
||||||
|
socket.server.timeout = consts.timeout;
|
||||||
|
server.keepAliveTimeout = consts.timeout;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(function(req, res, next) {
|
||||||
|
res.status(404).send(JSON.stringify({error: 'route not available'})+'\n');
|
||||||
|
});
|
3
app/constants.js
Normal file
3
app/constants.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
exports.fileSizeLimit = 524288000;
|
||||||
|
exports.port = 3000;
|
||||||
|
exports.timeout = 3600000;
|
37
app/endpoints.js
Normal file
37
app/endpoints.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
exports.types = {
|
||||||
|
jpg: {
|
||||||
|
extension: 'jpg',
|
||||||
|
outputOptions: [
|
||||||
|
'-pix_fmt yuv422p',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
m4a: {
|
||||||
|
extension: 'm4a',
|
||||||
|
outputOptions: [
|
||||||
|
'-codec:a libfdk_aac',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
mp3: {
|
||||||
|
extension: 'mp3',
|
||||||
|
outputOptions: [
|
||||||
|
'-codec:a libmp3lame',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
mp4: {
|
||||||
|
extension: 'mp4',
|
||||||
|
outputOptions: [
|
||||||
|
'-codec:v libx264',
|
||||||
|
'-profile:v high',
|
||||||
|
'-r 15',
|
||||||
|
'-crf 23',
|
||||||
|
'-preset ultrafast',
|
||||||
|
'-b:v 500k',
|
||||||
|
'-maxrate 500k',
|
||||||
|
'-bufsize 1000k',
|
||||||
|
'-vf scale=-2:640',
|
||||||
|
'-threads 8',
|
||||||
|
'-codec:a libfdk_aac',
|
||||||
|
'-b:a 128k',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
25
package.json
Normal file
25
package.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "node-ffmpeg-av",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "media conversion utilities",
|
||||||
|
"main": "index.js",
|
||||||
|
"author": {
|
||||||
|
"name": "Paul Visco",
|
||||||
|
"email": "paul.visco@gmail.com"
|
||||||
|
},
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"busboy": "^0.2.14",
|
||||||
|
"compression": "^1.7.2",
|
||||||
|
"express": "^4.16.3",
|
||||||
|
"express-readme": "0.0.5",
|
||||||
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
|
"fs": "0.0.1-security",
|
||||||
|
"unique-filename": "^1.1.0",
|
||||||
|
"winston": "^2.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^3.19.0",
|
||||||
|
"eslint-config-google": "^0.8.0"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user