├── .babelrc.js ├── .gitignore ├── .prettierignore ├── README.MD ├── commitlint.config.js ├── dist ├── es │ ├── FFMPEGWebWorker.js │ ├── FFMPEGWebWorkerClient.js │ └── index.js └── index.js ├── docs ├── build │ └── bundle.f3381c3c.js └── index.html ├── examples └── theme.css ├── package.json ├── rollup.config.js ├── src ├── FFMPEGWebWorker.js ├── FFMPEGWebWorkerClient.js └── index.js ├── styleguide.config.js ├── testSetup.js ├── typings └── ffmpeg-webworker.d.ts └── yarn.lock /.babelrc.js: -------------------------------------------------------------------------------- 1 | // https://babeljs.io/docs/en/configuration 2 | const presets = ["@babel/preset-react"]; 3 | if (process.env["BABEL_ENV"] === "es") { 4 | presets.unshift(["@babel/preset-env", { modules: false }]); 5 | } else { 6 | presets.unshift("@babel/preset-env"); 7 | } 8 | 9 | const plugins = [ 10 | "@babel/plugin-proposal-export-default-from", 11 | "@babel/plugin-proposal-logical-assignment-operators", 12 | ["@babel/plugin-proposal-optional-chaining", { loose: false }], 13 | ["@babel/plugin-proposal-pipeline-operator", { proposal: "minimal" }], 14 | ["@babel/plugin-proposal-nullish-coalescing-operator", { loose: false }], 15 | "@babel/plugin-proposal-class-properties", 16 | "@babel/plugin-proposal-do-expressions", 17 | "add-module-exports" 18 | ]; 19 | 20 | module.exports = { 21 | presets, 22 | plugins, 23 | env: { 24 | test: { 25 | presets, 26 | plugins: [ 27 | ...plugins, 28 | "@babel/plugin-transform-runtime", 29 | "dynamic-import-node" 30 | ] 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | # typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | docs/ 3 | node_modules/ -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # ffmpeg-webworker 2 | 3 | A WebWorker implementation that eases the use of ffmpeg library in the browser. 4 | 5 | This builds upon an existing work geniusly done by the folks at 6 | [Ffmpeg.js](https://github.com/muaz-khan/Ffmpeg.js/) and 7 | [videoconverter.js](https://github.com/bgrins/videoconverter.js) 8 | 9 | ## Demo 10 | 11 | [See it here](https://limistah.github.io/ffmpeg-webworker) 12 | 13 | ## Installation 14 | 15 | ```bash 16 | npm install --save ffmpeg-webworker 17 | ``` 18 | 19 | or: 20 | 21 | ```bash 22 | yarn add ffmpeg-webworker 23 | ``` 24 | 25 | ## Usage 26 | 27 | ```js 28 | import workerClient from "ffmpeg-webworker"; 29 | import React from "react"; 30 | 31 | class FFMPEG extends React.Component { 32 | constructor(props) { 33 | super(props); 34 | this.state = { 35 | stdOutputText: "", 36 | ffmpegReady: false 37 | }; 38 | 39 | this.updateStdOutputText = this.updateStdOutputText.bind(this); 40 | this.handleInputChange = this.handleInputChange.bind(this); 41 | this.handleListCodecs = this.handleListCodecs.bind(this); 42 | } 43 | 44 | componentWillMount() { 45 | workerClient.on("onReady", () => this.setState({ ffmpegReady: true })); 46 | workerClient.on("onStdout", msg => this.updateStdOutputText(msg)); 47 | workerClient.on("onFileReceived", msg => this.updateStdOutputText(msg)); 48 | workerClient.on("onDone", data => { 49 | this.updateStdOutputText("Command Completed, check the console"); 50 | console.log(data); 51 | }); 52 | } 53 | 54 | updateStdOutputText(text) { 55 | this.setState({ 56 | stdOutputText: `${this.state.stdOutputText} \n\n ${text}` 57 | }); 58 | } 59 | 60 | handleInputChange(e) { 61 | this.setState({ stdOutputText: "" }); 62 | 63 | const file = e.currentTarget.files[0]; 64 | // Set the file for processing 65 | workerClient.inputFile = file; 66 | // Run a valid ffmpeg command 67 | workerClient.runCommand("-ss 00:00:05 -c copy -t 12 sliced-output.mp4"); 68 | } 69 | 70 | Input({ onChange }) { 71 | return ( 72 | onChange(e)} /> 73 | ); 74 | } 75 | 76 | StdOutput({ text, ffmpegReady }) { 77 | return ( 78 |
 79 |         {ffmpegReady ? "FFMPEG is ready" : "Loading FFMPEG"}
 80 |         {text}
 81 |       
