From 267e8a9035aeecb13158dd2e42bd184ecbe4f4bb Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Sat, 2 Feb 2019 21:32:25 -0800 Subject: [PATCH 01/21] Doubled minimum compression threshold --- src/shouldCompress.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shouldCompress.js b/src/shouldCompress.js index f328cab1..6e95d8b3 100644 --- a/src/shouldCompress.js +++ b/src/shouldCompress.js @@ -1,5 +1,5 @@ -const MIN_COMPRESS_LENGTH = 1024 -const MIN_TRANSPARENT_COMPRESS_LENGTH = MIN_COMPRESS_LENGTH * 100 +const MIN_COMPRESS_LENGTH = 2048 +const MIN_TRANSPARENT_COMPRESS_LENGTH = MIN_COMPRESS_LENGTH * 50 function shouldCompress(req) { const { originType, originSize, webp } = req.params From 802ff12942b407f5f3386ce86cf6e038b4ad05fe Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Thu, 14 Feb 2019 19:27:46 -0800 Subject: [PATCH 02/21] implemented 'adaptive' compression to be higher based on resolution and byte size of image --- src/compress.js | 50 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/src/compress.js b/src/compress.js index 8aa544aa..0194612e 100644 --- a/src/compress.js +++ b/src/compress.js @@ -4,23 +4,41 @@ const redirect = require('./redirect') function compress(req, res, input) { const format = req.params.webp ? 'webp' : 'jpeg' - sharp(input) - .grayscale(req.params.grayscale) - .toFormat(format, { - quality: req.params.quality, - progressive: true, - optimizeScans: true - }) - .toBuffer((err, output, info) => { - if (err || !info || res.headersSent) return redirect(req, res) + const image = sharp(input); + + image + .metadata(function(err, metadata){ + let pixelCount = metadata.width * metadata.height; + var compressionQuality = req.params.quality; + + if(pixelCount > 3000000 || metadata.size > 1536000){ + compressionQuality *= 0.1 + }else if(pixelCount > 2000000 && metadata.size > 1024000){ + compressionQuality *= 0.25 + }else if(pixelCount > 1000000 && metadata.size > 512000){ + compressionQuality *= 0.5 + }else if(pixelCount > 500000 && metadata.size > 256000){ + compressionQuality *= 0.75 + } + + sharp(input) + .grayscale(req.params.grayscale) + .toFormat(format, { + quality: compressionQuality, + progressive: true, + optimizeScans: true + }) + .toBuffer((err, output, info) => { + if (err || !info || res.headersSent) return redirect(req, res) - res.setHeader('content-type', `image/${format}`) - res.setHeader('content-length', info.size) - res.setHeader('x-original-size', req.params.originSize) - res.setHeader('x-bytes-saved', req.params.originSize - info.size) - res.status(200) - res.write(output) - res.end() + res.setHeader('content-type', `image/${format}`) + res.setHeader('content-length', info.size) + res.setHeader('x-original-size', req.params.originSize) + res.setHeader('x-bytes-saved', req.params.originSize - info.size) + res.status(200) + res.write(output) + res.end() + }) }) } From 6b21719186de70c2b320c9e6d4c4240288e5814f Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Mon, 4 Mar 2019 20:40:50 -0800 Subject: [PATCH 03/21] implemented apng passthrough with threshold and animated gif2webp handling --- package.json | 6 ++- src/compress.js | 108 +++++++++++++++++++++++++++++------------- src/proxy.js | 2 +- src/shouldCompress.js | 14 ++++-- 4 files changed, 92 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index b88a296e..193c3dcd 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "dependencies": { "basic-auth": "^2.0.0", "express": "4.16.2", - "lodash": "^4.17.4", - "request": "^2.83.0", + "gif2webp-bin": "^2.0.0", + "is-animated": "^1.1.1", + "lodash": "^4.17.11", + "request": "^2.88.0", "sharp": "^0.18.4" }, "engines": { diff --git a/src/compress.js b/src/compress.js index 0194612e..ce7d4ef3 100644 --- a/src/compress.js +++ b/src/compress.js @@ -1,45 +1,89 @@ const sharp = require('sharp') const redirect = require('./redirect') +const isAnimated = require('is-animated') +const {execFile} = require('child_process') +const gif2webp = require('gif2webp-bin') +const fs = require('fs') +const os = require("os") function compress(req, res, input) { const format = req.params.webp ? 'webp' : 'jpeg' - - const image = sharp(input); + const originType = req.params.originType - image - .metadata(function(err, metadata){ - let pixelCount = metadata.width * metadata.height; - var compressionQuality = req.params.quality; - - if(pixelCount > 3000000 || metadata.size > 1536000){ - compressionQuality *= 0.1 - }else if(pixelCount > 2000000 && metadata.size > 1024000){ - compressionQuality *= 0.25 - }else if(pixelCount > 1000000 && metadata.size > 512000){ - compressionQuality *= 0.5 - }else if(pixelCount > 500000 && metadata.size > 256000){ - compressionQuality *= 0.75 - } - - sharp(input) + if(!req.params.grayscale && format === 'webp' && originType.endsWith('gif') && isAnimated(input)){ + let {hostname, pathname} = (new URL(req.params.url)) + + let path = `${os.tmpdir()}/${hostname + encodeURIComponent(pathname)}`; + fs.writeFile(path + '.gif', input, (err) => { + console.error(err) + if (err) return redirect(req, res) + //defer to gif2webp *high latency* + execFile(gif2webp, ['-lossy', '-m', 2, '-q', req.params.quality , '-mt', + `${path}.gif`, + '-o', + `${path}.webp`], (convErr) => { + if(convErr) console.error(convErr) + console.log('GIF Image converted!') + fs.readFile(`${path}.webp`, (readErr, data) => { + console.error(readErr); + if (readErr || res.headersSent) return redirect(req, res) + + setResponseHeaders(fs.statSync(`${path}.webp`), 'webp') + + //initiate cleanup procedures + fs.unlink(`${path}.gif`, function(){}) + fs.unlink(`${path}.webp`, function(){}) + + //Write to stream + res.write(data) + res.end() + }) + }) + }) + + }else{ + + const image = sharp(input); + + image + .metadata(function(err, metadata){ + let pixelCount = metadata.width * metadata.height; + var compressionQuality = req.params.quality; + + if(pixelCount > 3000000 || metadata.size > 1536000){ + compressionQuality *= 0.1 + }else if(pixelCount > 2000000 && metadata.size > 1024000){ + compressionQuality *= 0.25 + }else if(pixelCount > 1000000 && metadata.size > 512000){ + compressionQuality *= 0.5 + }else if(pixelCount > 500000 && metadata.size > 256000){ + compressionQuality *= 0.75 + } + + sharp(input) .grayscale(req.params.grayscale) .toFormat(format, { - quality: compressionQuality, - progressive: true, - optimizeScans: true - }) - .toBuffer((err, output, info) => { - if (err || !info || res.headersSent) return redirect(req, res) + quality: compressionQuality, + progressive: true, + optimizeScans: true + }) + .toBuffer((err, output, info) => { + if (err || !info || res.headersSent) return redirect(req, res) - res.setHeader('content-type', `image/${format}`) - res.setHeader('content-length', info.size) - res.setHeader('x-original-size', req.params.originSize) - res.setHeader('x-bytes-saved', req.params.originSize - info.size) - res.status(200) - res.write(output) - res.end() + setResponseHeaders(info, format) + res.write(output) + res.end() + }) }) - }) + } + + function setResponseHeaders (info, imgFormat){ + res.setHeader('content-type', `image/${imgFormat}`) + res.setHeader('content-length', info.size) + res.setHeader('x-original-size', req.params.originSize) + res.setHeader('x-bytes-saved', req.params.originSize - info.size) + res.status(200) + } } module.exports = compress diff --git a/src/proxy.js b/src/proxy.js index 23f18a00..8909d3a4 100644 --- a/src/proxy.js +++ b/src/proxy.js @@ -31,7 +31,7 @@ function proxy(req, res) { req.params.originType = origin.headers['content-type'] || '' req.params.originSize = buffer.length - if (shouldCompress(req)) { + if (shouldCompress(req, buffer)) { compress(req, res, buffer) } else { bypass(req, res, buffer) diff --git a/src/shouldCompress.js b/src/shouldCompress.js index 6e95d8b3..c59f17e3 100644 --- a/src/shouldCompress.js +++ b/src/shouldCompress.js @@ -1,7 +1,10 @@ -const MIN_COMPRESS_LENGTH = 2048 -const MIN_TRANSPARENT_COMPRESS_LENGTH = MIN_COMPRESS_LENGTH * 50 +const isAnimated = require('is-animated') -function shouldCompress(req) { +const MIN_COMPRESS_LENGTH = process.env.MIN_COMPRESS_LENGTH || 2048 +const MIN_TRANSPARENT_COMPRESS_LENGTH = MIN_COMPRESS_LENGTH * 50 //~100KB +const APNG_THRESH_LENGTH = MIN_COMPRESS_LENGTH * 100 //~200KB + +function shouldCompress(req, buffer) { const { originType, originSize, webp } = req.params if (!originType.startsWith('image')) return false @@ -14,6 +17,11 @@ function shouldCompress(req) { ) { return false } + + if((originType.endsWith('png')) && isAnimated(buffer) && originSize < APNG_THRESH_LENGTH){ + //It's an animated png file, let it pass through through if small enough + return false + } return true } From e99d0696378c6fb47bfefb33fbcd9bccbc76d5ca Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Mon, 4 Mar 2019 20:48:46 -0800 Subject: [PATCH 04/21] define URL for compatibility with node.js 8 and earlier --- src/compress.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/compress.js b/src/compress.js index ce7d4ef3..a5380fec 100644 --- a/src/compress.js +++ b/src/compress.js @@ -4,7 +4,8 @@ const isAnimated = require('is-animated') const {execFile} = require('child_process') const gif2webp = require('gif2webp-bin') const fs = require('fs') -const os = require("os") +const os = require('os') +const {URL} = require('url') function compress(req, res, input) { const format = req.params.webp ? 'webp' : 'jpeg' From e8c4a0573b9e64412598fe7e973c7911fd15e074 Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Mon, 25 Mar 2019 22:45:27 -0700 Subject: [PATCH 05/21] Added Content-Disposition filename --- src/bypass.js | 4 ++++ src/compress.js | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/bypass.js b/src/bypass.js index 5c99d24b..d476e451 100644 --- a/src/bypass.js +++ b/src/bypass.js @@ -1,6 +1,10 @@ function bypass(req, res, buffer) { res.setHeader('x-proxy-bypass', 1) res.setHeader('content-length', buffer.length) + let filename = (new URL(req.params.url).pathname.split('/').pop()) + if(filename){ + res.setHeader('Content-Disposition', ['inline', 'filename=' + filename ]) + } res.status(200) res.write(buffer) res.end() diff --git a/src/compress.js b/src/compress.js index a5380fec..1c3e8e81 100644 --- a/src/compress.js +++ b/src/compress.js @@ -12,7 +12,7 @@ function compress(req, res, input) { const originType = req.params.originType if(!req.params.grayscale && format === 'webp' && originType.endsWith('gif') && isAnimated(input)){ - let {hostname, pathname} = (new URL(req.params.url)) + let {hostname, pathname} = new URL(req.params.url) let path = `${os.tmpdir()}/${hostname + encodeURIComponent(pathname)}`; fs.writeFile(path + '.gif', input, (err) => { @@ -81,6 +81,8 @@ function compress(req, res, input) { function setResponseHeaders (info, imgFormat){ res.setHeader('content-type', `image/${imgFormat}`) res.setHeader('content-length', info.size) + let filename = (new URL(req.params.url).pathname.split('/').pop() || "image") + '.' + format + res.setHeader('Content-Disposition', 'inline; filename="' + filename + '"' ) res.setHeader('x-original-size', req.params.originSize) res.setHeader('x-bytes-saved', req.params.originSize - info.size) res.status(200) From 0a1f69970f8604ca5661ca853e6e2354b9f0e0a4 Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Mon, 23 Sep 2019 14:03:22 -0700 Subject: [PATCH 06/21] rebased to exclude gcf-specific changes --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 181487c1..ad29bcde 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,9 @@ You can deploy this service to Heroku: [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/ayastreb/bandwidth-hero-proxy) +ENVIRONMENT_VARIABLES +`MIN_COMPRESS_LENGTH=2048` (minimum byte length for an image to be compressible; default 2048 ~2kB) + [![Deploy to Heroku guide](http://img.youtube.com/vi/y3tkYEXAics/0.jpg)](http://www.youtube.com/watch?v=y3tkYEXAics) ### Self-hosted From 5dd8d4ed5149b047609a69c830f891f9d59486a4 Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Tue, 26 Mar 2019 18:06:46 -0700 Subject: [PATCH 07/21] add URL module to bypass.js --- src/bypass.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bypass.js b/src/bypass.js index d476e451..6e872f38 100644 --- a/src/bypass.js +++ b/src/bypass.js @@ -1,3 +1,5 @@ +const {URL} = require('url') + function bypass(req, res, buffer) { res.setHeader('x-proxy-bypass', 1) res.setHeader('content-length', buffer.length) From 09aa93c5c4c0f6474c10a49f85100201ee9ef0ec Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Wed, 10 Apr 2019 19:56:36 -0700 Subject: [PATCH 08/21] round up compression quality after applying compression scaling. Fixed header bug in Content-Disposition on bypass.js --- src/bypass.js | 2 +- src/compress.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bypass.js b/src/bypass.js index 6e872f38..463b66bd 100644 --- a/src/bypass.js +++ b/src/bypass.js @@ -5,7 +5,7 @@ function bypass(req, res, buffer) { res.setHeader('content-length', buffer.length) let filename = (new URL(req.params.url).pathname.split('/').pop()) if(filename){ - res.setHeader('Content-Disposition', ['inline', 'filename=' + filename ]) + res.setHeader('Content-Disposition', 'inline; filename="' + filename + '"') } res.status(200) res.write(buffer) diff --git a/src/compress.js b/src/compress.js index 1c3e8e81..818f183c 100644 --- a/src/compress.js +++ b/src/compress.js @@ -60,6 +60,7 @@ function compress(req, res, input) { }else if(pixelCount > 500000 && metadata.size > 256000){ compressionQuality *= 0.75 } + compressionQuality = Math.ceil(compressionQuality) sharp(input) .grayscale(req.params.grayscale) From 6f433eaa4ed9a635a6b2e41217eb969b31d7b057 Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Fri, 20 Sep 2019 20:35:04 -0700 Subject: [PATCH 09/21] updated sharp library --- package-lock.json | 1025 --------------------------------------------- package.json | 2 +- 2 files changed, 1 insertion(+), 1026 deletions(-) delete mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index dc8f69f9..00000000 --- a/package-lock.json +++ /dev/null @@ -1,1025 +0,0 @@ -{ - "name": "bandwidth-hero-proxy", - "version": "1.0.1", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "accepts": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", - "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", - "requires": { - "mime-types": "2.1.17", - "negotiator": "0.6.1" - } - }, - "ajv": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.3.0.tgz", - "integrity": "sha1-RBT/dKUIecII7l/cgm4ywwNUnto=", - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" - }, - "basic-auth": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", - "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "1.0.4", - "debug": "2.6.9", - "depd": "1.1.1", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "on-finished": "2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "1.6.15" - } - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "caw": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", - "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", - "requires": { - "get-proxy": "2.1.0", - "isurl": "1.0.0", - "tunnel-agent": "0.6.0", - "url-to-options": "1.0.1" - }, - "dependencies": { - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.1" - } - } - } - }, - "chownr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", - "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "color": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color/-/color-2.0.1.tgz", - "integrity": "sha512-ubUCVVKfT7r2w2D3qtHakj8mbmKms+tThR8gI8zEYCbUBl8/voqFGt3kgBqGwXAopgXybnkuOq+qMYCRrp4cXw==", - "requires": { - "color-convert": "1.9.1", - "color-string": "1.5.2" - } - }, - "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "color-string": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.2.tgz", - "integrity": "sha1-JuRYFLw8mny9Z1FkikFDRRSnc6k=", - "requires": { - "color-name": "1.1.3", - "simple-swizzle": "0.2.2" - } - }, - "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "requires": { - "delayed-stream": "1.0.0" - } - }, - "config-chain": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz", - "integrity": "sha1-q6CXR9++TD5w52am5BWG4YWfxvI=", - "requires": { - "ini": "1.3.4", - "proto-list": "1.2.4" - } - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "requires": { - "mimic-response": "1.0.0" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "detect-libc": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-0.2.0.tgz", - "integrity": "sha1-R/31ZzSKF+wl/L8LnkRjSKdvn7U=" - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "encodeurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", - "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "express": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", - "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", - "requires": { - "accepts": "1.3.4", - "array-flatten": "1.1.1", - "body-parser": "1.18.2", - "content-disposition": "0.5.2", - "content-type": "1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "1.1.1", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "etag": "1.8.1", - "finalhandler": "1.1.0", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "1.1.2", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "2.0.2", - "qs": "6.5.1", - "range-parser": "1.2.0", - "safe-buffer": "5.1.1", - "send": "0.16.1", - "serve-static": "1.13.1", - "setprototypeof": "1.1.0", - "statuses": "1.3.1", - "type-is": "1.6.15", - "utils-merge": "1.0.1", - "vary": "1.1.2" - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", - "requires": { - "debug": "2.6.9", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "statuses": "1.3.1", - "unpipe": "1.0.0" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "get-proxy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", - "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", - "requires": { - "npm-conf": "1.1.3" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "has-symbol-support-x": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.1.tgz", - "integrity": "sha512-JkaetveU7hFbqnAC1EV1sF4rlojU2D4Usc5CmS69l6NfmPDnpnFUegzFg33eDkkpNCxZ0mQp65HwUDrNFS/8MA==" - }, - "has-to-string-tag-x": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", - "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", - "requires": { - "has-symbol-support-x": "1.4.1" - } - }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": "1.3.1" - }, - "dependencies": { - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - } - } - }, - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", - "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=" - }, - "ipaddr.js": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", - "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" - }, - "is-arrayish": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.1.tgz", - "integrity": "sha1-wt/DhquqDD4zxI2z/ocFnmkGXv0=" - }, - "is-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", - "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "isurl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", - "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", - "requires": { - "has-to-string-tag-x": "1.4.1", - "is-object": "1.0.1" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" - }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "requires": { - "mime-db": "1.30.0" - } - }, - "mimic-response": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.0.tgz", - "integrity": "sha1-3z02Uqc/3ta5sLJBRub9BSNTRY4=" - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "minipass": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.1.tgz", - "integrity": "sha512-u1aUllxPJUI07cOqzR7reGmQxmCqlH88uIIsf6XZFEWgw7gXKpJdR+5R9Y3KEDmWYkdIz9wXZs3C0jOPxejk/Q==", - "requires": { - "yallist": "3.0.2" - }, - "dependencies": { - "yallist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" - } - } - }, - "minizlib": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.0.4.tgz", - "integrity": "sha512-sN4U9tIJtBRwKbwgFh9qJfrPIQ/GGTRr1MGqkgOeMTLy8/lM0FcWU//FqlnZ3Vb7gJ+Mxh3FOg1EklibdajbaQ==", - "requires": { - "minipass": "2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "nan": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", - "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "npm-conf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", - "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", - "requires": { - "config-chain": "1.1.11", - "pify": "3.0.0" - } - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" - }, - "proxy-addr": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", - "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", - "requires": { - "forwarded": "0.1.2", - "ipaddr.js": "1.5.2" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "unpipe": "1.0.0" - } - }, - "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.1", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.2.0" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "requires": { - "hoek": "4.2.0" - } - } - } - }, - "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.3.0", - "har-schema": "2.0.0" - } - }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.1.0" - } - }, - "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "requires": { - "hoek": "4.2.0" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" - } - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" - }, - "send": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", - "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", - "requires": { - "debug": "2.6.9", - "depd": "1.1.1", - "destroy": "1.0.4", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "etag": "1.8.1", - "fresh": "0.5.2", - "http-errors": "1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "2.3.0", - "range-parser": "1.2.0", - "statuses": "1.3.1" - } - }, - "serve-static": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", - "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", - "requires": { - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "parseurl": "1.3.2", - "send": "0.16.1" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "sharp": { - "version": "0.18.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.18.4.tgz", - "integrity": "sha1-/jKcDwaJbCiqJDdt8f/wKuV/LTQ=", - "requires": { - "caw": "2.0.1", - "color": "2.0.1", - "detect-libc": "0.2.0", - "nan": "2.8.0", - "semver": "5.4.1", - "simple-get": "2.7.0", - "tar": "3.2.1" - } - }, - "simple-concat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", - "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" - }, - "simple-get": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.7.0.tgz", - "integrity": "sha512-RkE9rGPHcxYZ/baYmgJtOSM63vH0Vyq+ma5TijBcLla41SWlh8t6XYIGMR/oeZcmr+/G8k+zrClkkVrtnQ0esg==", - "requires": { - "decompress-response": "3.3.0", - "once": "1.4.0", - "simple-concat": "1.0.0" - } - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "requires": { - "is-arrayish": "0.3.1" - } - }, - "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, - "tar": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-3.2.1.tgz", - "integrity": "sha512-ZSzds1E0IqutvMU8HxjMaU8eB7urw2fGwTq88ukDOVuUIh0656l7/P7LiVPxhO5kS4flcRJQk8USG+cghQbTUQ==", - "requires": { - "chownr": "1.0.1", - "minipass": "2.2.1", - "minizlib": "1.0.4", - "mkdirp": "0.5.1", - "yallist": "3.0.2" - }, - "dependencies": { - "yallist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" - } - } - }, - "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", - "requires": { - "punycode": "1.4.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-is": { - "version": "1.6.15", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", - "requires": { - "media-typer": "0.3.0", - "mime-types": "2.1.17" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "url-to-options": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", - "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } - } -} diff --git a/package.json b/package.json index 193c3dcd..f7a1ec59 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "is-animated": "^1.1.1", "lodash": "^4.17.11", "request": "^2.88.0", - "sharp": "^0.18.4" + "sharp": "^0.23.0" }, "engines": { "node": "^8.6.0" From f5eccc0aca97564e496a57fea2b6ec1a5d411e60 Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Sat, 21 Sep 2019 20:20:18 -0700 Subject: [PATCH 10/21] implemented webm vp9+opus re-encode --- package.json | 3 ++ src/compress.js | 11 ++++-- src/params.js | 2 +- src/proxy.js | 8 +++- src/reEncode.js | 102 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 src/reEncode.js diff --git a/package.json b/package.json index f7a1ec59..00ddb92c 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,11 @@ "start": "node server.js" }, "dependencies": { + "@ffmpeg-installer/ffmpeg": "^1.0.20", + "@ffprobe-installer/ffprobe": "^1.0.12", "basic-auth": "^2.0.0", "express": "4.16.2", + "fluent-ffmpeg": "^2.1.2", "gif2webp-bin": "^2.0.0", "is-animated": "^1.1.1", "lodash": "^4.17.11", diff --git a/src/compress.js b/src/compress.js index 818f183c..347e7416 100644 --- a/src/compress.js +++ b/src/compress.js @@ -18,7 +18,7 @@ function compress(req, res, input) { fs.writeFile(path + '.gif', input, (err) => { console.error(err) if (err) return redirect(req, res) - //defer to gif2webp *high latency* + //defer to gif2webp *higher latency* execFile(gif2webp, ['-lossy', '-m', 2, '-q', req.params.quality , '-mt', `${path}.gif`, '-o', @@ -31,12 +31,13 @@ function compress(req, res, input) { setResponseHeaders(fs.statSync(`${path}.webp`), 'webp') + //Write to stream + res.write(data) + //initiate cleanup procedures fs.unlink(`${path}.gif`, function(){}) fs.unlink(`${path}.webp`, function(){}) - //Write to stream - res.write(data) res.end() }) }) @@ -51,12 +52,16 @@ function compress(req, res, input) { let pixelCount = metadata.width * metadata.height; var compressionQuality = req.params.quality; + //3MP or 1.5MB if(pixelCount > 3000000 || metadata.size > 1536000){ compressionQuality *= 0.1 + //2MP or 1MB }else if(pixelCount > 2000000 && metadata.size > 1024000){ compressionQuality *= 0.25 + //1MP or 512KB }else if(pixelCount > 1000000 && metadata.size > 512000){ compressionQuality *= 0.5 + //0.5MP or 256KB }else if(pixelCount > 500000 && metadata.size > 256000){ compressionQuality *= 0.75 } diff --git a/src/params.js b/src/params.js index 39447799..8ac33914 100644 --- a/src/params.js +++ b/src/params.js @@ -10,7 +10,7 @@ function params(req, res, next) { req.params.webp = !req.query.jpeg req.params.grayscale = req.query.bw != 0 req.params.quality = parseInt(req.query.l, 10) || DEFAULT_QUALITY - + req.params.media = req.query.audio || req.query.video next() } diff --git a/src/proxy.js b/src/proxy.js index 8909d3a4..60370937 100644 --- a/src/proxy.js +++ b/src/proxy.js @@ -3,6 +3,7 @@ const pick = require('lodash').pick const shouldCompress = require('./shouldCompress') const redirect = require('./redirect') const compress = require('./compress') +const reEncode = require('./reEncode') const bypass = require('./bypass') const copyHeaders = require('./copyHeaders') @@ -28,12 +29,15 @@ function proxy(req, res) { copyHeaders(origin, res) res.setHeader('content-encoding', 'identity') - req.params.originType = origin.headers['content-type'] || '' + let originType = origin.headers['content-type'] || '' + req.params.originType = originType req.params.originSize = buffer.length if (shouldCompress(req, buffer)) { compress(req, res, buffer) - } else { + } else if (originType.startsWith('video') || originType.startsWith('audio')){ + reEncode(req, res, buffer) + }else { bypass(req, res, buffer) } } diff --git a/src/reEncode.js b/src/reEncode.js new file mode 100644 index 00000000..c49103d8 --- /dev/null +++ b/src/reEncode.js @@ -0,0 +1,102 @@ +const redirect = require('./redirect') +const ffmpegPath = require("@ffmpeg-installer/ffmpeg").path +const ffprobePath = require('@ffprobe-installer/ffprobe').path; +const ffmpeg = require("fluent-ffmpeg") +const fs = require('fs') +const os = require('os') +const {URL} = require('url') + +ffmpeg.setFfmpegPath(ffmpegPath); +ffmpeg.setFfprobePath(ffprobePath); + +function reEncode(req, res, input) { + var quality = req.params.quality; + var bitrateTarget = quality * 15; + + ffmpeg.ffprobe(req.params.url, function(err, metadata) { + let audioStreamInfo, videoStreamInfo, format, audioOnly + format = metadata.format + + for (let stream in metadata.streams){ + stream = metadata.streams[stream] + if (videoStreamInfo && audioStreamInfo){ + console.log("multiple audio or video streams detected") + } + //console.log(stream) + switch(stream.codec_type){ + case("video"): + videoStreamInfo = stream + break + case("audio"): + audioStreamInfo = stream + break + } + } + audioOnly = !videoStreamInfo; + + if((audioStreamInfo < 50000 && audioOnly) || videoStreamInfo && ( + videoStreamInfo.bit_rate < bitrateTarget * 1000 || videoStreamInfo.duration > 240)){ + return redirect(req, res) + } + + res.setHeader('content-type', `${audioOnly ? "audio":"video"}/webm`) + + //let {hostname, pathname} = new URL(req.params.url) + //let path = `${os.tmpdir()}/${hostname + encodeURIComponent(pathname)}.webm`; + + if(audioOnly){ + ffmpeg(req.params.url) + .audioCodec("opus") + .format("webm") + .audioBitrate(quality * 2) + .pipe(res, { end: true }) + }else{ + ffmpeg(req.params.url) + .videoCodec("libvpx-vp9")//videoStreamInfo.codec_name) + .videoBitrate(bitrateTarget) //300 - 1200 + .audioCodec("opus")//audioStreamInfo.codec_name) + //.audioQuality(Math.ceil(quality / 20)) //1-4 + .audioBitrate(quality * 2) + .size( + //480p cap + '?x' + Math.min(480, videoStreamInfo ? videoStreamInfo.height : 0) + ) + //.format(format.format_name.split(',')[0]) + .format('webm') + .outputOptions(["-deadline realtime","-cpu-used 5"]) + //.outputOptions("-movflags +frag_keyframe") + .on('error', function(err) { + console.log('An error occurred: ' + err.message) + console.error(err); + }) + .on('stderr', function(stderrLine) { + console.log('Stderr output: ' + stderrLine); + }) + .on('end', function() { + console.log('Processing finished !') + //res.end(); + // var readStream = fs.createReadStream(path) + // // This will wait until we know the readable stream is actually valid before piping + // readStream.on('open', function () { + // // This just pipes the read stream to the response object (which goes to the client) + // readStream.pipe(res); + // }); + + // // This catches any errors that happen while creating the readable stream (usually invalid names) + // readStream.on('error', function(err) { + // res.end(err); + // }); + + // readStream.on('end', function(err) { + // fs.unlink(path, function(){}) + // res.end(); + // }); + }) + .pipe(res, { end: true }) + //.save(path) + } + + }) +} + +module.exports = reEncode From a61928db33c8dc17c297bee2a99f09d5b8d7120f Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Sun, 22 Sep 2019 17:34:08 -0700 Subject: [PATCH 11/21] updated lodash, added some more tweaks to reEncode --- package.json | 2 +- src/reEncode.js | 136 +++++++++++++++++++++++++----------------------- 2 files changed, 72 insertions(+), 66 deletions(-) diff --git a/package.json b/package.json index 00ddb92c..fc209612 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "fluent-ffmpeg": "^2.1.2", "gif2webp-bin": "^2.0.0", "is-animated": "^1.1.1", - "lodash": "^4.17.11", + "lodash": "^4.17.15", "request": "^2.88.0", "sharp": "^0.23.0" }, diff --git a/src/reEncode.js b/src/reEncode.js index c49103d8..45d1b398 100644 --- a/src/reEncode.js +++ b/src/reEncode.js @@ -11,34 +11,38 @@ ffmpeg.setFfprobePath(ffprobePath); function reEncode(req, res, input) { var quality = req.params.quality; - var bitrateTarget = quality * 15; + var vBitrateTarget = quality * 10; //200 - 800 + var aBitrateTarget = quality * 2; ffmpeg.ffprobe(req.params.url, function(err, metadata) { - let audioStreamInfo, videoStreamInfo, format, audioOnly - format = metadata.format - - for (let stream in metadata.streams){ - stream = metadata.streams[stream] - if (videoStreamInfo && audioStreamInfo){ - console.log("multiple audio or video streams detected") + let audioStreamInfo, videoStreamInfo, audioOnly + //format = metadata.format + if(err || !metadata){ + return redirect(req, res) + } + if(metadata){ + for (let stream in metadata.streams){ + stream = metadata.streams[stream] + if (videoStreamInfo && audioStreamInfo){ + console.log("multiple audio or video streams detected") + } + //console.log(stream) + switch(stream.codec_type){ + case("video"): + videoStreamInfo = stream + break + case("audio"): + audioStreamInfo = stream + break + } } - //console.log(stream) - switch(stream.codec_type){ - case("video"): - videoStreamInfo = stream - break - case("audio"): - audioStreamInfo = stream - break + audioOnly = !videoStreamInfo; + + if((!audioStreamInfo && !videoStreamInfo) || (audioStreamInfo && audioStreamInfo.bit_rate <= aBitrateTarget * 1000 && audioOnly) || videoStreamInfo && ( + videoStreamInfo.bit_rate <= vBitrateTarget * 1000 || videoStreamInfo.duration > 240)){ + return redirect(req, res) } } - audioOnly = !videoStreamInfo; - - if((audioStreamInfo < 50000 && audioOnly) || videoStreamInfo && ( - videoStreamInfo.bit_rate < bitrateTarget * 1000 || videoStreamInfo.duration > 240)){ - return redirect(req, res) - } - res.setHeader('content-type', `${audioOnly ? "audio":"video"}/webm`) //let {hostname, pathname} = new URL(req.params.url) @@ -51,49 +55,51 @@ function reEncode(req, res, input) { .audioBitrate(quality * 2) .pipe(res, { end: true }) }else{ - ffmpeg(req.params.url) - .videoCodec("libvpx-vp9")//videoStreamInfo.codec_name) - .videoBitrate(bitrateTarget) //300 - 1200 - .audioCodec("opus")//audioStreamInfo.codec_name) - //.audioQuality(Math.ceil(quality / 20)) //1-4 - .audioBitrate(quality * 2) - .size( - //480p cap - '?x' + Math.min(480, videoStreamInfo ? videoStreamInfo.height : 0) - ) - //.format(format.format_name.split(',')[0]) - .format('webm') - .outputOptions(["-deadline realtime","-cpu-used 5"]) - //.outputOptions("-movflags +frag_keyframe") - .on('error', function(err) { - console.log('An error occurred: ' + err.message) - console.error(err); - }) - .on('stderr', function(stderrLine) { - console.log('Stderr output: ' + stderrLine); - }) - .on('end', function() { - console.log('Processing finished !') - //res.end(); - // var readStream = fs.createReadStream(path) - // // This will wait until we know the readable stream is actually valid before piping - // readStream.on('open', function () { - // // This just pipes the read stream to the response object (which goes to the client) - // readStream.pipe(res); - // }); + ffmpeg(req.params.url) + .videoCodec("libvpx-vp9")//videoStreamInfo.codec_name) + .videoBitrate(vBitrateTarget) + .audioCodec("opus")//audioStreamInfo.codec_name) + //.audioQuality(Math.ceil(quality / 20)) //1-4 + .audioBitrate(aBitrateTarget) + .size( + //480p cap + '?x' + Math.min(480, videoStreamInfo ? videoStreamInfo.height : 0) + ) + //.format(format.format_name.split(',')[0]) + .format('webm') + .outputOptions(["-deadline realtime","-cpu-used 7"]) + //.outputOptions("-movflags +frag_keyframe") + .on('error', function(err) { + console.log('An error occurred: ' + err.message) + console.error(err); + return redirect(req, res) + }) + .on('stderr', function(stderrLine) { + console.log('Stderr output: ' + stderrLine); + }) + .on('end', function() { + console.log('Processing finished !') - // // This catches any errors that happen while creating the readable stream (usually invalid names) - // readStream.on('error', function(err) { - // res.end(err); - // }); - - // readStream.on('end', function(err) { - // fs.unlink(path, function(){}) - // res.end(); - // }); - }) - .pipe(res, { end: true }) - //.save(path) + //res.end(); + // var readStream = fs.createReadStream(path) + // // This will wait until we know the readable stream is actually valid before piping + // readStream.on('open', function () { + // // This just pipes the read stream to the response object (which goes to the client) + // readStream.pipe(res); + // }); + + // // This catches any errors that happen while creating the readable stream (usually invalid names) + // readStream.on('error', function(err) { + // res.end(err); + // }); + + // readStream.on('end', function(err) { + // fs.unlink(path, function(){}) + // res.end(); + // }); + }) + .pipe(res, { end: true }) + //.save(path) } }) From 769d3eed0435ee96625990a4ef209b8223192671 Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Sun, 22 Sep 2019 19:53:00 -0700 Subject: [PATCH 12/21] reduce video resolution 480p => 360p --- src/reEncode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reEncode.js b/src/reEncode.js index 45d1b398..b74d40fb 100644 --- a/src/reEncode.js +++ b/src/reEncode.js @@ -63,7 +63,7 @@ function reEncode(req, res, input) { .audioBitrate(aBitrateTarget) .size( //480p cap - '?x' + Math.min(480, videoStreamInfo ? videoStreamInfo.height : 0) + '?x' + Math.min(360, videoStreamInfo ? videoStreamInfo.height : 0) ) //.format(format.format_name.split(',')[0]) .format('webm') From d6807b46924848e2d7c1881708c15590e1be2c50 Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Sun, 22 Sep 2019 20:09:37 -0700 Subject: [PATCH 13/21] set -cpu-used flag to 8 and set the duration cap to 90 --- src/reEncode.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/reEncode.js b/src/reEncode.js index b74d40fb..05af36ca 100644 --- a/src/reEncode.js +++ b/src/reEncode.js @@ -38,8 +38,8 @@ function reEncode(req, res, input) { } audioOnly = !videoStreamInfo; - if((!audioStreamInfo && !videoStreamInfo) || (audioStreamInfo && audioStreamInfo.bit_rate <= aBitrateTarget * 1000 && audioOnly) || videoStreamInfo && ( - videoStreamInfo.bit_rate <= vBitrateTarget * 1000 || videoStreamInfo.duration > 240)){ + if((!audioStreamInfo && !videoStreamInfo) || (audioStreamInfo && audioStreamInfo.bit_rate <= aBitrateTarget * 800 && audioOnly) || videoStreamInfo && ( + videoStreamInfo.bit_rate <= vBitrateTarget * 800 || videoStreamInfo.duration > 90)){ return redirect(req, res) } } @@ -67,7 +67,7 @@ function reEncode(req, res, input) { ) //.format(format.format_name.split(',')[0]) .format('webm') - .outputOptions(["-deadline realtime","-cpu-used 7"]) + .outputOptions(["-deadline realtime","-cpu-used 8"]) //.outputOptions("-movflags +frag_keyframe") .on('error', function(err) { console.log('An error occurred: ' + err.message) From 74bc1c7cc0dca264d132b3e65135b4aca34c722e Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Sun, 22 Sep 2019 20:21:32 -0700 Subject: [PATCH 14/21] only handle videos 30s or less --- src/reEncode.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reEncode.js b/src/reEncode.js index 05af36ca..e9141b6a 100644 --- a/src/reEncode.js +++ b/src/reEncode.js @@ -39,7 +39,7 @@ function reEncode(req, res, input) { audioOnly = !videoStreamInfo; if((!audioStreamInfo && !videoStreamInfo) || (audioStreamInfo && audioStreamInfo.bit_rate <= aBitrateTarget * 800 && audioOnly) || videoStreamInfo && ( - videoStreamInfo.bit_rate <= vBitrateTarget * 800 || videoStreamInfo.duration > 90)){ + videoStreamInfo.bit_rate <= vBitrateTarget * 800 || videoStreamInfo.duration > 30)){ return redirect(req, res) } } @@ -67,7 +67,7 @@ function reEncode(req, res, input) { ) //.format(format.format_name.split(',')[0]) .format('webm') - .outputOptions(["-deadline realtime","-cpu-used 8"]) + .outputOptions(["-deadline realtime","-cpu-used 7"]) //.outputOptions("-movflags +frag_keyframe") .on('error', function(err) { console.log('An error occurred: ' + err.message) From 2e0b014e3a0d89c86781f799a5341b4d6e444006 Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Sun, 22 Sep 2019 20:25:25 -0700 Subject: [PATCH 15/21] add stderr log to audio-only --- src/reEncode.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/reEncode.js b/src/reEncode.js index e9141b6a..ccb45aec 100644 --- a/src/reEncode.js +++ b/src/reEncode.js @@ -52,7 +52,10 @@ function reEncode(req, res, input) { ffmpeg(req.params.url) .audioCodec("opus") .format("webm") - .audioBitrate(quality * 2) + .audioBitrate(aBitrateTarget) + .on('stderr', function(stderrLine) { + console.log('Stderr output: ' + stderrLine); + }) .pipe(res, { end: true }) }else{ ffmpeg(req.params.url) From a39e4dc50e82a88b2c4d2736930669888a7468f7 Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Mon, 23 Sep 2019 18:14:07 -0700 Subject: [PATCH 16/21] set the video duration cap to 10 mins --- src/reEncode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reEncode.js b/src/reEncode.js index ccb45aec..ae703263 100644 --- a/src/reEncode.js +++ b/src/reEncode.js @@ -39,7 +39,7 @@ function reEncode(req, res, input) { audioOnly = !videoStreamInfo; if((!audioStreamInfo && !videoStreamInfo) || (audioStreamInfo && audioStreamInfo.bit_rate <= aBitrateTarget * 800 && audioOnly) || videoStreamInfo && ( - videoStreamInfo.bit_rate <= vBitrateTarget * 800 || videoStreamInfo.duration > 30)){ + videoStreamInfo.bit_rate <= vBitrateTarget * 800 || videoStreamInfo.duration > 600)){ return redirect(req, res) } } From f4e1cafc66a0b6444a8c5dc5d35dac6a4b1d34a9 Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Mon, 30 Sep 2019 22:59:37 -0700 Subject: [PATCH 17/21] moved reEncode functionality into Request's Response emitter to allow for internet radio streaming and live streams --- src/proxy.js | 37 +++++++++++++++++++++++++------------ src/reEncode.js | 27 +++++++++++++++++++-------- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/proxy.js b/src/proxy.js index 60370937..4a005a98 100644 --- a/src/proxy.js +++ b/src/proxy.js @@ -8,6 +8,7 @@ const bypass = require('./bypass') const copyHeaders = require('./copyHeaders') function proxy(req, res) { + var isMediaStream request.get( req.params.url, { @@ -27,21 +28,33 @@ function proxy(req, res) { (err, origin, buffer) => { if (err || origin.statusCode >= 400) return redirect(req, res) - copyHeaders(origin, res) - res.setHeader('content-encoding', 'identity') - let originType = origin.headers['content-type'] || '' - req.params.originType = originType - req.params.originSize = buffer.length + if(!isMediaStream){ + copyHeaders(origin, res) + res.setHeader('content-encoding', 'identity') + let originType = origin.headers['content-type'] || '' + req.params.originType = originType + req.params.originSize = buffer.length - if (shouldCompress(req, buffer)) { - compress(req, res, buffer) - } else if (originType.startsWith('video') || originType.startsWith('audio')){ - reEncode(req, res, buffer) - }else { - bypass(req, res, buffer) + if (shouldCompress(req, buffer)) { + isMediaStream = false + compress(req, res, buffer) + } else if (originType.startsWith('video') || originType.startsWith('audio')){ + // reEncode(req, res, buffer) + } else { + bypass(req, res, buffer) + } } } - ) + ).on('response', function(response) { + let originType = response.headers['content-type'] || '' + if (originType.startsWith('video') || originType.startsWith('audio')){ + isMediaStream = true + reEncode(req, res) + } + }).on('error', function(err) { + console.error(err) + return redirect(req, res) + }) } module.exports = proxy diff --git a/src/reEncode.js b/src/reEncode.js index ae703263..775955e9 100644 --- a/src/reEncode.js +++ b/src/reEncode.js @@ -10,14 +10,16 @@ ffmpeg.setFfmpegPath(ffmpegPath); ffmpeg.setFfprobePath(ffprobePath); function reEncode(req, res, input) { - var quality = req.params.quality; - var vBitrateTarget = quality * 10; //200 - 800 - var aBitrateTarget = quality * 2; + var quality = req.params.quality + var vBitrateTarget = quality * 10 //200 - 800 + var aBitrateTarget = quality * 2 + var timeoutSeconds = 7200 //2 hours ffmpeg.ffprobe(req.params.url, function(err, metadata) { let audioStreamInfo, videoStreamInfo, audioOnly //format = metadata.format if(err || !metadata){ + console.error(err) return redirect(req, res) } if(metadata){ @@ -49,16 +51,26 @@ function reEncode(req, res, input) { //let path = `${os.tmpdir()}/${hostname + encodeURIComponent(pathname)}.webm`; if(audioOnly){ - ffmpeg(req.params.url) + ffmpeg({ + source: req.params.url, + timeout: timeoutSeconds + }) .audioCodec("opus") .format("webm") .audioBitrate(aBitrateTarget) + .on('error', function(err) { + console.error('An error occurred: ' + err.message) + return redirect(req, res) + }) .on('stderr', function(stderrLine) { console.log('Stderr output: ' + stderrLine); }) .pipe(res, { end: true }) }else{ - ffmpeg(req.params.url) + ffmpeg({ + source: req.params.url, + timeout: timeoutSeconds + }) .videoCodec("libvpx-vp9")//videoStreamInfo.codec_name) .videoBitrate(vBitrateTarget) .audioCodec("opus")//audioStreamInfo.codec_name) @@ -73,12 +85,11 @@ function reEncode(req, res, input) { .outputOptions(["-deadline realtime","-cpu-used 7"]) //.outputOptions("-movflags +frag_keyframe") .on('error', function(err) { - console.log('An error occurred: ' + err.message) - console.error(err); + console.error('An error occurred: ' + err.message) return redirect(req, res) }) .on('stderr', function(stderrLine) { - console.log('Stderr output: ' + stderrLine); + console.log('Stderr output: ' + stderrLine) }) .on('end', function() { console.log('Processing finished !') From 5ee8725ca1de9e5ba5a27432cad50e7ff30e370d Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Sat, 5 Oct 2019 12:40:23 -0700 Subject: [PATCH 18/21] exposed some of the new functionality variables as environment variables --- README.md | 8 +++++++- src/compress.js | 2 +- src/reEncode.js | 17 +++++++++++------ src/shouldCompress.js | 2 +- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ad29bcde..7aaf94c2 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,13 @@ You can deploy this service to Heroku: [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/ayastreb/bandwidth-hero-proxy) ENVIRONMENT_VARIABLES -`MIN_COMPRESS_LENGTH=2048` (minimum byte length for an image to be compressible; default 2048 ~2kB) +`MIN_COMPRESS_LENGTH=2048` Default=2048 (minimum byte length for an image to be compressible; default 2048 ~2kB) +`DISABLE_ANIMATED=1` (Disable small apng passthrough and animated GIF to animated webp conversion (uses temp dir; Can also be "true") +`VIDEO_QUALITY_MULTIPLIER` Default=10 (The Integer to multiply the 20-80 Quality value (l param) by to get the target video bitrate in kbps. For example, if multiplier is set to 10. Low(20) sets a target bitrate of 200kbps) +`AUDIO_QUALITY_MULTIPLIER` Default=2 (The Integer to multiply the 20-80 Quality value (l param) by to get the target video bitrate in kbps. For example, if multiplier is set to 2. Low(20) sets a target bitrate of 40kbps) +`MEDIA_TIMEOUT` Default=7200 (Set the timeout in seconds for outputted Audio and Video streams) +`VIDEO_HEIGHT_THRES` Default=360 (Set the maximum video height threshold in Pixels. 480 becomes 480p, etc) +`VIDEO_WEBM_CPU_USED` Default=7 (Read more about this [here](https://trac.ffmpeg.org/wiki/Encode/VP9#CPUUtilizationSpeed)) [![Deploy to Heroku guide](http://img.youtube.com/vi/y3tkYEXAics/0.jpg)](http://www.youtube.com/watch?v=y3tkYEXAics) diff --git a/src/compress.js b/src/compress.js index 347e7416..fadf7658 100644 --- a/src/compress.js +++ b/src/compress.js @@ -11,7 +11,7 @@ function compress(req, res, input) { const format = req.params.webp ? 'webp' : 'jpeg' const originType = req.params.originType - if(!req.params.grayscale && format === 'webp' && originType.endsWith('gif') && isAnimated(input)){ + if(!process.env.DISABLE_ANIMATED && !req.params.grayscale && format === 'webp' && originType.endsWith('gif') && isAnimated(input)){ let {hostname, pathname} = new URL(req.params.url) let path = `${os.tmpdir()}/${hostname + encodeURIComponent(pathname)}`; diff --git a/src/reEncode.js b/src/reEncode.js index 775955e9..63ff927f 100644 --- a/src/reEncode.js +++ b/src/reEncode.js @@ -9,11 +9,17 @@ const {URL} = require('url') ffmpeg.setFfmpegPath(ffmpegPath); ffmpeg.setFfprobePath(ffprobePath); +const VIDEO_QUALITY_MULTIPLIER = parseInt(process.env.VIDEO_QUALITY_MULTIPLIER) || 10 +const AUDIO_QUALITY_MULTIPLIER = parseInt(process.env.AUDIO_QUALITY_MULTIPLIER) || 2 +const MEDIA_TIMEOUT = parseInt(process.env.MEDIA_TIMEOUT) || 7200 +const VIDEO_HEIGHT_THRESH = parseInt(process.env.VIDEO_HEIGHT_THRESH) || 360 +const VIDEO_WEBM_CPU_USED = parseInt(process.env.VIDEO_WEBM_CPU_USED) || 7 + function reEncode(req, res, input) { var quality = req.params.quality - var vBitrateTarget = quality * 10 //200 - 800 - var aBitrateTarget = quality * 2 - var timeoutSeconds = 7200 //2 hours + var vBitrateTarget = quality * VIDEO_QUALITY_MULTIPLIER //200 - 800 + var aBitrateTarget = quality * AUDIO_QUALITY_MULTIPLIER + var timeoutSeconds = MEDIA_TIMEOUT //2 hours ffmpeg.ffprobe(req.params.url, function(err, metadata) { let audioStreamInfo, videoStreamInfo, audioOnly @@ -77,12 +83,11 @@ function reEncode(req, res, input) { //.audioQuality(Math.ceil(quality / 20)) //1-4 .audioBitrate(aBitrateTarget) .size( - //480p cap - '?x' + Math.min(360, videoStreamInfo ? videoStreamInfo.height : 0) + '?x' + Math.min(VIDEO_HEIGHT_THRESH, videoStreamInfo ? videoStreamInfo.height : 0) ) //.format(format.format_name.split(',')[0]) .format('webm') - .outputOptions(["-deadline realtime","-cpu-used 7"]) + .outputOptions(["-deadline realtime",`-cpu-used ${VIDEO_WEBM_CPU_USED}`]) //.outputOptions("-movflags +frag_keyframe") .on('error', function(err) { console.error('An error occurred: ' + err.message) diff --git a/src/shouldCompress.js b/src/shouldCompress.js index c59f17e3..1ad973a3 100644 --- a/src/shouldCompress.js +++ b/src/shouldCompress.js @@ -18,7 +18,7 @@ function shouldCompress(req, buffer) { return false } - if((originType.endsWith('png')) && isAnimated(buffer) && originSize < APNG_THRESH_LENGTH){ + if(!process.env.DISABLE_ANIMATED && (originType.endsWith('png')) && isAnimated(buffer) && originSize < APNG_THRESH_LENGTH){ //It's an animated png file, let it pass through through if small enough return false } From 9b69834e99acf10aa257cfbfccef77f881f89e6e Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Sat, 5 Oct 2019 12:45:06 -0700 Subject: [PATCH 19/21] Update README.md Cleanup Environment Variables section --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7aaf94c2..cdb83535 100644 --- a/README.md +++ b/README.md @@ -15,21 +15,21 @@ and user's IP address through to the origin host. ## Deployment +####ENVIRONMENT_VARIABLES +`MIN_COMPRESS_LENGTH=2048` Default=2048 (minimum byte length for an image to be compressible; default 2048 ~2kB) \ +`DISABLE_ANIMATED=1` (Disable small apng passthrough and animated GIF to animated webp conversion (uses temp dir; Can also be "true") \ +`VIDEO_QUALITY_MULTIPLIER` Default=10 (The Integer to multiply the 20-80 Quality value (l param) by to get the target video bitrate in kbps. For example, if multiplier is set to 10. Low(20) sets a target bitrate of 200kbps) \ +`AUDIO_QUALITY_MULTIPLIER` Default=2 (The Integer to multiply the 20-80 Quality value (l param) by to get the target video bitrate in kbps. For example, if multiplier is set to 2. Low(20) sets a target bitrate of 40kbps) \ +`MEDIA_TIMEOUT` Default=7200 (Set the timeout in seconds for outputted Audio and Video streams) \ +`VIDEO_HEIGHT_THRES` Default=360 (Set the maximum video height threshold in Pixels. 480 becomes 480p, etc) \ +`VIDEO_WEBM_CPU_USED` Default=7 (Read more about this [here](https://trac.ffmpeg.org/wiki/Encode/VP9#CPUUtilizationSpeed)) \ + ### Heroku You can deploy this service to Heroku: [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/ayastreb/bandwidth-hero-proxy) -ENVIRONMENT_VARIABLES -`MIN_COMPRESS_LENGTH=2048` Default=2048 (minimum byte length for an image to be compressible; default 2048 ~2kB) -`DISABLE_ANIMATED=1` (Disable small apng passthrough and animated GIF to animated webp conversion (uses temp dir; Can also be "true") -`VIDEO_QUALITY_MULTIPLIER` Default=10 (The Integer to multiply the 20-80 Quality value (l param) by to get the target video bitrate in kbps. For example, if multiplier is set to 10. Low(20) sets a target bitrate of 200kbps) -`AUDIO_QUALITY_MULTIPLIER` Default=2 (The Integer to multiply the 20-80 Quality value (l param) by to get the target video bitrate in kbps. For example, if multiplier is set to 2. Low(20) sets a target bitrate of 40kbps) -`MEDIA_TIMEOUT` Default=7200 (Set the timeout in seconds for outputted Audio and Video streams) -`VIDEO_HEIGHT_THRES` Default=360 (Set the maximum video height threshold in Pixels. 480 becomes 480p, etc) -`VIDEO_WEBM_CPU_USED` Default=7 (Read more about this [here](https://trac.ffmpeg.org/wiki/Encode/VP9#CPUUtilizationSpeed)) - [![Deploy to Heroku guide](http://img.youtube.com/vi/y3tkYEXAics/0.jpg)](http://www.youtube.com/watch?v=y3tkYEXAics) ### Self-hosted From bb06efa9534f3c834501fdff7372756a1eac7e74 Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Sat, 5 Oct 2019 12:46:37 -0700 Subject: [PATCH 20/21] Update README.md --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cdb83535..6d65dcc9 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,14 @@ and user's IP address through to the origin host. ## Deployment -####ENVIRONMENT_VARIABLES -`MIN_COMPRESS_LENGTH=2048` Default=2048 (minimum byte length for an image to be compressible; default 2048 ~2kB) \ -`DISABLE_ANIMATED=1` (Disable small apng passthrough and animated GIF to animated webp conversion (uses temp dir; Can also be "true") \ -`VIDEO_QUALITY_MULTIPLIER` Default=10 (The Integer to multiply the 20-80 Quality value (l param) by to get the target video bitrate in kbps. For example, if multiplier is set to 10. Low(20) sets a target bitrate of 200kbps) \ -`AUDIO_QUALITY_MULTIPLIER` Default=2 (The Integer to multiply the 20-80 Quality value (l param) by to get the target video bitrate in kbps. For example, if multiplier is set to 2. Low(20) sets a target bitrate of 40kbps) \ -`MEDIA_TIMEOUT` Default=7200 (Set the timeout in seconds for outputted Audio and Video streams) \ -`VIDEO_HEIGHT_THRES` Default=360 (Set the maximum video height threshold in Pixels. 480 becomes 480p, etc) \ -`VIDEO_WEBM_CPU_USED` Default=7 (Read more about this [here](https://trac.ffmpeg.org/wiki/Encode/VP9#CPUUtilizationSpeed)) \ +#### ENVIRONMENT_VARIABLES +`MIN_COMPRESS_LENGTH=2048` Default=2048 (minimum byte length for an image to be compressible; default 2048 ~2kB)
+`DISABLE_ANIMATED=1` (Disable small apng passthrough and animated GIF to animated webp conversion (uses temp dir; Can also be "true")
+`VIDEO_QUALITY_MULTIPLIER` Default=10 (The Integer to multiply the 20-80 Quality value (l param) by to get the target video bitrate in kbps. For example, if multiplier is set to 10. Low(20) sets a target bitrate of 200kbps)
+`AUDIO_QUALITY_MULTIPLIER` Default=2 (The Integer to multiply the 20-80 Quality value (l param) by to get the target video bitrate in kbps. For example, if multiplier is set to 2. Low(20) sets a target bitrate of 40kbps)
+`MEDIA_TIMEOUT` Default=7200 (Set the timeout in seconds for outputted Audio and Video streams)
+`VIDEO_HEIGHT_THRES` Default=360 (Set the maximum video height threshold in Pixels. 480 becomes 480p, etc)
+`VIDEO_WEBM_CPU_USED` Default=7 (Read more about this [here](https://trac.ffmpeg.org/wiki/Encode/VP9#CPUUtilizationSpeed)) ### Heroku From ff084c99f0185a3bc633b77a6a97e9523ae5e7af Mon Sep 17 00:00:00 2001 From: Travis Juntara Date: Sat, 5 Oct 2019 12:48:04 -0700 Subject: [PATCH 21/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d65dcc9..524b470b 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ and user's IP address through to the origin host. `AUDIO_QUALITY_MULTIPLIER` Default=2 (The Integer to multiply the 20-80 Quality value (l param) by to get the target video bitrate in kbps. For example, if multiplier is set to 2. Low(20) sets a target bitrate of 40kbps)
`MEDIA_TIMEOUT` Default=7200 (Set the timeout in seconds for outputted Audio and Video streams)
`VIDEO_HEIGHT_THRES` Default=360 (Set the maximum video height threshold in Pixels. 480 becomes 480p, etc)
-`VIDEO_WEBM_CPU_USED` Default=7 (Read more about this [here](https://trac.ffmpeg.org/wiki/Encode/VP9#CPUUtilizationSpeed)) +`VIDEO_WEBM_CPU_USED` Default=7 (sets -cpu-used flag in the libvpx-vp9 video encoder. Current valid values at time of writing are (-8 thru 8) Read more about this [here](https://trac.ffmpeg.org/wiki/Encode/VP9#CPUUtilizationSpeed)) ### Heroku