82 | ); 83 | } 84 | 85 | handleListCodecs(e) { 86 | e.preventDefault(); 87 | this.setState({ stdOutputText: "" }); 88 | // Run a valid ffmpeg command 89 | workerClient.runCommand("-codecs"); 90 | } 91 | 92 | render() { 93 | return ( 94 |
95 | {} 96 | 97 | 101 |
102 | ); 103 | } 104 | } 105 | 106 | ; 107 | ``` 108 | 109 | ## Docs 110 | 111 | The default export from the library is a standard NodeJS event emitter client 112 | would listen to and dispatch events based on interactions with an already loaded 113 | ffmpeg webworker Javascript library inside of a webworker. 114 | 115 | It supports the following properties: 116 | 117 | ### workerClient.on : void 118 | 119 | Receives an event from the ffmpeg webworker. Below are the supported commands: 120 | 121 | - _onReady_: The webworker has loaded ffmpeg successfully. 122 | - _onStdout_: Listens to standard ffmpeg-js message outputs. 123 | - _onStart_: Command has been received and started. 124 | - _onDone_: Command has been completed. Receives the data from ffmpeg as the 125 | first parameter. 126 | - _onFileReceived_: Input file has been set on the client. 127 | 128 | ### workerClient.inputFile : File 129 | 130 | The file that ffmpeg-webworker would be working with on issue of command. 131 | 132 | ### workerClient.worker : WebWorker 133 | 134 | An instance of the web worker that has ffmpeg-js loaded in it. 135 | 136 | ### workerClient.readFileAsBufferArray (file: File) : ArrayBuffer 137 | 138 | Converts the passed file to a file buffer array. 139 | 140 | ### workerClient.inputFileExists : boolean 141 | 142 | Detects if the inputFile has been passed in correctly. 143 | 144 | ### workerClient.convertInputFileToArrayBuffer (file: File) : ArrayBuffer 145 | 146 | Converts already set input file in the library to Buffer Array for processing 147 | 148 | ### workerClient.runCommand (command: String, totalMemory = 33554432: Number) : void 149 | 150 | Accepts a valid ffmpeg command to be run against the input file 151 | 152 | ### workerClient.log (message: String [, String []]) : void 153 | 154 | Logs messages to standard console. 155 | 156 | This is the default exported property of the module. It ease the interaction 157 | with the already initialized and loaded ffmpeg webworker Javascript library. 158 | 159 | Other exported modules are: 160 | 161 | ### FFMPEGWebworker 162 | 163 | The official web worker implementation that makes easier the loading of 164 | ffmpeg-js and running command in a worker. 165 | 166 | ### FFMPEGWebworkerClient 167 | 168 | Official listener for FFMPEGWebworker. 169 | 170 | ```js static 171 | import { FFMPEGWebworkerClient } from "ffmpeg-webworker"; 172 | 173 | const webworkerClient = new FFMPEGWebworkerClient(); 174 | 175 | // The above is same as 176 | // import webworkerClient from "ffmpeg-webworker"; 177 | 178 | webworkerClient.on("ready", () => { 179 | console.log("FFMPEG has been loaded, and can receive commands"); 180 | }); 181 | ``` 182 | 183 | ## Release Notes 184 | 185 | 1.0.0 186 | 187 | - Supports all ffmpeg codecs 188 | 189 | Initial Release: 190 | 191 | - Setup for the library 192 | - Only supports lib H246 193 | 194 | ## Contributions 195 | 196 | Github issues can be used to log bug reports, suggestions and improvements 197 | 198 | You can fork, then send a pull request. 199 | 200 | Thank you... 201 | 202 | ## License 203 | 204 | MIT 205 | 206 | ## Credits 207 | 208 | This library has been made possible with the awesome work by folks at 209 | [Ffmpeg.js](https://github.com/muaz-khan/Ffmpeg.js/) 210 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/label-has-associated-control */ 2 | // eslint-disable-next-line quotes 3 | module.exports = { extends: ["@commitlint/config-angular"] }; 4 | -------------------------------------------------------------------------------- /dist/es/FFMPEGWebWorker.js: -------------------------------------------------------------------------------- 1 | var workerFile = function workerFile() { 2 | var workerPath = "https://cdn.rawgit.com/bgrins/videoconverter.js/master/build/ffmpeg-all-codecs.js"; 3 | importScripts(workerPath); 4 | var now = Date.now; 5 | 6 | function print(text) { 7 | postMessage({ 8 | type: "stdout", 9 | data: text 10 | }); 11 | } 12 | 13 | onmessage = function onmessage(event) { 14 | var message = event.data; 15 | 16 | if (message.type === "command") { 17 | var Module = { 18 | print: print, 19 | printErr: print, 20 | files: message.files || [], 21 | arguments: message.arguments || [], 22 | TOTAL_MEMORY: message.totalMemory || 33554432 23 | }; 24 | postMessage({ 25 | type: "start", 26 | data: Module.arguments.join(" ") 27 | }); 28 | postMessage({ 29 | type: "stdout", 30 | data: "Received command: " + Module.arguments.join(" ") + (Module.TOTAL_MEMORY ? ". Processing with " + Module.TOTAL_MEMORY + " bits." : "") 31 | }); 32 | var time = now(); 33 | var result = ffmpeg_run(Module); 34 | var totalTime = now() - time; 35 | postMessage({ 36 | type: "stdout", 37 | data: "Finished processing (took " + totalTime + "ms)" 38 | }); 39 | postMessage({ 40 | type: "done", 41 | data: result, 42 | time: totalTime 43 | }); 44 | } 45 | }; 46 | 47 | postMessage({ 48 | type: "ready" 49 | }); 50 | }; 51 | 52 | export default workerFile; -------------------------------------------------------------------------------- /dist/es/FFMPEGWebWorkerClient.js: -------------------------------------------------------------------------------- 1 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 2 | 3 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 4 | 5 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 6 | 7 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 8 | 9 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 10 | 11 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 12 | 13 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 14 | 15 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 16 | 17 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 18 | 19 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 20 | 21 | import WorkerFile from "webworker-file"; 22 | import workerFile from "./FFMPEGWebWorker"; 23 | import { EventEmitter } from "events"; 24 | 25 | var FFMPEGWebworkerClient = 26 | /*#__PURE__*/ 27 | function (_EventEmitter) { 28 | _inherits(FFMPEGWebworkerClient, _EventEmitter); 29 | 30 | /** 31 | * @type {Worker} 32 | */ 33 | 34 | /** 35 | * @type {Blob} 36 | */ 37 | 38 | /** 39 | * @type {Boolean} 40 | */ 41 | function FFMPEGWebworkerClient() { 42 | var _this; 43 | 44 | _classCallCheck(this, FFMPEGWebworkerClient); 45 | 46 | _this = _possibleConstructorReturn(this, _getPrototypeOf(FFMPEGWebworkerClient).call(this)); 47 | 48 | _defineProperty(_assertThisInitialized(_this), "_worker", {}); 49 | 50 | _defineProperty(_assertThisInitialized(_this), "_inputFile", {}); 51 | 52 | _defineProperty(_assertThisInitialized(_this), "workerIsReady", false); 53 | 54 | _defineProperty(_assertThisInitialized(_this), "readFileAsBufferArray", function (file) { 55 | return new Promise(function (resolve, reject) { 56 | var fileReader = new FileReader(); 57 | 58 | fileReader.onload = function () { 59 | resolve(this.result); 60 | }; 61 | 62 | fileReader.onerror = function () { 63 | reject(this.error); 64 | }; 65 | 66 | fileReader.readAsArrayBuffer(file); 67 | }); 68 | }); 69 | 70 | _defineProperty(_assertThisInitialized(_this), "runCommand", function (command) { 71 | var totalMemory = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 33554432; 72 | 73 | if (typeof command !== "string" || !command.length) { 74 | throw new Error("command should be string and not empty"); 75 | } 76 | 77 | if (_this.inputFile && _this.inputFile.type) { 78 | _this.convertInputFileToArrayBuffer().then(function (arrayBuffer) { 79 | while (!_this.workerIsReady) {} 80 | 81 | var filename = "video-".concat(Date.now(), ".webm"); 82 | var inputCommand = "-i ".concat(filename, " ").concat(command); 83 | 84 | _this.worker.postMessage({ 85 | type: "command", 86 | arguments: inputCommand.split(" "), 87 | files: [{ 88 | data: new Uint8Array(arrayBuffer), 89 | name: filename 90 | }], 91 | totalMemory: totalMemory 92 | }); 93 | }); 94 | } else { 95 | _this.worker.postMessage({ 96 | type: "command", 97 | arguments: command.split(" "), 98 | totalMemory: totalMemory 99 | }); 100 | } 101 | }); 102 | 103 | _defineProperty(_assertThisInitialized(_this), "log", function (message) { 104 | return Array.isArray(message) ? console.log.call(null, message) : console.log(message); 105 | }); 106 | 107 | _defineProperty(_assertThisInitialized(_this), "isVideo", function (file) { 108 | var fileType = file.type; 109 | return file instanceof Blob && (fileType.includes("video") || fileType.includes("audio")); 110 | }); 111 | 112 | _this.initWebWorker(); 113 | 114 | return _this; 115 | } 116 | 117 | _createClass(FFMPEGWebworkerClient, [{ 118 | key: "initWebWorker", 119 | value: function initWebWorker() { 120 | var _this2 = this; 121 | 122 | this.worker = new WorkerFile(workerFile); 123 | this.log; 124 | 125 | var log = this.worker.onmessage = function (event) { 126 | var message = event.data; 127 | 128 | if (event && event.type) { 129 | if (message.type == "ready") { 130 | _this2.emit("onReady", "ffmpeg-asm.js file has been loaded."); 131 | 132 | _this2.workerIsReady = true; 133 | } else if (message.type == "stdout") { 134 | _this2.emit("onStdout", message.data); 135 | } else if (message.type == "start") { 136 | _this2.emit("onFileReceived", "File Received"); 137 | 138 | log("file received ffmpeg command."); 139 | } else if (message.type == "done") { 140 | _this2.emit("onDone", message.data); 141 | } 142 | } 143 | }; 144 | } 145 | }, { 146 | key: "inputFileExists", 147 | value: function inputFileExists() { 148 | var inputFile = this.inputFile; 149 | return !!(inputFile && inputFile instanceof Blob && inputFile.size && inputFile.type); 150 | } 151 | /** 152 | * use worker to encode audio 153 | * @param {Blob} inputFile 154 | * @return {Promise} 155 | */ 156 | 157 | }, { 158 | key: "convertInputFileToArrayBuffer", 159 | value: function convertInputFileToArrayBuffer() { 160 | if (!this.inputFileExists()) { 161 | throw new Error("Input File has not been set"); 162 | } 163 | 164 | return this.readFileAsBufferArray(this.inputFile); 165 | } 166 | /** 167 | * @param {String} command 168 | */ 169 | 170 | }, { 171 | key: "worker", 172 | set: function set(worker) { 173 | this._worker = worker; 174 | }, 175 | get: function get() { 176 | return this._worker; 177 | } 178 | }, { 179 | key: "inputFile", 180 | set: function set(inputFile) { 181 | if (!this.isVideo(inputFile)) { 182 | throw new Error("Input file is expected to be an audio or a video"); 183 | } 184 | 185 | this._inputFile = inputFile; 186 | }, 187 | get: function get() { 188 | return this._inputFile; 189 | } 190 | /** 191 | * use worker to encode audio 192 | * @param {Blob} file 193 | * @return {Promise} 194 | */ 195 | 196 | }]); 197 | 198 | return FFMPEGWebworkerClient; 199 | }(EventEmitter); 200 | 201 | export { FFMPEGWebworkerClient as default }; -------------------------------------------------------------------------------- /dist/es/index.js: -------------------------------------------------------------------------------- 1 | import WebworkerClient from "./FFMPEGWebWorkerClient"; 2 | import Webworker from "./FFMPEGWebWorker"; 3 | export var FFMPEGWebworker = Webworker; 4 | export var FFMPEGWebworkerClient = WebworkerClient; 5 | var workerClient = { 6 | on: function on() {}, 7 | emit: function emit() {} 8 | }; 9 | 10 | var _window = global || window; 11 | 12 | if (_window && _window.Blob) { 13 | workerClient = new WebworkerClient(); 14 | } 15 | 16 | export default workerClient; -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).ffmpegWebworker={})}(this,function(e){"use strict";var t="undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{};function r(e,t){for(var n=0;ni){s.warned=!0;var a=new Error("Possible EventEmitter memory leak detected. "+s.length+" "+t+" listeners added. Use emitter.setMaxListeners() to increase limit");a.name="MaxListenersExceededWarning",a.emitter=e,a.type=t,a.count=s.length,function(e){"function"==typeof console.warn?console.warn(e):console.log(e)}(a)}}else s=o[t]=n,++e._eventsCount;return e}function m(e,t,n){var r=!1;function i(){e.removeListener(t,i),r||(r=!0,n.apply(e,arguments))}return i.listener=n,i}function w(e){var t=this._events;if(t){var n=t[e];if("function"==typeof n)return 1;if(n)return n.length}return 0}function g(e,t){for(var n=new Array(t);t--;)n[t]=e[t];return n}h.prototype=Object.create(null),(d.EventEmitter=d).usingDomains=!1,d.prototype.domain=void 0,d.prototype._events=void 0,d.prototype._maxListeners=void 0,d.defaultMaxListeners=10,d.init=function(){this.domain=null,d.usingDomains&&l.active&&l.Domain,this._events&&this._events!==Object.getPrototypeOf(this)._events||(this._events=new h,this._eventsCount=0),this._maxListeners=this._maxListeners||void 0},d.prototype.setMaxListeners=function(e){if("number"!=typeof e||e<0||isNaN(e))throw new TypeError('"n" argument must be a positive number');return this._maxListeners=e,this},d.prototype.getMaxListeners=function(){return y(this)},d.prototype.emit=function(e,t,n,r){var i,o,s,a,u,f,l,c="error"===e;if(f=this._events)c=c&&null==f.error;else if(!c)return!1;if(l=this.domain,c){if(i=t,l)return(i=i||new Error('Uncaught, unspecified "error" event')).domainEmitter=this,i.domain=l,i.domainThrown=!1,l.emit("error",i),!1;if(i instanceof Error)throw i;var p=new Error('Uncaught, unspecified "error" event. ('+i+")");throw p.context=i,p}if(!(o=f[e]))return!1;var h="function"==typeof o;switch(s=arguments.length){case 1:!function(e,t,n){if(t)e.call(n);else for(var r=e.length,i=g(e,r),o=0;o 2 | 3 | 4 | 5 | 6 | 7 | ffmpeg-webworker 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/theme.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | font-family: sans-serif; 5 | } 6 | 7 | .container > p { 8 | font-size: 1rem; 9 | } 10 | 11 | .container > em { 12 | font-size: 0.8rem; 13 | } 14 | 15 | .dropzone { 16 | flex: 1; 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | padding: 20px; 21 | border-width: 2px; 22 | border-radius: 2px; 23 | border-color: #eeeeee; 24 | border-style: dashed; 25 | background-color: #fafafa; 26 | color: #bdbdbd; 27 | outline: none; 28 | transition: border 0.24s ease-in-out; 29 | } 30 | 31 | .dropzone:focus { 32 | border-color: #2196f3; 33 | } 34 | 35 | .dropzone.disabled { 36 | opacity: 0.6; 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ffmpeg-webworker", 3 | "version": "1.3.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist/" 8 | ], 9 | "module": "dist/es/index.js", 10 | "scripts": { 11 | "clean": "rimraf ./dist", 12 | "test": "cross-env NODE_ENV=test jest --coverage && npm run typescript", 13 | "build": "npm run clean && npm run build:umd && npm run build:es", 14 | "build:umd": "cross-env NODE_ENV=es rollup -c", 15 | "build:es": "cross-env BABEL_ENV=es babel ./src --out-dir ./dist/es --ignore '**/*.spec.js'", 16 | "start": "styleguidist server", 17 | "styleguide": "styleguidist build", 18 | "test:watch": "cross-env NODE_ENV=test jest --watch", 19 | "commitmsg": "commitlint -e", 20 | "precommit": "pretty-quick --staged", 21 | "prepublish": "npm run build && npm run size", 22 | "logo": "cd logo && sketchtool export artboards logo.sketch", 23 | "imagemin": "imagemin --out-dir=logo --plugin=pngquant --plugin=svgo", 24 | "size": "size-limit", 25 | "size:why": "size-limit --why", 26 | "typescript": "tsc --project ./typings/tests" 27 | }, 28 | "size-limit": [ 29 | { 30 | "path": "dist/index.js", 31 | "limit": "7 KB" 32 | }, 33 | { 34 | "path": "dist/es/index.js", 35 | "limit": "7 KB" 36 | } 37 | ], 38 | "jest": { 39 | "clearMocks": true, 40 | "setupFilesAfterEnv": [ 41 | "/testSetup.js" 42 | ], 43 | "coveragePathIgnorePatterns": [ 44 | "/dist/", 45 | "/node_modules/", 46 | "/testSetup.js" 47 | ], 48 | "testPathIgnorePatterns": [ 49 | "/node_modules/", 50 | "/dist/" 51 | ] 52 | }, 53 | "keywords": [ 54 | "ffmpeg", 55 | "webworker", 56 | "ffmpegjs", 57 | "audio", 58 | "video", 59 | "transcode", 60 | "convert" 61 | ], 62 | "author": "", 63 | "license": "MIT", 64 | "peerDependencies": { 65 | "react": ">= 16.8" 66 | }, 67 | "dependencies": { 68 | "attr-accept": "^1.1.3", 69 | "file-selector": "^0.1.11", 70 | "prop-types": "^15.7.2", 71 | "react-styleguidist": "^9.2.0", 72 | "rollup-plugin-node-globals": "^1.4.0", 73 | "webpack-blocks": "^2.0.1", 74 | "webworker-file": "^0.2.0" 75 | }, 76 | "devDependencies": { 77 | "@babel/cli": "^7.5.5", 78 | "@babel/core": "^7.5.5", 79 | "@babel/plugin-proposal-class-properties": "^7.5.5", 80 | "@babel/plugin-proposal-do-expressions": "^7.5.0", 81 | "@babel/plugin-proposal-export-default-from": "^7.5.2", 82 | "@babel/plugin-proposal-logical-assignment-operators": "^7.2.0", 83 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.4", 84 | "@babel/plugin-proposal-optional-chaining": "^7.2.0", 85 | "@babel/plugin-proposal-pipeline-operator": "^7.5.0", 86 | "@babel/plugin-transform-runtime": "^7.5.5", 87 | "@babel/preset-env": "^7.5.5", 88 | "@babel/preset-react": "^7.0.0", 89 | "@babel/register": "^7.0.0", 90 | "@commitlint/cli": "^7.0.0", 91 | "@commitlint/config-angular": "^7.0.1", 92 | "@commitlint/prompt": "^7.0.0", 93 | "@commitlint/prompt-cli": "^7.0.0", 94 | "@types/react": "^16.8.3", 95 | "@types/react-dom": "^16.8.1", 96 | "babel-plugin-add-module-exports": "^1.0.0", 97 | "babel-plugin-dynamic-import-node": "^2.2.0", 98 | "commitizen": "^2.10.1", 99 | "cross-env": "^5.2.0", 100 | "husky": "^0.14.3", 101 | "imagemin-cli": "^3.0.0", 102 | "imagemin-pngquant": "^6.0.0", 103 | "lint-staged": "^7.2.2", 104 | "markdownlint-cli": "^0.13.0", 105 | "prettier": "*", 106 | "pretty-quick": "^1.11.1", 107 | "react": "^16.8.2", 108 | "react-dom": "^16.8.2", 109 | "rimraf": "^2.5.2", 110 | "rollup": "^1.17.0", 111 | "rollup-plugin-babel": "^4.3.3", 112 | "rollup-plugin-commonjs": "^10.0.1", 113 | "rollup-plugin-node-builtins": "^2.1.2", 114 | "rollup-plugin-node-resolve": "^5.2.0", 115 | "rollup-plugin-uglify": "^6.0.2", 116 | "sinon": "^3.2.1", 117 | "size-limit": "^0.19.2", 118 | "style-loader": "^0.18.2", 119 | "styled-components": "^4.1.2", 120 | "tslint": "^5.9.1", 121 | "typescript": "^3.2.4", 122 | "webpack": "^4.29.5" 123 | }, 124 | "repository": { 125 | "type": "git", 126 | "url": "https://github.com/limistah/ffmpeg-webworker.git" 127 | }, 128 | "typings": "typings/ffmpeg-webworker.d.ts", 129 | "config": { 130 | "commitizen": { 131 | "path": "@commitlint/prompt" 132 | } 133 | }, 134 | "husky": { 135 | "hooks": { 136 | "prepare-commit-msg": "exec < /dev/tty && git cz --hook" 137 | } 138 | }, 139 | "engines": { 140 | "node": ">= 8" 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel"; 2 | import resolve from "rollup-plugin-node-resolve"; 3 | import commonjs from "rollup-plugin-commonjs"; 4 | import { uglify } from "rollup-plugin-uglify"; 5 | import builtins from "rollup-plugin-node-builtins"; 6 | import globals from "rollup-plugin-node-globals"; 7 | 8 | const umdGlobals = { 9 | Blob: "Blob", 10 | URL: "URL", 11 | Worker: "Worker" 12 | }; 13 | 14 | export default { 15 | input: "./src/index.js", 16 | output: { 17 | file: "dist/index.js", 18 | format: "umd", 19 | name: "ffmpegWebworker", 20 | globals: umdGlobals, 21 | sourcemap: "inline", 22 | exports: "named" 23 | }, 24 | external: Object.keys(umdGlobals), 25 | plugins: [ 26 | babel({ 27 | exclude: "node_modules/**" 28 | }), 29 | resolve({ preferBuiltins: true }), 30 | globals(), 31 | builtins(), 32 | commonjs(), 33 | uglify() 34 | ] 35 | }; 36 | -------------------------------------------------------------------------------- /src/FFMPEGWebWorker.js: -------------------------------------------------------------------------------- 1 | const workerFile = () => { 2 | const workerPath = 3 | "https://raw.githubusercontent.com/bgrins/videoconverter.js/master/build/ffmpeg-all-codecs.js"; 4 | 5 | importScripts(workerPath); 6 | const now = Date.now; 7 | function print(text) { 8 | postMessage({ type: "stdout", data: text }); 9 | } 10 | onmessage = function(event) { 11 | const message = event.data; 12 | if (message.type === "command") { 13 | const Module = { 14 | print: print, 15 | printErr: print, 16 | files: message.files || [], 17 | arguments: message.arguments || [], 18 | TOTAL_MEMORY: message.totalMemory || 33554432 19 | }; 20 | postMessage({ type: "start", data: Module.arguments.join(" ") }); 21 | postMessage({ 22 | type: "stdout", 23 | data: 24 | "Received command: " + 25 | Module.arguments.join(" ") + 26 | (Module.TOTAL_MEMORY 27 | ? ". Processing with " + Module.TOTAL_MEMORY + " bits." 28 | : "") 29 | }); 30 | const time = now(); 31 | const result = ffmpeg_run(Module); 32 | const totalTime = now() - time; 33 | postMessage({ 34 | type: "stdout", 35 | data: "Finished processing (took " + totalTime + "ms)" 36 | }); 37 | postMessage({ type: "done", data: result, time: totalTime }); 38 | } 39 | }; 40 | postMessage({ type: "ready" }); 41 | }; 42 | 43 | export default workerFile; 44 | -------------------------------------------------------------------------------- /src/FFMPEGWebWorkerClient.js: -------------------------------------------------------------------------------- 1 | import WorkerFile from "webworker-file"; 2 | import workerFile from "./FFMPEGWebWorker"; 3 | import { EventEmitter } from "events"; 4 | 5 | export default class FFMPEGWebworkerClient extends EventEmitter { 6 | /** 7 | * @type {Worker} 8 | */ 9 | _worker = {}; 10 | /** 11 | * @type {Blob} 12 | */ 13 | _inputFile = {}; 14 | 15 | /** 16 | * @type {Boolean} 17 | */ 18 | workerIsReady = false; 19 | constructor() { 20 | super(); 21 | this.initWebWorker(); 22 | } 23 | 24 | initWebWorker() { 25 | this.worker = new WorkerFile(workerFile); 26 | this.log; 27 | const log = (this.worker.onmessage = event => { 28 | let message = event.data; 29 | if (event && event.type) { 30 | if (message.type == "ready") { 31 | this.emit("onReady", "ffmpeg-asm.js file has been loaded."); 32 | this.workerIsReady = true; 33 | } else if (message.type == "stdout") { 34 | this.emit("onStdout", message.data); 35 | } else if (message.type == "start") { 36 | this.emit("onFileReceived", "File Received"); 37 | log("file received ffmpeg command."); 38 | } else if (message.type == "done") { 39 | this.emit("onDone", message.data); 40 | } 41 | } 42 | }); 43 | } 44 | 45 | set worker(worker) { 46 | this._worker = worker; 47 | } 48 | get worker() { 49 | return this._worker; 50 | } 51 | 52 | set inputFile(inputFile) { 53 | if (!this.isVideo(inputFile)) { 54 | throw new Error("Input file is expected to be an audio or a video"); 55 | } 56 | this._inputFile = inputFile; 57 | } 58 | get inputFile() { 59 | return this._inputFile; 60 | } 61 | 62 | /** 63 | * use worker to encode audio 64 | * @param {Blob} file 65 | * @return {Promise} 66 | */ 67 | readFileAsBufferArray = file => { 68 | return new Promise((resolve, reject) => { 69 | let fileReader = new FileReader(); 70 | fileReader.onload = function() { 71 | resolve(this.result); 72 | }; 73 | fileReader.onerror = function() { 74 | reject(this.error); 75 | }; 76 | fileReader.readAsArrayBuffer(file); 77 | }); 78 | }; 79 | 80 | inputFileExists() { 81 | const inputFile = this.inputFile; 82 | return !!( 83 | inputFile && 84 | inputFile instanceof Blob && 85 | inputFile.size && 86 | inputFile.type 87 | ); 88 | } 89 | 90 | /** 91 | * use worker to encode audio 92 | * @param {Blob} inputFile 93 | * @return {Promise} 94 | */ 95 | convertInputFileToArrayBuffer() { 96 | if (!this.inputFileExists()) { 97 | throw new Error("Input File has not been set"); 98 | } 99 | return this.readFileAsBufferArray(this.inputFile); 100 | } 101 | 102 | /** 103 | * @param {String} command 104 | */ 105 | runCommand = (command, totalMemory = 33554432) => { 106 | if (typeof command !== "string" || !command.length) { 107 | throw new Error("command should be string and not empty"); 108 | } 109 | if (this.inputFile && this.inputFile.type) { 110 | this.convertInputFileToArrayBuffer().then(arrayBuffer => { 111 | while (!this.workerIsReady) {} 112 | const filename = `video-${Date.now()}.webm`; 113 | const inputCommand = `-i ${filename} ${command}`; 114 | this.worker.postMessage({ 115 | type: "command", 116 | arguments: inputCommand.split(" "), 117 | files: [ 118 | { 119 | data: new Uint8Array(arrayBuffer), 120 | name: filename 121 | } 122 | ], 123 | totalMemory 124 | }); 125 | }); 126 | } else { 127 | this.worker.postMessage({ 128 | type: "command", 129 | arguments: command.split(" "), 130 | totalMemory 131 | }); 132 | } 133 | }; 134 | 135 | /** 136 | * @param {String | Array} message 137 | * @return {void} 138 | */ 139 | log = message => 140 | Array.isArray(message) 141 | ? console.log.call(null, message) 142 | : console.log(message); 143 | 144 | /** 145 | * @param {Blob} file 146 | * @return {Boolean} 147 | */ 148 | isVideo = file => { 149 | const fileType = file.type; 150 | return ( 151 | file instanceof Blob && 152 | (fileType.includes("video") || fileType.includes("audio")) 153 | ); 154 | }; 155 | } 156 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import WebworkerClient from "./FFMPEGWebWorkerClient"; 2 | import Webworker from "./FFMPEGWebWorker"; 3 | 4 | export const FFMPEGWebworker = Webworker; 5 | export const FFMPEGWebworkerClient = WebworkerClient; 6 | let workerClient = { on() {}, emit() {} }; 7 | const _window = global || window; 8 | if (_window && _window.Blob) { 9 | workerClient = new WebworkerClient(); 10 | } 11 | export default workerClient; 12 | -------------------------------------------------------------------------------- /styleguide.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { createConfig, babel, css } = require("webpack-blocks"); 3 | 4 | module.exports = { 5 | title: "ffmpeg-webworker", 6 | styleguideDir: path.join(__dirname, "docs"), 7 | webpackConfig: createConfig([babel(), css()]), 8 | exampleMode: "expand", 9 | usageMode: "expand", 10 | showSidebar: false, 11 | serverPort: 8080, 12 | moduleAliases: { 13 | "ffmpeg-webworker": path.resolve(__dirname, "./src") 14 | }, 15 | require: [path.join(__dirname, "examples/theme.css")], 16 | sections: [ 17 | { 18 | name: "", 19 | content: "README.MD" 20 | } 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /testSetup.js: -------------------------------------------------------------------------------- 1 | const jest = require('jest'); 2 | // https://www.npmjs.com/package/jest-dom 3 | require('jest-dom/extend-expect'); 4 | 5 | // TODO: Ignore warnings about act(), it refers to having async side effects updating the state; 6 | // This happens because our async getFilesFromEvent() fn 7 | // See https://github.com/kentcdodds/react-testing-library/issues/281, 8 | // https://github.com/facebook/react/issues/14769 9 | jest.spyOn(console, 'error').mockImplementation(() => jest.fn()); 10 | -------------------------------------------------------------------------------- /typings/ffmpeg-webworker.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/limistah/ffmpeg-webworker/ba47aa0efc8bc2e822652e0833f853e1a731d5fc/typings/ffmpeg-webworker.d.ts --------------------------------------------------------------------------------