├── images └── beamcoder_small.jpg ├── .circleci ├── images │ └── testbeam10-4.1 │ │ └── Dockerfile └── config.yml ├── types ├── PrivClass.d.ts ├── Packet.d.ts ├── Beamstreams.d.ts ├── CodecPar.d.ts ├── Encoder.d.ts ├── Demuxer.d.ts ├── Codec.d.ts ├── Muxer.d.ts ├── Stream.d.ts ├── Decoder.d.ts ├── Frame.d.ts └── Filter.d.ts ├── .eslintrc.js ├── src ├── governor.h ├── filter.h ├── codec_par.h ├── codec.h ├── packet.h ├── frame.h ├── encode.h ├── decode.h ├── format.h ├── demux.h ├── mux.h ├── adaptor.h ├── beamcoder_util.h └── governor.cc ├── .vscode ├── c_cpp_properties.json └── settings.json ├── package.json ├── .gitignore ├── scratch ├── read_wav.js ├── decode_hevc.js ├── decode_aac.js ├── decode_pcm.js ├── simple_mux.js ├── stream_mux.js ├── decode_avci.js ├── stream_wav.js ├── muxer.js ├── make_a_mux.js ├── stream_mp4.js ├── stream_pcm.js └── stream_avci.js ├── test ├── muxerSpec.js ├── filtererSpec.js ├── demuxerSpec.js ├── decoderSpec.js ├── introspectionSpec.js ├── encoderSpec.js ├── packetSpec.js ├── codecParamsSpec.js ├── frameSpec.js └── formatSpec.js ├── index.js ├── index.d.ts ├── examples ├── jpeg_app.js ├── encode_h264.js ├── make_mp4.js └── jpeg_filter_app.js ├── binding.gyp └── install_ffmpeg.js /images/beamcoder_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rifkyprayoga/beamcoder/HEAD/images/beamcoder_small.jpg -------------------------------------------------------------------------------- /.circleci/images/testbeam10-4.1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM circleci/node:10 2 | 3 | # install OpenCL driver 4 | RUN sudo apt-get update \ 5 | && sudo apt-get install software-properties-common \ 6 | && sudo add-apt-repository ppa:jonathonf/ffmpeg-4 \ 7 | && sudo apt-get update \ 8 | && sudo apt-get install libavcodec-dev libavformat-dev libavdevice-dev libavfilter-dev libavutil-dev libpostproc-dev libswresample-dev libswscale-dev 9 | 10 | # delete all the apt list files since they're big and get stale quickly 11 | RUN sudo rm -rf /var/lib/apt/lists/* 12 | # this forces "apt-get update" in dependent images, which is also good 13 | -------------------------------------------------------------------------------- /types/PrivClass.d.ts: -------------------------------------------------------------------------------- 1 | export interface PrivClass { 2 | readonly type: 'Class' 3 | readonly class_name: string 4 | readonly options: { 5 | [key: string]: { 6 | name: string 7 | help: string 8 | option_type: string 9 | flags: { 10 | ENCODING_PARAM: boolean 11 | DECODING_PARAM: boolean 12 | AUDIO_PARAM: boolean 13 | VIDEO_PARAM: boolean 14 | SUBTITLE_PARAM: boolean 15 | EXPORT: boolean 16 | READONLY: boolean 17 | BSF_PARAM: boolean 18 | FILTERING_PARAM: boolean 19 | DEPRECATED: boolean 20 | } 21 | unit?: string 22 | const?: Array 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 2018, 8 | "sourceType": "module", 9 | }, 10 | "extends": "eslint:recommended", 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 2 15 | ], 16 | "quotes": [ 17 | "error", 18 | "single" 19 | ], 20 | "semi": [ 21 | "error", 22 | "always" 23 | ], 24 | "no-console": "off", 25 | "prefer-arrow-callback": "error" 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/governor.h: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | #ifndef GOVERNER_H 23 | #define GOVERNER_H 24 | 25 | #include "beamcoder_util.h" 26 | #include "node_api.h" 27 | 28 | napi_value governor(napi_env env, napi_callback_info info); 29 | 30 | #endif // GOVERNER_H 31 | -------------------------------------------------------------------------------- /src/filter.h: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2018 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | #ifndef FILTER_H 23 | #define FILTER_H 24 | 25 | #include "node_api.h" 26 | 27 | napi_value filterer(napi_env env, napi_callback_info info); 28 | napi_value filter(napi_env env, napi_callback_info info); 29 | 30 | #endif // FILTER_H 31 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Win32", 5 | "includePath": [ 6 | "C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/include/*", 7 | "C:/Program Files (x86)/Windows Kits/8.1/Include/um", 8 | "C:/Program Files (x86)/Windows Kits/10/Include/10.0.10240.0/ucrt", 9 | "C:/Program Files (x86)/Windows Kits/8.1/Include/shared", 10 | "C:/Program Files (x86)/Windows Kits/8.1/Include/winrt", 11 | "${workspaceFolder}/**", 12 | "${workspaceFolder}/../ffmpeg/ffmpeg-4.1-win64-dev/include/**", 13 | "C:/Users/simon/.node-gyp/10.14.1/include/node" 14 | ], 15 | "defines": [ 16 | "_DEBUG", 17 | "UNICODE", 18 | "_UNICODE" 19 | ], 20 | "windowsSdkVersion": "8.1", 21 | "compilerPath": "C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/cl.exe", 22 | "cStandard": "c11", 23 | "cppStandard": "c++17", 24 | "intelliSenseMode": "msvc-x64" 25 | } 26 | ], 27 | "version": 4 28 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beamcoder", 3 | "version": "0.5.1", 4 | "description": "Node.js native bindings to FFmpeg.", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "preinstall": "node install_ffmpeg.js", 9 | "install": "node-gyp rebuild", 10 | "test": "tape test/*.js", 11 | "lint": "eslint **/*.js", 12 | "lint-html": "eslint **/*.js -f html -o ./reports/lint-results.html", 13 | "lint-fix": "eslint --fix **/*.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/Streampunk/beamcoder.git" 18 | }, 19 | "keywords": [ 20 | "FFmpeg", 21 | "libav", 22 | "video", 23 | "audio", 24 | "sound", 25 | "encode", 26 | "decode", 27 | "transcode", 28 | "N-API" 29 | ], 30 | "author": "Streampunk Media Ltd", 31 | "license": "GPL-3.0-or-later", 32 | "bugs": { 33 | "url": "https://github.com/Streampunk/beamcoder/issues" 34 | }, 35 | "homepage": "https://github.com/Streampunk/beamcoder#readme", 36 | "dependencies": { 37 | "bindings": "^1.5.0", 38 | "segfault-handler": "^1.3.0" 39 | }, 40 | "devDependencies": { 41 | "eslint": "^6.8.0", 42 | "tape": "^4.13.0" 43 | }, 44 | "gypfile": true 45 | } 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Native module build dir 64 | build/ 65 | 66 | # FFmpeg install folder 67 | ffmpeg/ 68 | 69 | # Test media files 70 | *.MP4 71 | *.mp4 72 | 73 | # Editors and IDE's 74 | *.swp 75 | .vscode/ 76 | -------------------------------------------------------------------------------- /scratch/read_wav.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const beamcoder = require('../index.js'); 23 | 24 | async function run() { 25 | let demuxer = await beamcoder.demuxer('../media/sound/BBCNewsCountdown.wav'); 26 | let packet = {}; 27 | for ( let x = 0 ; x < 100 && packet !== null ; x++ ) { 28 | packet = await demuxer.read(); 29 | console.log(x, packet); 30 | } 31 | console.log(await demuxer.seek({ frame : 120 })); 32 | console.log(await demuxer.read()); 33 | } 34 | 35 | run(); 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "vector": "cpp", 4 | "xstring": "cpp", 5 | "sstream": "cpp", 6 | "algorithm": "cpp", 7 | "chrono": "cpp", 8 | "cmath": "cpp", 9 | "condition_variable": "cpp", 10 | "cstddef": "cpp", 11 | "cstdint": "cpp", 12 | "cstdio": "cpp", 13 | "cstdlib": "cpp", 14 | "cstring": "cpp", 15 | "cwchar": "cpp", 16 | "deque": "cpp", 17 | "exception": "cpp", 18 | "functional": "cpp", 19 | "initializer_list": "cpp", 20 | "ios": "cpp", 21 | "iosfwd": "cpp", 22 | "istream": "cpp", 23 | "limits": "cpp", 24 | "list": "cpp", 25 | "map": "cpp", 26 | "memory": "cpp", 27 | "mutex": "cpp", 28 | "new": "cpp", 29 | "ostream": "cpp", 30 | "queue": "cpp", 31 | "ratio": "cpp", 32 | "stdexcept": "cpp", 33 | "streambuf": "cpp", 34 | "string": "cpp", 35 | "system_error": "cpp", 36 | "xthread": "cpp", 37 | "thread": "cpp", 38 | "tuple": "cpp", 39 | "type_traits": "cpp", 40 | "typeinfo": "cpp", 41 | "unordered_map": "cpp", 42 | "utility": "cpp", 43 | "xfacet": "cpp", 44 | "xfunctional": "cpp", 45 | "xhash": "cpp", 46 | "xiosbase": "cpp", 47 | "xlocale": "cpp", 48 | "xlocinfo": "cpp", 49 | "xlocnum": "cpp", 50 | "xmemory": "cpp", 51 | "xmemory0": "cpp", 52 | "xstddef": "cpp", 53 | "xtr1common": "cpp", 54 | "xtree": "cpp", 55 | "xutility": "cpp" 56 | } 57 | } -------------------------------------------------------------------------------- /scratch/decode_hevc.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const beamcoder = require('../index.js'); 23 | 24 | async function run() { 25 | let demuxer = await beamcoder.demuxer('../media/bbb_1080p_c.ts'); 26 | console.log(demuxer); 27 | let decoder = await beamcoder.decoder({ name: 'hevc' }); 28 | for ( let x = 0 ; x < 100 ; x++ ) { 29 | let packet = await demuxer.read(); 30 | if (packet.stream_index == 0) { 31 | // console.log(packet); 32 | let frames = await decoder.decode(packet); // eslint-disable-line 33 | // console.log(frames); 34 | } 35 | } 36 | } 37 | 38 | run(); 39 | -------------------------------------------------------------------------------- /test/muxerSpec.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const test = require('tape'); 23 | const beamcoder = require('../index.js'); 24 | 25 | test('Creating a muxer', t => { 26 | let mx = beamcoder.muxer({ name: 'mpegts' }); 27 | t.ok(mx, 'is truthy.'); 28 | t.equal(typeof mx.iformat, 'undefined', 'input format is undefined.'); 29 | t.ok(mx.oformat, 'has output format.'); 30 | t.equal(mx.oformat.name, 'mpegts', 'output format is mpegts.'); 31 | t.equal(mx.type, 'muxer', 'type name is set to muxer.'); 32 | t.throws(() => beamcoder.muxer({ name: 'wibble' }), 'throws when unknown name.'); 33 | t.end(); 34 | }); 35 | -------------------------------------------------------------------------------- /scratch/decode_aac.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const beamcoder = require('../index.js'); 23 | 24 | async function run() { 25 | let demuxer = await beamcoder.demuxer({ url: '../media/bbb_1080p_c.ts'}); 26 | let decoder = beamcoder.decoder({ name: 'aac' }); 27 | let packet = {}; 28 | for ( let x = 0 ; packet !== null && x < 100 ; x++ ) { 29 | packet = await demuxer.read(); 30 | if (packet.stream_index == 1) { 31 | console.log(JSON.stringify(packet, null, 2)); 32 | let frames = await decoder.decode(packet); 33 | console.log(JSON.stringify(frames.frames[0], null, 2)); 34 | } 35 | } 36 | } 37 | 38 | run(); 39 | -------------------------------------------------------------------------------- /scratch/decode_pcm.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const beamcoder = require('../index.js'); 23 | 24 | async function run() { 25 | let demuxer = await beamcoder.demuxer('../media/dpp/AS11_DPP_HD_EXAMPLE_1.mxf'); 26 | console.log(demuxer.streams[1]); 27 | let decoder = await beamcoder.decoder({ demuxer: demuxer, stream_index : 1 }); 28 | console.log(decoder); 29 | for ( let x = 0 ; x < 100 ; x++ ) { 30 | let packet = await demuxer.read(); 31 | if (packet.stream == 1) { 32 | //console.log(packet); 33 | let frames = await decoder.decode(packet); 34 | console.log(frames.frames); 35 | } 36 | } 37 | } 38 | 39 | run(); 40 | -------------------------------------------------------------------------------- /src/codec_par.h: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | #ifndef CODEC_PAR_H 23 | #define CODEC_PAR_H 24 | 25 | #include "node_api.h" 26 | #include "beamcoder_util.h" 27 | 28 | extern "C" { 29 | #include 30 | } 31 | 32 | void codecParamsFinalizer(napi_env env, void* data, void* hint); 33 | 34 | napi_value makeCodecParameters(napi_env env, napi_callback_info info); 35 | napi_value makeCodecParamsInternal(napi_env env, napi_callback_info info, bool ownAlloc); 36 | napi_status fromAVCodecParameters(napi_env env, AVCodecParameters* c, bool ownAlloc, napi_value* result); 37 | 38 | napi_value codecParToJSON(napi_env env, napi_callback_info info); 39 | 40 | #endif // CODEC_PAR_H 41 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/Streampunk/beamcoder 5 | parallelism: 1 6 | shell: /bin/bash --login 7 | environment: 8 | CIRCLE_ARTIFACTS: /tmp/circleci-artifacts 9 | CIRCLE_TEST_REPORTS: /tmp/circleci-test-results 10 | UV_THREADPOOL_SIZE: 16 11 | docker: 12 | - image: streampunkmedia/testbeam:10-4.1 13 | steps: 14 | - checkout 15 | - run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS 16 | - restore_cache: 17 | keys: 18 | # This branch if available 19 | - v1-dep-{{ .Branch }}- 20 | # Default branch if not 21 | - v1-dep-master- 22 | # Any branch if there are none on the default branch - this should be unnecessary if you have your default branch configured correctly 23 | - v1-dep- 24 | - run: npm install tap-xunit 25 | - run: npm install --unsafe-perm 26 | - save_cache: 27 | key: v1-dep-{{ .Branch }}-{{ epoch }} 28 | paths: 29 | - ./node_modules 30 | - run: echo 'export PATH="~/Streampunk/beamcoder/node_modules/.bin:$PATH"' >> $BASH_ENV 31 | - run: mkdir -p $CIRCLE_TEST_REPORTS/eslint 32 | - run: mkdir -p $CIRCLE_TEST_REPORTS/xunit 33 | - run: eslint '**/*.js' -f junit -o /tmp/circleci-test-results/eslint/eslint.xml 34 | - run: set -eo pipefail && npm test | tap-xunit > /tmp/circleci-test-results/xunit/results.xml 35 | 36 | - store_test_results: 37 | path: /tmp/circleci-test-results 38 | - store_artifacts: 39 | path: /tmp/circleci-artifacts 40 | - store_artifacts: 41 | path: /tmp/circleci-test-results 42 | -------------------------------------------------------------------------------- /test/filtererSpec.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const test = require('tape'); 23 | const beamcoder = require('../index.js'); 24 | 25 | test('Create a filterer', async t => { 26 | let flt = await beamcoder.filterer({ 27 | filterType: 'audio', 28 | inputParams: [ 29 | { 30 | sampleRate: 48000, 31 | sampleFormat: 's16', 32 | channelLayout: 'mono', 33 | timeBase: [1, 48000] 34 | } 35 | ], 36 | outputParams: [ 37 | { 38 | sampleRate: 8000, 39 | sampleFormat: 's16', 40 | channelLayout: 'mono' 41 | } 42 | ], 43 | filterSpec: 'aresample=8000, aformat=sample_fmts=s16:channel_layouts=mono' 44 | }); 45 | t.ok(flt, 'is truthy.'); 46 | t.end(); 47 | }); 48 | -------------------------------------------------------------------------------- /scratch/simple_mux.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | const beamcoder = require('../index.js'); 22 | 23 | async function run() { 24 | let demuxer = await beamcoder.demuxer('../media/sound/BBCNewsCountdown.wav'); 25 | let muxer = beamcoder.muxer({ filename: 'file:test.wav' }); 26 | let stream = muxer.newStream(demuxer.streams[0]); // eslint-disable-line 27 | // stream.time_base = demuxer.streams[0].time_base; 28 | // stream.codecpar = demuxer.streams[0].codecpar; 29 | await muxer.openIO(); 30 | await muxer.writeHeader(); 31 | let packet = {}; 32 | for ( let x = 0 ; x < 100 && packet !== null ; x++ ) { 33 | packet = await demuxer.read(); 34 | await muxer.writeFrame(packet); 35 | } 36 | await muxer.writeTrailer(); 37 | } 38 | 39 | run(); 40 | -------------------------------------------------------------------------------- /src/codec.h: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | #ifndef BEAM_CODEC_H 23 | #define BEAM_CODEC_H 24 | 25 | #include "node_api.h" 26 | #include "beamcoder_util.h" 27 | #include "decode.h" 28 | #include "encode.h" 29 | #include "codec_par.h" 30 | 31 | extern "C" { 32 | #include 33 | } 34 | 35 | void codecContextFinalizer(napi_env env, void* data, void* hint); 36 | napi_status fromAVCodecContext(napi_env env, AVCodecContext* codec, 37 | napi_value* result, bool encoding); 38 | napi_status fromAVCodecDescriptor(napi_env env, const AVCodecDescriptor* codecDesc, 39 | napi_value *result); 40 | 41 | napi_value extractParams(napi_env env, napi_callback_info info); 42 | napi_value useParams(napi_env env, napi_callback_info info); 43 | 44 | #endif // BEAM_CODEC_H 45 | -------------------------------------------------------------------------------- /src/packet.h: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | #ifndef PACKET_H 23 | #define PACKET_H 24 | 25 | #include "node_api.h" 26 | #include "beamcoder_util.h" 27 | 28 | extern "C" { 29 | #include 30 | } 31 | 32 | void packetFinalizer(napi_env env, void* data, void* hint); 33 | void packetDataFinalizer(napi_env env, void* data, void* hint); 34 | void packetBufferFinalizer(napi_env env, void* data, void* hint); 35 | void packetBufferFree(void* opaque, uint8_t* data); 36 | 37 | struct packetData { 38 | AVPacket* packet = nullptr; 39 | napi_ref dataRef = nullptr; 40 | int32_t extSize = 0; 41 | ~packetData() { 42 | av_packet_free(&packet); 43 | } 44 | }; 45 | 46 | napi_value makePacket(napi_env env, napi_callback_info info); 47 | napi_status fromAVPacket(napi_env env, packetData* packet, napi_value* result); 48 | 49 | #endif // PACKET_H 50 | -------------------------------------------------------------------------------- /test/demuxerSpec.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const test = require('tape'); 23 | const beamcoder = require('../index.js'); 24 | 25 | test('Creating a demuxer', async t => { 26 | let dm = await beamcoder.demuxer('https://www.elecard.com/storage/video/bbb_1080p_c.ts'); 27 | t.ok(dm, 'is truthy.'); 28 | t.equal(dm.type, 'demuxer', 'type name says demuxer.'); 29 | t.equal(typeof dm.oformat, 'undefined', 'output format is undefined.'); 30 | t.ok(dm.iformat, 'has an input format.'); 31 | t.equal(dm.iformat.name, 'mpegts', 'input format is mpegts.'); 32 | t.equal(dm.streams.length, 2, 'has 2 streams.'); 33 | try { 34 | await beamcoder.demuxer('file:jaberwocky.junk'); 35 | t.fail('Did not throw when opening non-existant file.'); 36 | } catch(e) { 37 | console.log(e.message); 38 | t.ok(e.message.match(/Problem opening/), 'throws opening non-existant file.'); 39 | } 40 | t.end(); 41 | }); 42 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings to FFmpeg 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const beamcoder = require('bindings')('beamcoder'); 23 | const beamstreams = require('./beamstreams.js'); 24 | 25 | // Provide useful debug on segfault-related crash 26 | const SegfaultHandler = require('segfault-handler'); 27 | SegfaultHandler.registerHandler('crash.log'); 28 | 29 | const splash = `Aerostat Beam Coder Copyright (C) 2019 Streampunk Media Ltd 30 | GPL v3.0 or later license. This program comes with ABSOLUTELY NO WARRANTY. 31 | This is free software, and you are welcome to redistribute it 32 | under certain conditions. Conditions and warranty at: 33 | https://github.com/Streampunk/beamcoder/blob/master/LICENSE`; 34 | 35 | console.log(splash); 36 | 37 | beamcoder.demuxerStream = beamstreams.demuxerStream; 38 | beamcoder.muxerStream = beamstreams.muxerStream; 39 | 40 | beamcoder.makeSources = beamstreams.makeSources; 41 | beamcoder.makeStreams = beamstreams.makeStreams; 42 | 43 | module.exports = beamcoder; 44 | -------------------------------------------------------------------------------- /scratch/stream_mux.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const beamcoder = require('../index.js'); 23 | const fs = require('fs'); 24 | 25 | async function run() { 26 | let demuxer = await beamcoder.demuxer('../../media/sound/BBCNewsCountdown.wav'); 27 | 28 | let muxerStream = beamcoder.muxerStream({ highwaterMark: 65536 }); 29 | muxerStream.pipe(fs.createWriteStream('test.wav')); 30 | 31 | let muxer = muxerStream.muxer({ format_name: 'wav' }); 32 | let stream = muxer.newStream(demuxer.streams[0]); // eslint-disable-line 33 | // stream.time_base = demuxer.streams[0].time_base; 34 | // stream.codecpar = demuxer.streams[0].codecpar; 35 | await muxer.openIO(); 36 | 37 | await muxer.writeHeader(); 38 | let packet = {}; 39 | for ( let x = 0 ; x < 10000 && packet !== null ; x++ ) { 40 | packet = await demuxer.read(); 41 | if (packet) 42 | await muxer.writeFrame(packet); 43 | } 44 | await muxer.writeTrailer(); 45 | } 46 | 47 | run(); 48 | -------------------------------------------------------------------------------- /src/frame.h: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.JS native mappings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | #ifndef FRAME_H 23 | #define FRAME_H 24 | 25 | #include "node_api.h" 26 | #include "beamcoder_util.h" 27 | #include 28 | 29 | extern "C" { 30 | #include 31 | #include 32 | } 33 | 34 | void frameFinalizer(napi_env env, void* data, void* hint); 35 | void frameDataFinalizer(napi_env env, void* data, void* hint); 36 | void frameBufferFinalizer(napi_env env, void* data, void* hint); 37 | void frameBufferFree(void* opaque, uint8_t* data); 38 | 39 | struct frameData { 40 | AVFrame* frame = nullptr; 41 | std::vector dataRefs; 42 | int32_t extSize = 0; 43 | ~frameData() { 44 | // printf("Freeing frame with pts = %i\n", frame->pts); 45 | av_frame_free(&frame); 46 | } 47 | }; 48 | 49 | napi_value makeFrame(napi_env env, napi_callback_info info); 50 | napi_status fromAVFrame(napi_env env, frameData* frame, napi_value* result); 51 | 52 | #endif // FRAME_H 53 | -------------------------------------------------------------------------------- /scratch/decode_avci.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const beamcoder = require('../index.js'); 23 | 24 | async function run() { 25 | let demuxer = await beamcoder.demuxer('../media/dpp/AS11_DPP_HD_EXAMPLE_1.mxf'); 26 | console.log(JSON.stringify(demuxer, null, 2)); 27 | demuxer.streams.forEach(s => s.discard = (0 == s.index) ? 'default' : 'all'); 28 | let decoder = beamcoder.decoder({ name: 'h264' }); 29 | //console.log(JSON.stringify(decoder, null, 2)); 30 | let packet = {}; 31 | for ( let x = 0 ; x < 200 && packet != null; x++ ) { 32 | let packet = await demuxer.read(); 33 | if (packet.stream_index === 0) { 34 | //console.log(JSON.stringify(packet, null, 2)); 35 | let frames = await decoder.decode(packet); 36 | console.log(JSON.stringify(frames.frames[0], null, 2)); 37 | console.log(x, frames.total_time); 38 | } 39 | } 40 | let frames = await decoder.flush(); 41 | console.log('flush', frames.total_time, frames.length); 42 | console.log(await demuxer.seek({ pos: 79389000 })); 43 | console.log(await demuxer.read()); 44 | } 45 | 46 | run(); 47 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./types/CodecPar" 2 | export * from "./types/Packet" 3 | export * from "./types/Frame" 4 | export * from "./types/Stream" 5 | export * from "./types/Codec" 6 | export * from "./types/FormatContext" 7 | export * from "./types/Demuxer" 8 | export * from "./types/Decoder" 9 | export * from "./types/Filter" 10 | export * from "./types/Encoder" 11 | export * from "./types/Muxer" 12 | export * from "./types/Beamstreams" 13 | 14 | export const AV_NOPTS_VALUE: number 15 | 16 | /** The LIBAV**_VERSION_INT for each FFmpeg library */ 17 | export function versions(): { 18 | avcodec: number 19 | avdevice: number 20 | avfilter: number 21 | avformat: number 22 | avutil: number 23 | postproc: number 24 | swresample: number 25 | swscale: number 26 | } 27 | /** 28 | * FFmpeg version string. This usually is the actual release 29 | * version number or a git commit description. This string has no fixed format 30 | * and can change any time. It should never be parsed by code. 31 | */ 32 | export function avVersionInfo(): string 33 | /** Informative version strings for each FFmpeg library */ 34 | export function versionStrings(): { 35 | avcodec: string 36 | avdevice: string 37 | avfilter: string 38 | avformat: string 39 | avutil: string 40 | postproc: string 41 | swresample: string 42 | swscale: string 43 | } 44 | /** Build configuration strings for each FFmpeg library */ 45 | export function configurations(): { 46 | avcodec: string 47 | avdevice: string 48 | avfilter: string 49 | avformat: string 50 | avutil: string 51 | postproc: string 52 | swresample: string 53 | swscale: string 54 | } 55 | /** License strings for each FFmpeg library */ 56 | export function licenses(): { 57 | avcodec: string 58 | avdevice: string 59 | avfilter: string 60 | avformat: string 61 | avutil: string 62 | postproc: string 63 | swresample: string 64 | swscale: string 65 | } 66 | /** List the available protocols */ 67 | export function protocols(): { inputs: Array, outputs: Array } 68 | 69 | export as namespace Beamcoder 70 | -------------------------------------------------------------------------------- /scratch/stream_wav.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const beamcoder = require('../index.js'); 23 | 24 | async function run() { 25 | const urls = [ 'file:../Media/sound/Countdown.wav' ]; 26 | const spec = { start: 50, end: 58 }; 27 | const params = { 28 | video: [], 29 | audio: [ 30 | { 31 | sources: [ 32 | { url: urls[0], ms: spec, streamIndex: 0 } 33 | ], 34 | filterSpec: '[in0:a] \ 35 | volume=precision=float:volume=0.8 \ 36 | [out0:a]', 37 | streams: [ 38 | { name: 'aac', time_base: [1, 90000], 39 | codecpar: { 40 | sample_rate: 48000, format: 'fltp', channel_layout: 'stereo' 41 | } 42 | } 43 | ] 44 | }, 45 | ], 46 | out: { 47 | formatName: 'mp4', 48 | url: 'file:temp.mp4' 49 | } 50 | }; 51 | 52 | await beamcoder.makeSources(params); 53 | const beamStreams = await beamcoder.makeStreams(params); 54 | await beamStreams.run(); 55 | } 56 | 57 | console.log('Running wav maker'); 58 | let start = Date.now(); 59 | run() 60 | .then(() => console.log(`Finished ${Date.now() - start}ms`)) 61 | .catch(console.error); 62 | 63 | -------------------------------------------------------------------------------- /scratch/muxer.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | // Work in progress 23 | 24 | const beamcoder = require('../index.js'); 25 | 26 | const STREAM_FRAME_RATE = 25; 27 | 28 | function allocAudioFrame(sampleFormat, channelLayout, sampleRate, nbSamples) { 29 | 30 | return beamcoder.frame({ 31 | format: sampleFormat, 32 | channel_layout: channelLayout, 33 | sample_rate: sampleRate, 34 | nb_samples: nbSamples 35 | }).alloc(); 36 | } 37 | 38 | function allocPicture(pixelFmt, width, height) { // eslint-disable-line 39 | 40 | return beamcoder.frame({ 41 | format: pixelFmt, 42 | width: width, 43 | height: height 44 | }).alloc(); 45 | } 46 | 47 | async function addStream(stream, muxer, codecID) { // eslint-disable-line 48 | let codec = await beamcoder.encoder({ codec_id: codecID }); 49 | 50 | stream.st = muxer.newStream(); 51 | stream.enc = codec; 52 | switch (codec.media_type) { 53 | case 'video': 54 | codec.setParameters({ 55 | codec_id: codecID, 56 | bit_rate: 400000, 57 | width: 352, 58 | height: 288, 59 | time_base: [1, STREAM_FRAME_RATE], 60 | gop_size: 12 61 | }); 62 | break; 63 | case 'audio': 64 | break; 65 | default: 66 | break; 67 | } 68 | } 69 | 70 | async function run() { 71 | allocAudioFrame('s32p', 'stereo', 48000, 1920); 72 | } 73 | 74 | run(); 75 | -------------------------------------------------------------------------------- /test/decoderSpec.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const test = require('tape'); 23 | const beamcoder = require('../index.js'); 24 | 25 | test('Creating a decoder', t => { 26 | let dec = beamcoder.decoder({ name: 'h264' }); 27 | t.ok(dec, 'is truthy.'); 28 | t.equal(dec.name, 'h264', 'has the expected name.'); 29 | t.equal(dec.codec_id, 27, 'has the expected codec_id.'); 30 | t.ok(typeof dec._CodecContext == 'object', 'external value present.'); 31 | t.equal(dec.type, 'decoder', 'has expected type name.'); 32 | t.end(); 33 | }); 34 | 35 | test('Checking the A properties:', t => { 36 | let dec = beamcoder.decoder({ name: 'h264' }); 37 | 38 | t.deepEqual(dec.active_thread_type, { FRAME: false, SLICE: false}, 39 | 'active_thread_type has expected default.'); 40 | t.throws(() => { dec.active_thread_type = { FRAME: true }; }, /User cannot/, 41 | 'active_thread_type cannot be set.'); 42 | 43 | t.equals(dec.apply_cropping, 1, 'apply_cropping has exepceted default.'); 44 | t.doesNotThrow(() => { dec.apply_cropping = 0; }, 45 | 'apply_cropping setting does not throw.'); 46 | t.equals(dec.apply_cropping, 0, 'value has been updated.'); 47 | 48 | t.equals(dec.audio_service_type, 'main', 49 | 'audio_service_type has expected default value.'); 50 | t.throws(() => { dec.audio_service_type = 'dialogue'; }, 51 | /decoding/, 'cannot be updated when deocoding.'); 52 | 53 | t.end(); 54 | }); 55 | 56 | // TODO properties B to Z 57 | -------------------------------------------------------------------------------- /src/encode.h: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | #ifndef ENCODE_H 23 | #define ENCODE_H 24 | 25 | #include "beamcoder_util.h" 26 | #include "frame.h" 27 | #include "packet.h" 28 | #include "codec.h" 29 | #include 30 | 31 | extern "C" { 32 | #include 33 | #include 34 | #include 35 | } 36 | 37 | napi_value encoder(napi_env env, napi_callback_info info); 38 | 39 | void encodeExecute(napi_env env, void* data); 40 | void encodeComplete(napi_env env, napi_status asyncStatus, void* data); 41 | napi_value encode(napi_env env, napi_callback_info info); 42 | napi_value flushEnc(napi_env env, napi_callback_info info); 43 | 44 | void encoderFinalizer(napi_env env, void* data, void* hint); 45 | 46 | /* struct encoderCarrier : carrier { 47 | AVCodecContext* encoder; 48 | // AVCodecParameters* params = nullptr; 49 | const AVCodec* codec = nullptr; 50 | char* codecName; 51 | size_t codecNameLen = 0; 52 | int32_t codecID = -1; 53 | ~encoderCarrier() { 54 | if (encoder != nullptr) { 55 | avcodec_close(encoder); 56 | avcodec_free_context(&encoder); 57 | } 58 | } 59 | }; */ 60 | 61 | struct encodeCarrier : carrier { 62 | AVCodecContext* encoder; 63 | std::vector frames; 64 | std::vector packets; 65 | std::vector frameRefs; 66 | ~encodeCarrier() { } 67 | }; 68 | 69 | napi_status isFrame(napi_env env, napi_value packet); 70 | AVFrame* getFrame(napi_env env, napi_value packet); 71 | 72 | #endif // ENCODE_H 73 | -------------------------------------------------------------------------------- /test/introspectionSpec.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const test = require('tape'); 23 | const beamcoder = require('../index.js'); 24 | 25 | test('Version information', t => { 26 | t.equals(beamcoder.avVersionInfo().slice(0, 2), '4.', 'has expected version number.'); 27 | t.equals(typeof beamcoder.versions(), 'object', 'versions is an object.'); 28 | t.equals(Object.keys(beamcoder.versions()).length, 8, 'versions has 8 entries.'); 29 | t.ok(Object.values(beamcoder.versions()).every(x => typeof x === 'number' && x > 0), 30 | 'versions values are numbers.'); 31 | t.equals(typeof beamcoder.versionStrings(), 'object', 32 | 'versionStrings is an object.'); 33 | t.equals(Object.keys(beamcoder.versions()).length, 8, 34 | 'versionStrings is an object.'); 35 | t.ok(Object.values(beamcoder.versionStrings()).every(x => 36 | typeof x === 'string' && x.match(/\d+\.\d+\.\d+/)), 37 | 'versionStrings match expected pattern.'); 38 | t.equals(typeof beamcoder.configurations(), 'object', 39 | 'configurations is an object.'); 40 | t.equals(Object.keys(beamcoder.configurations()).length, 8, 41 | 'configurations has 8 entries.'); 42 | t.ok(Object.values(beamcoder.configurations()).every(x => typeof x === 'string'), 43 | 'configurations entry are strings.'); 44 | t.end(); 45 | }); 46 | 47 | test('Muxer information', t => { 48 | let muxers = beamcoder.muxers(); 49 | t.ok(muxers, 'muxers is truthy.'); 50 | t.equal(typeof muxers, 'object', 'muxers is an object.'); 51 | t.ok(JSON.stringify(muxers), 'can be converted to JSON.'); 52 | t.end(); 53 | }); 54 | -------------------------------------------------------------------------------- /src/decode.h: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | #ifndef DECODE_H 23 | #define DECODE_H 24 | 25 | #include "beamcoder_util.h" 26 | #include "packet.h" 27 | #include "frame.h" 28 | #include "codec.h" 29 | #include 30 | 31 | extern "C" { 32 | #include 33 | #include 34 | #include 35 | #include 36 | } 37 | 38 | napi_value decoder(napi_env env, napi_callback_info info); 39 | 40 | void decodeExecute(napi_env env, void* data); 41 | void decodeComplete(napi_env env, napi_status asyncStatus, void* data); 42 | napi_value decode(napi_env env, napi_callback_info info); 43 | napi_value flushDec(napi_env env, napi_callback_info info); 44 | 45 | void decoderFinalizer(napi_env env, void* data, void* hint); 46 | 47 | /* struct decoderCarrier : carrier { 48 | AVCodecContext* decoder = nullptr; 49 | AVCodecParameters* params = nullptr; 50 | int streamIdx = -1; 51 | char* codecName; 52 | size_t codecNameLen = 0; 53 | int32_t codecID = -1; 54 | ~decoderCarrier() { 55 | if (decoder != nullptr) { 56 | avcodec_close(decoder); 57 | avcodec_free_context(&decoder); 58 | } 59 | } 60 | }; */ 61 | 62 | struct decodeCarrier : carrier { 63 | AVCodecContext* decoder; 64 | std::vector packets; 65 | std::vector frames; 66 | std::vector packetRefs; 67 | ~decodeCarrier() { 68 | // printf("Decode carrier destructor.\n"); 69 | } 70 | }; 71 | 72 | napi_status isPacket(napi_env env, napi_value packet); 73 | AVPacket* getPacket(napi_env env, napi_value packet); 74 | 75 | #endif // DECODE_H 76 | -------------------------------------------------------------------------------- /scratch/make_a_mux.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const beamcoder = require('../index.js'); 23 | 24 | async function run() { 25 | let demuxer = await beamcoder.demuxer('../media/sound/BBCNewsCountdown.wav'); 26 | console.log(demuxer.streams[0].codecpar); 27 | let muxer = beamcoder.muxer({ filename: 'file:test.wav' }); 28 | let stream = muxer.newStream({ 29 | name: 'pcm_s16le', 30 | time_base: [1, 48000 ], 31 | interleaved: false, }); 32 | Object.assign(stream.codecpar, { 33 | channels: 2, 34 | sample_rate: 48000, 35 | format: 's16', 36 | channel_layout: 'stereo', 37 | block_align: 4, 38 | bits_per_coded_sample: 16, 39 | bit_rate: 48000 * 4 * 8 40 | }); 41 | /* let stream = muxer.newStream({ 42 | name: 'pcm_s16le', 43 | time_base: [1, 48000 ], 44 | codecpar: { 45 | name: 'pcm_s16le', 46 | channels: 2, 47 | sample_rate: 48000, 48 | format: 's16', 49 | channel_layout: 'stereo', 50 | block_align: 4, 51 | bits_per_coded_sample: 16, 52 | bit_rate: 48000*4 53 | } 54 | }); */ 55 | console.log(stream.codecpar); 56 | // stream.time_base = demuxer.streams[0].time_base; 57 | // stream.codecpar = demuxer.streams[0].codecpar; 58 | await muxer.openIO({ options: { blocksize: 8192 }}).then(console.log); 59 | await muxer.writeHeader({ options: { write_bext: true, write_peak: 'on', peak_format: 2 }}).then(console.log); 60 | let packet = {}; 61 | for ( let x = 0 ; x < 100 && packet !== null ; x++ ) { 62 | packet = await demuxer.read(); 63 | await muxer.writeFrame(packet); 64 | } 65 | await muxer.writeTrailer(); 66 | } 67 | 68 | run(); 69 | -------------------------------------------------------------------------------- /scratch/stream_mp4.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const beamcoder = require('../index.js'); 23 | 24 | async function run() { 25 | const urls = [ 'file:../../Media/big_buck_bunny_1080p_h264.mov' ]; 26 | const spec = { start: 0, end: 24 }; 27 | 28 | const params = { 29 | video: [ 30 | { 31 | sources: [ 32 | { url: urls[0], ms: spec, streamIndex: 0 } 33 | ], 34 | filterSpec: '[in0:v] scale=1280:720, colorspace=all=bt709 [out0:v]', 35 | streams: [ 36 | { name: 'h264', time_base: [1, 90000], 37 | codecpar: { 38 | width: 1280, height: 720, format: 'yuv422p', color_space: 'bt709', 39 | sample_aspect_ratio: [1, 1] 40 | } 41 | } 42 | ] 43 | } 44 | ], 45 | audio: [ 46 | { 47 | sources: [ 48 | { url: urls[0], ms: spec, streamIndex: 2 } 49 | ], 50 | filterSpec: '[in0:a] aformat=sample_fmts=fltp:channel_layouts=mono [out0:a]', 51 | streams: [ 52 | { name: 'aac', time_base: [1, 90000], 53 | codecpar: { 54 | sample_rate: 48000, format: 'fltp', frame_size: 1024, 55 | channels: 1, channel_layout: 'mono' 56 | } 57 | } 58 | ] 59 | }, 60 | ], 61 | out: { 62 | formatName: 'mp4', 63 | url: 'file:temp.mp4' 64 | } 65 | }; 66 | 67 | await beamcoder.makeSources(params); 68 | const beamStreams = await beamcoder.makeStreams(params); 69 | 70 | await beamStreams.run(); 71 | } 72 | 73 | console.log('Running mp4 maker'); 74 | let start = Date.now(); 75 | run() 76 | .then(() => console.log(`Finished ${Date.now() - start}ms`)) 77 | .catch(console.error); 78 | 79 | -------------------------------------------------------------------------------- /src/format.h: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | #ifndef FORMAT_H 23 | #define FORMAT_H 24 | 25 | #include "node_api.h" 26 | #include "beamcoder_util.h" 27 | #include "codec_par.h" 28 | #include "packet.h" 29 | #include "adaptor.h" 30 | 31 | extern "C" { 32 | #include 33 | #include 34 | } 35 | 36 | napi_value muxers(napi_env env, napi_callback_info info); 37 | napi_value demuxers(napi_env env, napi_callback_info info); 38 | napi_value guessFormat(napi_env env, napi_callback_info info); 39 | napi_value newStream(napi_env env, napi_callback_info info); 40 | 41 | napi_status fromAVInputFormat(napi_env env, 42 | const AVInputFormat* iformat, napi_value* result); 43 | napi_status fromAVOutputFormat(napi_env env, 44 | const AVOutputFormat* iformat, napi_value* result); 45 | napi_status fromAVFormatContext(napi_env env, 46 | AVFormatContext* fmtCtx, Adaptor *adaptor, napi_value* result); 47 | napi_status fromAVStream(napi_env env, AVStream* stream, napi_value* result); 48 | 49 | void formatContextFinalizer(napi_env env, void* data, void* hint); 50 | 51 | napi_value makeFormat(napi_env env, napi_callback_info info); 52 | napi_value streamToJSON(napi_env env, napi_callback_info info); 53 | napi_value formatToJSON(napi_env env, napi_callback_info info); 54 | 55 | /* Notes 56 | 57 | AVInputFormat and AVOutputFormats 58 | - iterate over to produce lists of available 59 | - JS objects that reflect the internal values only - lazy 60 | - Ability to "guess" an output formats 61 | 62 | AVFormatContext 63 | - full getters and setters pattern 64 | - demuxer to offer add new streams 65 | - uses setters and getters 66 | 67 | AVStream 68 | - only public constructor is muxer.newStream() 69 | - uses setters and getters 70 | */ 71 | 72 | #endif // FORMAT_H 73 | -------------------------------------------------------------------------------- /examples/jpeg_app.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | /* Main example from the README.md. Run in a folder of media files. 23 | 24 | Only supports 8-bit YUV 4:2:2 or 4:2:0 pixel formats. 25 | */ 26 | 27 | const beamcoder = require('../index.js'); // Use require('beamcoder') externally 28 | const Koa = require('koa'); // Add koa to package.json dependencies 29 | const app = new Koa(); 30 | 31 | app.use(async (ctx) => { // Assume HTTP GET with path // 32 | let parts = ctx.path.split('/'); // Split the path into filename and time 33 | if ((parts.length < 3) || (isNaN(+parts[2]))) return; // Ignore favicon etc.. 34 | let dm = await beamcoder.demuxer('file:' + parts[1]); // Probe the file 35 | await dm.seek({ time: +parts[2] }); // Seek to the closest keyframe to time 36 | let packet = await dm.read(); // Find the next video packet (assumes stream 0) 37 | for ( ; packet.stream_index !== 0 ; packet = await dm.read() ); 38 | let dec = beamcoder.decoder({ demuxer: dm, stream_index: 0 }); // Create a decoder 39 | let decResult = await dec.decode(packet); // Decode the frame 40 | if (decResult.frames.length === 0) // Frame may be buffered, so flush it out 41 | decResult = await dec.flush(); 42 | // Filtering could be used to transform the picture here, e.g. scaling 43 | let enc = beamcoder.encoder({ // Create an encoder for JPEG data 44 | name : 'mjpeg', // FFmpeg does not have an encoder called 'jpeg' 45 | width : dec.width, 46 | height: dec.height, 47 | pix_fmt: dec.pix_fmt.indexOf('422') >= 0 ? 'yuvj422p' : 'yuvj420p', 48 | time_base: [1, 1] }); 49 | let jpegResult = await enc.encode(decResult.frames[0]); // Encode the frame 50 | await enc.flush(); // Tidy the encoder 51 | ctx.type = 'image/jpeg'; // Set the Content-Type of the data 52 | ctx.body = jpegResult.packets[0].data; // Return the JPEG image data 53 | }); 54 | 55 | app.listen(3000); // Start the server on port 3000 56 | -------------------------------------------------------------------------------- /test/encoderSpec.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const test = require('tape'); 23 | const beamcoder = require('../index.js'); 24 | 25 | test('Creating a video encoder', t => { 26 | let enc = beamcoder.encoder({ name: 'h264' }); 27 | t.ok(enc, 'is truthy.'); 28 | t.equal(enc.name, 'libx264', 'has the expected name.'); 29 | t.equal(enc.codec_id, 27, 'has the expected codec_id.'); 30 | t.ok(typeof enc._CodecContext == 'object', 'external value present.'); 31 | t.equal(enc.type, 'encoder', 'has expected type name.'); 32 | t.end(); 33 | }); 34 | 35 | test('Creating an audio encoder', t => { 36 | let enc = beamcoder.encoder({ name: 'aac' }); 37 | t.ok(enc, 'is truthy.'); 38 | t.equal(enc.name, 'aac', 'has the expected name.'); 39 | t.equal(enc.codec_id, 86018, 'has the expected codec_id.'); 40 | t.ok(typeof enc._CodecContext == 'object', 'external value present.'); 41 | t.equal(enc.type, 'encoder', 'has expected type name.'); 42 | t.end(); 43 | }); 44 | 45 | test('Checking the A properties:', t => { 46 | let enc = beamcoder.encoder({ name: 'h264' }); 47 | 48 | t.deepEqual(enc.active_thread_type, { FRAME: false, SLICE: false}, 49 | 'active_thread_type has expected default.'); 50 | t.throws(() => { enc.active_thread_type = { FRAME: true }; }, /User cannot/, 51 | 'active_thread_type cannot be set.'); 52 | 53 | t.notOk(enc.apply_cropping, 'apply_cropping not defined for encoding.'); 54 | t.throws(() => { enc.apply_cropping = 0; }, /encoding/, 55 | 'apply_cropping setting does not throw.'); 56 | 57 | t.equals(enc.audio_service_type, 'main', 58 | 'audio_service_type has expected default value.'); 59 | t.doesNotThrow(() => { enc.audio_service_type = 'dialogue'; }, 60 | 'audio_service_type can be updated.'); 61 | t.equals(enc.audio_service_type, 'dialogue', 62 | 'audio_service_type has been updated.'); 63 | t.throws(() => { enc.audio_service_type = 'wibble'; }, 64 | 'audio_service_type throws with unknown value.'); 65 | 66 | t.end(); 67 | }); 68 | 69 | // TODO properties B to Z 70 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [{ 3 | "target_name" : "beamcoder", 4 | "sources" : [ "src/beamcoder.cc", "src/beamcoder_util.cc", 5 | "src/governor.cc", "src/demux.cc", 6 | "src/decode.cc", "src/filter.cc", 7 | "src/encode.cc", "src/mux.cc", 8 | "src/packet.cc", "src/frame.cc", 9 | "src/codec_par.cc", "src/format.cc", 10 | "src/codec.cc" ], 11 | "conditions": [ 12 | ['OS=="mac"', { 13 | "include_dirs" : [ 14 | "/usr/local/Cellar/ffmpeg/4.2.2/include" 15 | ], 16 | "library_dirs": [ 17 | "/usr/local/Cellar/ffmpeg/4.2.2/lib", 18 | ] 19 | }], 20 | ['OS!="win"', { 21 | "defines": [ 22 | "__STDC_CONSTANT_MACROS" 23 | ], 24 | "cflags_cc!": [ 25 | "-fno-rtti", 26 | "-fno-exceptions" 27 | ], 28 | "cflags_cc": [ 29 | "-std=c++11", 30 | "-fexceptions" 31 | ], 32 | "link_settings": { 33 | "libraries": [ 34 | "-lavcodec", 35 | "-lavdevice", 36 | "-lavfilter", 37 | "-lavformat", 38 | "-lavutil", 39 | "-lpostproc", 40 | "-lswresample", 41 | "-lswscale" 42 | ] 43 | } 44 | }], 45 | ['OS=="win"', { 46 | "configurations": { 47 | "Release": { 48 | "msvs_settings": { 49 | "VCCLCompilerTool": { 50 | "RuntimeTypeInfo": "true" 51 | } 52 | } 53 | } 54 | }, 55 | "include_dirs" : [ 56 | "ffmpeg/ffmpeg-4.2.1-win64-dev/include" 57 | ], 58 | "libraries": [ 59 | "-l../ffmpeg/ffmpeg-4.2.1-win64-dev/lib/avcodec", 60 | "-l../ffmpeg/ffmpeg-4.2.1-win64-dev/lib/avdevice", 61 | "-l../ffmpeg/ffmpeg-4.2.1-win64-dev/lib/avfilter", 62 | "-l../ffmpeg/ffmpeg-4.2.1-win64-dev/lib/avformat", 63 | "-l../ffmpeg/ffmpeg-4.2.1-win64-dev/lib/avutil", 64 | "-l../ffmpeg/ffmpeg-4.2.1-win64-dev/lib/postproc", 65 | "-l../ffmpeg/ffmpeg-4.2.1-win64-dev/lib/swresample", 66 | "-l../ffmpeg/ffmpeg-4.2.1-win64-dev/lib/swscale" 67 | ], 68 | "copies": [ 69 | { 70 | "destination": "build/Release/", 71 | "files": [ 72 | "ffmpeg/ffmpeg-4.2.1-win64-shared/bin/avcodec-58.dll", 73 | "ffmpeg/ffmpeg-4.2.1-win64-shared/bin/avdevice-58.dll", 74 | "ffmpeg/ffmpeg-4.2.1-win64-shared/bin/avfilter-7.dll", 75 | "ffmpeg/ffmpeg-4.2.1-win64-shared/bin/avformat-58.dll", 76 | "ffmpeg/ffmpeg-4.2.1-win64-shared/bin/avutil-56.dll", 77 | "ffmpeg/ffmpeg-4.2.1-win64-shared/bin/postproc-55.dll", 78 | "ffmpeg/ffmpeg-4.2.1-win64-shared/bin/swresample-3.dll", 79 | "ffmpeg/ffmpeg-4.2.1-win64-shared/bin/swscale-5.dll" 80 | ] 81 | } 82 | ] 83 | }] 84 | ] 85 | }] 86 | } 87 | -------------------------------------------------------------------------------- /src/demux.h: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | #ifndef DEMUX_H 23 | #define DEMUX_H 24 | 25 | extern "C" { 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | } 33 | 34 | #include "beamcoder_util.h" 35 | #include "packet.h" 36 | #include "format.h" 37 | #include "node_api.h" 38 | #include "adaptor.h" 39 | 40 | void demuxerExecute(napi_env env, void* data); 41 | void demuxerComplete(napi_env env, napi_status asyncStatus, void* data); 42 | napi_value demuxer(napi_env env, napi_callback_info info); 43 | 44 | void readFrameExecute(napi_env env, void* data); 45 | void readFrameComplete(napi_env env, napi_status asyncStatus, void* data); 46 | napi_value readFrame(napi_env env, napi_callback_info info); 47 | 48 | void seekFrameExecute(napi_env env, void *data); 49 | void seekFrameComplete(napi_env env, napi_status asyncStatus, void *data); 50 | napi_value seekFrame(napi_env env, napi_callback_info info); 51 | 52 | void demuxerFinalizer(napi_env env, void* data, void* hint); 53 | void readBufferFinalizer(napi_env env, void* data, void* hint); 54 | 55 | struct demuxerCarrier : carrier { 56 | const char* filename = nullptr; 57 | Adaptor *adaptor = nullptr; 58 | AVFormatContext* format = nullptr; 59 | AVInputFormat* iformat = nullptr; 60 | AVDictionary* options = nullptr; 61 | ~demuxerCarrier() { 62 | if (format != nullptr) { avformat_close_input(&format); } 63 | if (options != nullptr) { av_dict_free(&options); } 64 | } 65 | }; 66 | 67 | struct readFrameCarrier : carrier { 68 | AVFormatContext* format = nullptr; 69 | Adaptor *adaptor = nullptr; 70 | AVPacket* packet = av_packet_alloc(); 71 | ~readFrameCarrier() { 72 | if (packet != nullptr) av_packet_free(&packet); 73 | } 74 | }; 75 | 76 | struct seekFrameCarrier : carrier { 77 | AVFormatContext* format = nullptr; 78 | int streamIndex = -1; 79 | int64_t timestamp = 0; 80 | // weird semantic - backward actually means 'find nearest key frame before timestamp' 81 | // setting as default as more natural 82 | int flags = AVSEEK_FLAG_BACKWARD; 83 | ~seekFrameCarrier() { } 84 | }; 85 | 86 | #endif // DEMUX_H 87 | -------------------------------------------------------------------------------- /scratch/stream_pcm.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const beamcoder = require('../index.js'); 23 | const fs = require('fs'); 24 | const util = require('util'); // eslint-disable-line 25 | 26 | async function run() { 27 | let demuxerStream = beamcoder.demuxerStream({ highwaterMark: 65536 }); 28 | // fs.createReadStream('../../media/dpp/AS11_DPP_HD_EXAMPLE_1.mxf').pipe(demuxerStream); 29 | fs.createReadStream('../../media/sound/BBCNewsCountdown.wav').pipe(demuxerStream); 30 | 31 | let demuxer = await demuxerStream.demuxer(); 32 | console.log(demuxer.streams); 33 | 34 | let decoder = await beamcoder.decoder({ demuxer: demuxer, stream_index : 0 }); 35 | // console.log(decoder); 36 | 37 | const audStream = demuxer.streams[0]; 38 | let filterer = await beamcoder.filterer({ 39 | filterType: 'audio', 40 | inputParams: [ 41 | { 42 | name: '0:a', 43 | sampleRate: audStream.codecpar.sample_rate, 44 | sampleFormat: audStream.codecpar.format, 45 | channelLayout: 'stereo', //audStream.codecpar.channel_layout, 46 | timeBase: audStream.time_base 47 | } 48 | ], 49 | outputParams: [ 50 | { 51 | name: 'out0:a', 52 | sampleRate: 8000, 53 | sampleFormat: 's16', 54 | channelLayout: 'stereo' 55 | } 56 | ], 57 | filterSpec: '[0:a] aresample=8000, aformat=sample_fmts=s16:channel_layouts=stereo [out0:a]' 58 | }); 59 | // console.log(filterer.graph); 60 | // console.log(util.inspect(filterer.graph.filters[2], {depth: null})); 61 | console.log(filterer.graph.dump()); 62 | 63 | // const abuffersink = filterer.graph.filters.find(f => 'abuffersink' === f.filter.name); 64 | // console.log(util.inspect(abuffersink, {depth: null})); 65 | 66 | let packet = {}; 67 | for ( let x = 0 ; x < 10000 && packet !== null ; x++ ) { 68 | packet = await demuxer.read(); 69 | if (packet && packet.stream_index == 0) { 70 | // console.log(packet); 71 | let frames = await decoder.decode(packet); 72 | // console.log(frames); 73 | 74 | let filtFrames = await filterer.filter([ // eslint-disable-line 75 | { name: '0:a', frames: frames.frames } 76 | ]); 77 | // console.log(filtFrames); 78 | } 79 | } 80 | } 81 | 82 | run().catch(console.error); 83 | -------------------------------------------------------------------------------- /examples/encode_h264.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | /* Generate a 200 frame test pattern and encode it as a raw H.264 file. 23 | 24 | Usage: node encode_h264.js 25 | 26 | Output can be viewed in VLC. Make sure "All Files" is selected to see the file. 27 | */ 28 | 29 | const beamcoder = require('../index.js'); // Use require('beamcoder') externally 30 | const fs = require('fs'); 31 | 32 | let endcode = Buffer.from([0, 0, 1, 0xb7]); 33 | 34 | async function run() { 35 | let start = process.hrtime(); 36 | let encParams = { 37 | name: 'libx264', 38 | width: 1920, 39 | height: 1080, 40 | bit_rate: 2000000, 41 | time_base: [1, 25], 42 | framerate: [25, 1], 43 | gop_size: 10, 44 | max_b_frames: 1, 45 | pix_fmt: 'yuv420p', 46 | priv_data: { preset: 'slow' } 47 | }; 48 | 49 | let encoder = beamcoder.encoder(encParams); 50 | console.log('Encoder', encoder); 51 | 52 | let outFile = fs.createWriteStream(process.argv[2]); 53 | 54 | for ( let i = 0 ; i < 200 ; i++ ) { 55 | let frame = beamcoder.frame({ 56 | width: encParams.width, 57 | height: encParams.height, 58 | format: encParams.pix_fmt 59 | }).alloc(); 60 | 61 | let linesize = frame.linesize; 62 | let [ ydata, bdata, cdata ] = frame.data; 63 | frame.pts = i; 64 | 65 | for ( let y = 0 ; y < frame.height ; y++ ) { 66 | for ( let x = 0 ; x < linesize[0] ; x++ ) { 67 | ydata[y * linesize[0] + x] = x + y + i * 3; 68 | } 69 | } 70 | 71 | for ( let y = 0 ; y < frame.height / 2 ; y++) { 72 | for ( let x = 0; x < linesize[1] ; x++) { 73 | bdata[y * linesize[1] + x] = 128 + y + i * 2; 74 | cdata[y * linesize[1] + x] = 64 + x + i * 5; 75 | } 76 | } 77 | 78 | let packets = await encoder.encode(frame); 79 | if ( i % 10 === 0) console.log('Encoding frame', i); 80 | packets.packets.forEach(x => outFile.write(x.data)); 81 | } 82 | 83 | let p2 = await encoder.flush(); 84 | console.log('Flushing', p2.packets.length, 'frames.'); 85 | p2.packets.forEach(x => outFile.write(x.data)); 86 | outFile.end(endcode); 87 | 88 | console.log('Total time ', process.hrtime(start)); 89 | } 90 | 91 | if (typeof process.argv[2] === 'string') { run(); } 92 | else { console.error('Error: Please provide a file name.'); } 93 | -------------------------------------------------------------------------------- /types/Packet.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This object stores compressed data. It is typically exported by demuxers 3 | * and then passed as input to decoders, or received as output from encoders and 4 | * then passed to muxers. 5 | * 6 | * For video, it should typically contain one compressed frame. For audio it may 7 | * contain several compressed frames. Encoders are allowed to output empty 8 | * packets, with no compressed data, containing only side data 9 | * (e.g. to update some stream parameters at the end of encoding). 10 | */ 11 | export interface Packet { 12 | /** Object name. */ 13 | readonly type: 'Packet' 14 | /** 15 | * Presentation timestamp in AVStream->time_base units the time at which 16 | * the decompressed packet will be presented to the user. 17 | * Can be AV_NOPTS_VALUE if it is not stored in the file. 18 | * pts MUST be larger or equal to dts as presentation cannot happen before 19 | * decompression, unless one wants to view hex dumps. Some formats misuse 20 | * the terms dts and pts/cts to mean something different. Such timestamps 21 | * must be converted to true pts/dts before they are stored in Packet. 22 | */ 23 | pts: number 24 | /** 25 | * Decompression timestamp in AVStream->time_base units the time at which 26 | * the packet is decompressed. 27 | * Can be AV_NOPTS_VALUE if it is not stored in the file. 28 | */ 29 | dts: number 30 | /** 31 | * The raw data of the packet 32 | * Packet data buffers are shared between C and Javascript so can be written to and modified without having to write the buffer back into the packet 33 | */ 34 | data: Buffer 35 | /** The size in bytes of the raw data */ 36 | size: number 37 | /** The index in the format's stream array that this packet belongs to */ 38 | stream_index: number 39 | /** A combination of AV_PKT_FLAG values */ 40 | flags: { 41 | /** The packet contains a keyframe */ 42 | KEY: boolean 43 | /** The packet content is corrupted */ 44 | CORRUPT: boolean 45 | /** 46 | * Flag is used to discard packets which are required to maintain valid 47 | * decoder state but are not required for output and should be dropped 48 | * after decoding. 49 | **/ 50 | DISCARD: boolean 51 | /** 52 | * The packet comes from a trusted source. 53 | * 54 | * Otherwise-unsafe constructs such as arbitrary pointers to data 55 | * outside the packet may be followed. 56 | */ 57 | TRUSTED: boolean 58 | /** 59 | * Flag is used to indicate packets that contain frames that can 60 | * be discarded by the decoder. I.e. Non-reference frames. 61 | */ 62 | DISPOSABLE: boolean // Frames that can be discarded by the decoder 63 | } 64 | /** 65 | * Additional packet data that can be provided by the container. 66 | * Packet can contain several types of side information. 67 | */ 68 | side_data: { type: string, [key: string]: Buffer | string } | null 69 | /** 70 | * Duration of this packet in AVStream->time_base units, 0 if unknown. 71 | * Equals next_pts - this_pts in presentation order. 72 | */ 73 | duration: number 74 | /** byte position in stream, -1 if unknown */ 75 | pos: number 76 | } 77 | 78 | /** 79 | * Packets for decoding can be created without reading them from a demuxer 80 | * Set parameters as required from the Packet object, passing in a buffer and the required size in bytes 81 | */ 82 | export function packet(options: { [key: string]: any, data: Buffer, size: number }): Packet 83 | -------------------------------------------------------------------------------- /src/mux.h: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | #ifndef MUX_H 23 | #define MUX_H 24 | 25 | #include "node_api.h" 26 | #include "beamcoder_util.h" 27 | #include "format.h" 28 | #include "frame.h" 29 | #include "adaptor.h" 30 | 31 | extern "C" { 32 | #include 33 | } 34 | 35 | napi_value muxer(napi_env env, napi_callback_info info); // Set to interleaving once 36 | 37 | void openIOExecute(napi_env env, void* data); 38 | void openIOComplete(napi_env env, napi_status asyncStatus, void* data); 39 | napi_value openIO(napi_env env, napi_callback_info info); 40 | 41 | void writeHeaderExecute(napi_env env, void* data); 42 | void writeHeaderComplete(napi_env env, napi_status asyncStatus, void* data); 43 | napi_value writeHeader(napi_env env, napi_callback_info info); 44 | 45 | void initOutputExecute(napi_env env, void* data); 46 | void initOutputComplete(napi_env env, napi_status asyncStatus, void* data); 47 | napi_value initOutput(napi_env env, napi_callback_info info); 48 | 49 | void writeFrameExecute(napi_env env, void* data); 50 | void writeFrameComplete(napi_env env, napi_status asyncStatus, void* data); 51 | napi_value writeFrame(napi_env env, napi_callback_info info); // IF AVFrame, must include stream_index 52 | 53 | void writeTrailerExecute(napi_env env, void* data); 54 | void writeTrailerComplete(napi_env env, napi_status asyncStatus, void* data); 55 | napi_value writeTrailer(napi_env env, napi_callback_info info); 56 | 57 | napi_value forceClose(napi_env env, napi_callback_info info); 58 | 59 | struct openIOCarrier : carrier { 60 | AVFormatContext* format; 61 | int flags = AVIO_FLAG_WRITE; 62 | AVDictionary* options = nullptr; 63 | ~openIOCarrier() { 64 | if (options != nullptr) av_dict_free(&options); 65 | } 66 | }; 67 | 68 | struct writeHeaderCarrier : carrier { 69 | AVFormatContext* format; 70 | AVDictionary* options = nullptr; 71 | int result = -1; 72 | ~writeHeaderCarrier() { 73 | if (options != nullptr) av_dict_free(&options); 74 | } 75 | }; 76 | 77 | struct initOutputCarrier : carrier { 78 | AVFormatContext* format; 79 | AVDictionary* options = nullptr; 80 | int result = -1; 81 | ~initOutputCarrier() { 82 | if (options != nullptr) av_dict_free(&options); 83 | } 84 | }; 85 | 86 | struct writeFrameCarrier : carrier { 87 | AVFormatContext* format; 88 | Adaptor *adaptor = nullptr; 89 | AVPacket* packet = nullptr; 90 | AVFrame* frame = nullptr; 91 | int streamIndex = 0; 92 | bool interleaved = true; 93 | ~writeFrameCarrier() { 94 | if (packet != nullptr) { av_packet_free(&packet); } 95 | if (frame != nullptr) { av_frame_free(&frame); } 96 | } 97 | }; 98 | 99 | struct writeTrailerCarrier : carrier { 100 | AVFormatContext* format; 101 | Adaptor *adaptor = nullptr; 102 | ~writeTrailerCarrier() { 103 | } 104 | }; 105 | 106 | #endif // MUX_H 107 | -------------------------------------------------------------------------------- /types/Beamstreams.d.ts: -------------------------------------------------------------------------------- 1 | import { Demuxer, DemuxerCreateOptions } from "./Demuxer" 2 | import { Muxer, MuxerCreateOptions } from "./Muxer" 3 | import { InputFormat } from "./FormatContext" 4 | 5 | /** 6 | * A [Node.js Writable stream](https://nodejs.org/docs/latest-v12.x/api/stream.html#stream_writable_streams) 7 | * allowing source data to be streamed to the demuxer from a file or other stream source such as a network connection 8 | */ 9 | export interface WritableDemuxerStream extends NodeJS.WritableStream { 10 | /** 11 | * Create a demuxer for this source 12 | * @param options a DemuxerCreateOptions object 13 | * @returns a promise that resolves to a Demuxer when it has determined sufficient 14 | * format details by consuming data from the source. The promise will wait indefinitely 15 | * until sufficient source data has been read. 16 | */ 17 | demuxer(options: DemuxerCreateOptions): Promise 18 | } 19 | /** 20 | * Create a WritableDemuxerStream to allow streaming to a Demuxer 21 | * @param options.highwaterMark Buffer level when `stream.write()` starts returng false. 22 | * @returns A WritableDemuxerStream that can be streamed to. 23 | */ 24 | export function demuxerStream(options: { highwaterMark?: number }): WritableDemuxerStream 25 | 26 | /** 27 | * A [Node.js Readable stream](https://nodejs.org/docs/latest-v12.x/api/stream.html#stream_readable_streams) 28 | * allowing data to be streamed from the muxer to a file or other stream destination such as a network connection 29 | */ 30 | export interface ReadableMuxerStream extends NodeJS.ReadableStream { 31 | /** 32 | * Create a muxer for this source 33 | * @param options a MuxerCreateOptions object 34 | * @returns A Muxer object 35 | */ 36 | muxer(options: MuxerCreateOptions): Muxer 37 | } 38 | /** 39 | * Create a ReadableMuxerStream to allow streaming from a Muxer 40 | * @param options.highwaterMark The maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource. 41 | * @returns A ReadableMuxerStream that can be streamed from. 42 | */ 43 | export function muxerStream(options: { highwaterMark?: number }): ReadableMuxerStream 44 | 45 | /** Create object for AVIOContext based buffered I/O */ 46 | export function governor(options: { highWaterMark: number }): { 47 | read(len: number): Promise 48 | write(data: Buffer): Promise 49 | finish(): undefined 50 | } 51 | 52 | /** Source definition for a beamstream channel, from either a file or NodeJS ReadableStream */ 53 | export interface BeamstreamSource { 54 | url?: string 55 | input_stream?: NodeJS.ReadableStream 56 | ms?: { start: number, end: number } 57 | streamIndex?: number 58 | iformat?: InputFormat 59 | options?: { [key: string]: any } 60 | } 61 | /** Codec definition for the destination channel */ 62 | export interface BeamstreamStream { 63 | name: string 64 | time_base: Array 65 | codecpar: { [key: string]: any } 66 | } 67 | /** Definition for a channel of beamstream processing */ 68 | export interface BeamstreamChannel { 69 | sources: Array 70 | filterSpec: string 71 | streams: Array 72 | } 73 | /** 74 | * Definition for a beamstream process consisting of a number of audio and video sources 75 | * that are to be processed and multiplexed into an output file or stream 76 | */ 77 | export interface BeamstreamParams { 78 | video?: Array 79 | audio?: Array 80 | /** Destination definition for the beamstream process, to either a file or NodeJS WritableStream */ 81 | out: { 82 | formatName: string 83 | url?: string 84 | output_stream?: NodeJS.WritableStream 85 | } 86 | } 87 | /** 88 | * Initialise the sources for the beamstream process. 89 | * Note - the params object is updated by the function. 90 | */ 91 | export function makeSources(params: BeamstreamParams): Promise 92 | /** 93 | * Initialise the output streams for the beamstream process. 94 | * Note - the params object is updated by the function. 95 | * @returns Promise which resolves to an object with a run function that starts the processing 96 | */ 97 | export function makeStreams(params: BeamstreamParams): Promise<{ run(): Promise} > 98 | 99 | -------------------------------------------------------------------------------- /examples/make_mp4.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | /* Generate a 200 frame test pattern and encode it as a raw H.264 file. 23 | 24 | Usage: node encode_h264.js 25 | 26 | Output can be viewed in VLC. Make sure "All Files" is selected to see the file. 27 | */ 28 | 29 | const beamcoder = require('../index.js'); // Use require('beamcoder') externally 30 | const fs = require('fs'); 31 | 32 | let endcode = Buffer.from([0, 0, 1, 0xb7]); 33 | 34 | async function run() { 35 | let start = process.hrtime(); 36 | let encParams = { 37 | name: 'libx264', 38 | width: 1920, 39 | height: 1080, 40 | bit_rate: 2000000, 41 | time_base: [1, 25], 42 | framerate: [25, 1], 43 | gop_size: 10, 44 | max_b_frames: 1, 45 | pix_fmt: 'yuv420p', 46 | priv_data: { preset: 'slow' } 47 | }; 48 | 49 | let encoder = await beamcoder.encoder(encParams); 50 | console.log('Encoder', encoder); 51 | 52 | const mux = beamcoder.muxer({ format_name: 'mp4' }); 53 | let vstr = mux.newStream({ 54 | name: 'h264', 55 | time_base: [1, 90000], 56 | interleaved: true }); // Set to false for manual interleaving, true for automatic 57 | Object.assign(vstr.codecpar, { 58 | width: 1920, 59 | height: 1080, 60 | format: 'yuv420p' 61 | }); 62 | console.log(vstr); 63 | await mux.openIO({ 64 | url: 'file:test.mp4' 65 | }); 66 | await mux.writeHeader(); 67 | 68 | let outFile = fs.createWriteStream(process.argv[2]); 69 | 70 | for ( let i = 0 ; i < 200 ; i++ ) { 71 | let frame = beamcoder.frame({ 72 | width: encParams.width, 73 | height: encParams.height, 74 | format: encParams.pix_fmt 75 | }).alloc(); 76 | 77 | let linesize = frame.linesize; 78 | let [ ydata, bdata, cdata ] = frame.data; 79 | frame.pts = i+100; 80 | 81 | for ( let y = 0 ; y < frame.height ; y++ ) { 82 | for ( let x = 0 ; x < linesize[0] ; x++ ) { 83 | ydata[y * linesize[0] + x] = x + y + i * 3; 84 | } 85 | } 86 | 87 | for ( let y = 0 ; y < frame.height / 2 ; y++) { 88 | for ( let x = 0; x < linesize[1] ; x++) { 89 | bdata[y * linesize[1] + x] = 128 + y + i * 2; 90 | cdata[y * linesize[1] + x] = 64 + x + i * 5; 91 | } 92 | } 93 | 94 | let packets = await encoder.encode(frame); 95 | if ( i % 10 === 0) console.log('Encoding frame', i); 96 | for (const pkt of packets.packets) { 97 | pkt.duration = 1; 98 | pkt.stream_index = vstr.index; 99 | pkt.pts = pkt.pts * 90000/25; 100 | pkt.dts = pkt.dts * 90000/25; 101 | await mux.writeFrame(pkt); 102 | outFile.write(pkt.data); 103 | } 104 | } 105 | 106 | let p2 = await encoder.flush(); 107 | console.log('Flushing', p2.packets.length, 'frames.'); 108 | for (const pkt of p2.packets) { 109 | pkt.duration = 1; 110 | pkt.stream_index = vstr.index; 111 | pkt.pts = pkt.pts * 90000/25; 112 | pkt.dts = pkt.dts * 90000/25; 113 | await mux.writeFrame(pkt); 114 | outFile.write(pkt.data); 115 | } 116 | await mux.writeTrailer(); 117 | outFile.end(endcode); 118 | 119 | console.log('Total time ', process.hrtime(start)); 120 | } 121 | 122 | if (typeof process.argv[2] === 'string') { run(); } 123 | else { console.error('Error: Please provide a file name.'); } 124 | -------------------------------------------------------------------------------- /test/packetSpec.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const test = require('tape'); 23 | const beamcoder = require('../index.js'); 24 | 25 | test('Create a packet', t => { 26 | let pkt = beamcoder.packet(); 27 | t.ok(pkt, 'is truthy.'); 28 | t.equal(typeof pkt._packet, 'object', 'external value present.'); 29 | t.deepEqual(pkt, { type: 'Packet', 30 | pts: null, 31 | dts: null, 32 | data: null, 33 | size: 0, 34 | stream_index: 0, 35 | flags: 36 | { KEY: false, 37 | CORRUPT: false, 38 | DISCARD: false, 39 | TRUSTED: false, 40 | DISPOSABLE: false }, 41 | side_data: null, 42 | duration: 0, 43 | pos: -1 }, 'has expected minimal value.'); 44 | t.end(); 45 | }); 46 | 47 | test('Minimal JSON serialization', t => { 48 | let pkt = beamcoder.packet(); 49 | t.equal(typeof pkt.toJSON, 'function', 'has hidden toJSON function.'); 50 | let ps = JSON.stringify(pkt); 51 | t.equal(typeof ps, 'string', 'stringify created a string.'); 52 | let pps = JSON.parse(ps); 53 | t.deepEqual(pps, { type: 'Packet', stream_index: 0 }, 'made minimal value.'); 54 | let rpkt = beamcoder.packet(ps); 55 | t.ok(rpkt, 'roundtrip packet is truthy.'); 56 | t.deepEqual(rpkt, { type: 'Packet', 57 | pts: null, 58 | dts: null, 59 | data: null, 60 | size: 0, 61 | stream_index: 0, 62 | flags: 63 | { KEY: false, 64 | CORRUPT: false, 65 | DISCARD: false, 66 | TRUSTED: false, 67 | DISPOSABLE: false }, 68 | side_data: null, 69 | duration: 0, 70 | pos: -1 }, 'has expected minimal value.'); 71 | t.end(); 72 | }); 73 | 74 | test('Maximal JSON serialization', t => { 75 | let pkt = beamcoder.packet({ type: 'Packet', 76 | pts: 42, 77 | dts: 43, 78 | data: Buffer.from('wibble'), 79 | stream_index: 7, 80 | flags: 81 | { KEY: true, 82 | CORRUPT: false, 83 | DISCARD: false, 84 | TRUSTED: true, 85 | DISPOSABLE: false }, 86 | side_data: { replaygain: Buffer.from('wobble') }, 87 | duration: 44, 88 | pos: 45 }); 89 | let ps = JSON.stringify(pkt); 90 | t.equal(typeof ps, 'string', 'stringify created a string.'); 91 | let rpkt = beamcoder.packet(ps); 92 | t.ok(rpkt, 'roundtrip packet is truthy.'); 93 | t.deepEqual(rpkt, { type: 'Packet', 94 | pts: 42, 95 | dts: 43, 96 | data: null, 97 | size: 0, 98 | stream_index: 7, 99 | flags: 100 | { KEY: true, 101 | CORRUPT: false, 102 | DISCARD: false, 103 | TRUSTED: true, 104 | DISPOSABLE: false }, 105 | side_data: 106 | { type: 'PacketSideData', 107 | replaygain: Buffer.from([0x77, 0x6f, 0x62, 0x62, 0x6c, 0x65]) }, 108 | duration: 44, 109 | pos: 45 }, 'roundtrips expected values.'); 110 | t.end(); 111 | }); 112 | 113 | test('Reset packet data', t => { 114 | let p = beamcoder.packet({ pts: 42, size: 4321, data: Buffer.alloc(4321 + beamcoder.AV_INPUT_BUFFER_PADDING_SIZE) }); 115 | t.ok(Buffer.isBuffer(p.data), 'data is a buffer.'); 116 | t.notEqual(p.data.length, p.size, 'data length is greater than the packet size.'); 117 | t.equal(p.data.length, 4321 + beamcoder.AV_INPUT_BUFFER_PADDING_SIZE, 'length is as expected.'); 118 | p.data = null; 119 | t.equal(p.data, null, 'after reset, packet data is set to null.'); 120 | t.equal(p.size, 4321, 'after reset, size remains at original value.'); 121 | t.end(); 122 | }); 123 | -------------------------------------------------------------------------------- /types/CodecPar.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * CodecPar describes the properties of an encoded stream. 3 | */ 4 | export interface CodecPar { 5 | /** Object name. */ 6 | readonly type: 'CodecParameters' 7 | /** General type of the encoded data. */ 8 | codec_type: string 9 | /** Specific type of the encoded data (the codec used). */ 10 | codec_id: number 11 | /** The name corresponding to the codec_id. */ 12 | name: string 13 | /** Additional information about the codec (corresponds to the AVI FOURCC). */ 14 | codec_tag: string 15 | /** Extra binary data needed for initializing the decoder, codec-dependent. */ 16 | extradata: Buffer 17 | /** 18 | * - video: the pixel format. 19 | * - audio: the sample format. 20 | */ 21 | format: string 22 | /** The average bitrate of the encoded data (in bits per second). */ 23 | bit_rate: number 24 | /** 25 | * The number of bits per sample in the codedwords. 26 | * 27 | * This is basically the bitrate per sample. It is mandatory for a bunch of 28 | * formats to actually decode them. It's the number of bits for one sample in 29 | * the actual coded bitstream. 30 | * 31 | * This could be for example 4 for ADPCM 32 | * For PCM formats this matches bits_per_raw_sample 33 | * Can be 0 34 | */ 35 | bits_per_coded_sample: number 36 | /** 37 | * This is the number of valid bits in each output sample. If the 38 | * sample format has more bits, the least significant bits are additional 39 | * padding bits, which are always 0. Use right shifts to reduce the sample 40 | * to its actual size. For example, audio formats with 24 bit samples will 41 | * have bits_per_raw_sample set to 24, and format set to AV_SAMPLE_FMT_S32. 42 | * To get the original sample use "(int32_t)sample >> 8"." 43 | * 44 | * For ADPCM this might be 12 or 16 or similar 45 | * Can be 0 46 | */ 47 | bits_per_raw_sample: number 48 | /** Codec-specific bitstream restrictions that the stream conforms to. */ 49 | profile: string | number 50 | level: number 51 | /** Video only. The video frame width in pixels. */ 52 | width: number 53 | /** Video only. The video frame height in pixels. */ 54 | height: number 55 | /** 56 | * Video only. The aspect ratio (width / height) which a single pixel 57 | * should have when displayed. 58 | * 59 | * When the aspect ratio is unknown / undefined, the numerator should be 60 | * set to 0 (the denominator may have any value). 61 | */ 62 | sample_aspect_ratio: Array 63 | /** Video only. The order of the fields in interlaced video. */ 64 | field_order: string 65 | /** Video only. Additional colorspace characteristics. */ 66 | color_range: string 67 | color_primaries: string 68 | color_trc: string 69 | color_space: string 70 | chroma_location: string 71 | /** Video only. Number of delayed frames. */ 72 | video_delay: number 73 | /** Audio only. A description of the channel layout. */ 74 | channel_layout: string 75 | /** Audio only. The number of audio channels. */ 76 | channels: number 77 | /** Audio only. The number of audio samples per second. */ 78 | sample_rate: number 79 | /** 80 | * Audio only. The number of bytes per coded audio frame, required by some 81 | * formats. 82 | * 83 | * Corresponds to nBlockAlign in WAVEFORMATEX. 84 | */ 85 | block_align: number 86 | /** Audio only. Audio frame size, if known. Required by some formats to be static. */ 87 | frame_size: number 88 | /** 89 | * Audio only. The amount of padding (in samples) inserted by the encoder at 90 | * the beginning of the audio. I.e. this number of leading decoded samples 91 | * must be discarded by the caller to get the original audio without leading 92 | * padding. 93 | */ 94 | initial_padding: number 95 | /** 96 | * Audio only. The amount of padding (in samples) appended by the encoder to 97 | * the end of the audio. I.e. this number of decoded samples must be 98 | * discarded by the caller from the end of the stream to get the original 99 | * audio without any trailing padding. 100 | */ 101 | trailing_padding: number 102 | /** Audio only. Number of samples to skip after a discontinuity. */ 103 | seek_preroll: number 104 | /** Retun a JSON string containing the object properties. */ 105 | toJSON(): string 106 | } 107 | 108 | export function codecParameters(options?: { [key: string]: any }): CodecPar; 109 | -------------------------------------------------------------------------------- /examples/jpeg_filter_app.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | /* Main example from the README.md with added filtering. Run in a folder of media files. 23 | 24 | Will convert source pixel formats to 8-bit YUV 4:2:2 25 | */ 26 | 27 | const beamcoder = require('../index.js'); // Use require('beamcoder') externally 28 | const Koa = require('koa'); // Add koa to package.json dependencies 29 | const app = new Koa(); 30 | 31 | app.use(async (ctx) => { // Assume HTTP GET with path // 32 | let parts = ctx.path.split('/'); // Split the path into filename and time 33 | if ((parts.length < 3) || (isNaN(+parts[2]))) return; // Ignore favicon etc.. 34 | let dm = await beamcoder.demuxer('file:' + parts[1]); // Probe the file 35 | await dm.seek({ time: +parts[2] }); // Seek to the closest keyframe to time 36 | let packet = await dm.read(); // Find the next video packet (assumes stream 0) 37 | for ( ; packet.stream_index !== 0 ; packet = await dm.read() ); 38 | let dec = beamcoder.decoder({ demuxer: dm, stream_index: 0 }); // Create a decoder 39 | let decResult = await dec.decode(packet); // Decode the frame 40 | if (decResult.frames.length === 0) // Frame may be buffered, so flush it out 41 | decResult = await dec.flush(); 42 | 43 | // audio test 44 | const aindex = 2; 45 | const audStr = dm.streams[aindex]; 46 | // console.log(audStr); 47 | let adec = beamcoder.decoder({ demuxer: dm, stream_index: aindex }); // Create a decoder 48 | // console.log(adec); 49 | let apkt = await dm.read(); 50 | let afrm = await adec.decode(apkt); 51 | console.log(afrm.frames); 52 | const audEnc = beamcoder.encoder({ 53 | name: 'aac', 54 | sample_fmt: 'fltp', 55 | sample_rate: 48000, 56 | channels: 1, 57 | channel_layout: 'mono', }); 58 | 59 | const audFilt = await beamcoder.filterer({ // Create a filterer for audio 60 | filterType: 'audio', 61 | inputParams: [{ 62 | sampleRate: audStr.codecpar.sample_rate, 63 | sampleFormat: adec.sample_fmt, 64 | channelLayout: audStr.codecpar.channel_layout, 65 | timeBase: audStr.time_base }], 66 | outputParams: [{ 67 | sampleRate: 1024, 68 | sampleFormat: 'fltp', 69 | channelLayout: 'mono' }], 70 | filterSpec: 'aresample=1024' }); 71 | 72 | const audFiltPkt = await audFilt.filter([{ frames: afrm }]); 73 | const encPkt = await audEnc.encode(audFiltPkt[0].frames[0]); 74 | console.log(encPkt); 75 | 76 | let vstr = dm.streams[0]; // Select the video stream (assumes stream 0) 77 | let filt = await beamcoder.filterer({ // Create a filterer for video 78 | filterType: 'video', 79 | inputParams: [{ 80 | width: vstr.codecpar.width, 81 | height: vstr.codecpar.height, 82 | pixelFormat: vstr.codecpar.format, 83 | timeBase: vstr.time_base, 84 | pixelAspect: vstr.sample_aspect_ratio }], 85 | outputParams: [{ pixelFormat: 'yuv422p' }], 86 | filterSpec: 'scale=640:360, colorspace=range=jpeg:all=bt709' }); 87 | let filtResult = await filt.filter([{ frames: decResult }]); // Filter the frame 88 | let filtFrame = filtResult[0].frames[0]; 89 | let enc = beamcoder.encoder({ // Create an encoder for JPEG data 90 | name : 'mjpeg', // FFmpeg does not have an encoder called 'jpeg' 91 | width : filtFrame.width, 92 | height: filtFrame.height, 93 | pix_fmt: 'yuvj422p', 94 | time_base: [1, 1] }); 95 | let jpegResult = await enc.encode(filtFrame); // Encode the filtered frame 96 | await enc.flush(); // Tidy the encoder 97 | ctx.type = 'image/jpeg'; // Set the Content-Type of the data 98 | ctx.body = jpegResult.packets[0].data; // Return the JPEG image data 99 | }); 100 | 101 | app.listen(3000); // Start the server on port 3000 102 | -------------------------------------------------------------------------------- /types/Encoder.d.ts: -------------------------------------------------------------------------------- 1 | import { CodecPar } from "./CodecPar" 2 | import { Packet } from "./Packet"; 3 | import { Frame } from "./Frame"; 4 | import { Codec } from "./Codec" 5 | import { CodecContext } from "./CodecContext" 6 | 7 | /** The EncodedPackets object is returned as the result of a encode operation */ 8 | export interface EncodedPackets { 9 | /** Object name. */ 10 | readonly type: 'packets' 11 | /** 12 | * Encoded packets that are now available. If the array is empty, the encoder has buffered 13 | * the frame as part of the process of producing future packets 14 | */ 15 | readonly packets: Array 16 | /** Total time in microseconds that the encode operation took to complete */ 17 | readonly total_time: number 18 | } 19 | /** 20 | * Encoder takes a stream of uncompressed data in the form of Frames and converts them into coded Packets. 21 | * Encoding takes place on a single type of stream, for example audio or video. 22 | */ 23 | export interface Encoder extends Omit { 30 | readonly type: 'encoder' 31 | readonly extradata: Buffer | null 32 | readonly slice_count: number 33 | readonly slice_offset: Array | null 34 | readonly bits_per_coded_sample: number 35 | 36 | /** 37 | * Encode a Frame or array of Frames and create a compressed Packet or Packets. 38 | * Encoders may need more than one Frame to produce a Packet and may subsequently 39 | * produce more than one Packet per Frame. This is particularly the case for long-GOP formats. 40 | * @param frame A Frame or an array of Frames to be encoded 41 | * @returns a promise that resolves to a EncodedPackets object when the encode has completed successfully 42 | */ 43 | encode(frame: Frame | Frame[]): Promise 44 | /** 45 | * Encode a number of Frames passed as separate parameters and create compressed Packets 46 | * Encoders may need more than one Frame to produce a Packet and may subsequently 47 | * produce more than one Packet per Frame. This is particularly the case for long-GOP formats. 48 | * @param frames An arbitrary number of Frames to be encoded 49 | * @returns a promise that resolves to a EncodedPackets object when the encode has completed successfully 50 | */ 51 | encode(...frames: Frame[]): Promise 52 | /** 53 | * Once all Frames have been passed to the encoder, it is necessary to call its 54 | * asynchronous flush() method. If any Packets are yet to be delivered by the encoder 55 | * they will be provided in the resolved value. 56 | * 57 | * Call the flush operation once and do not use the encoder for further encoding once it has 58 | * been flushed. The resources held by the encoder will be cleaned up as part of the Javascript 59 | * garbage collection process, so make sure that the reference to the encoder goes out of scope. 60 | * @returns a promise that resolves to a EncodedPackets object when the flush has completed successfully 61 | */ 62 | flush(): Promise 63 | /** 64 | * Extract the CodecPar object for the Encoder 65 | * @returns A CodecPar object 66 | */ 67 | extractParams(): any 68 | /** 69 | * Initialise the encoder with parameters from a CodecPar object 70 | * @param param The CodecPar object that is to be used to override the current Encoder parameters 71 | * @returns the modified Encoder object 72 | */ 73 | useParams(params: CodecPar): Encoder 74 | } 75 | 76 | /** 77 | * Provides a list and details of all the available encoders 78 | * @returns an object with name and details of each of the available encoders 79 | */ 80 | export function encoders(): { [key: string]: Codec } 81 | /** 82 | * Create an encoder by name 83 | * @param name The codec name required 84 | * @param ... Any non-readonly parameters from the Encoder object as required 85 | * @returns An Encoder object - note creation is synchronous 86 | */ 87 | export function encoder(options: { name: string, [key: string]: any }): Encoder 88 | /** 89 | * Create an encoder by codec_id 90 | * @param codec_id The codec ID from AV_CODEC_ID_xxx 91 | * @param ... Any non-readonly parameters from the Encoder object as required 92 | * @returns An Encoder object - note creation is synchronous 93 | */ 94 | export function encoder(options: { codec_id: number, [key: string]: any }): Encoder 95 | -------------------------------------------------------------------------------- /scratch/stream_avci.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const beamcoder = require('../index.js'); 23 | const fs = require('fs'); 24 | // const util = require('util'); 25 | 26 | async function run() { 27 | // let demuxer = await createDemuxer('../../media/dpp/AS11_DPP_HD_EXAMPLE_1.mxf'); 28 | let demuxerStream = beamcoder.demuxerStream({ highwaterMark: 65536 }); 29 | fs.createReadStream('../../media/dpp/AS11_DPP_HD_EXAMPLE_1.mxf').pipe(demuxerStream); 30 | let demuxer = await demuxerStream.demuxer({}); 31 | // console.log(demuxer); 32 | 33 | let decoder = await beamcoder.decoder({ name: 'h264' }); 34 | // console.log(decoder); 35 | 36 | const vidStream = demuxer.streams[0]; 37 | let filterer = await beamcoder.filterer({ 38 | filterType: 'video', 39 | inputParams: [ 40 | { 41 | name: 'in0:v', 42 | width: vidStream.codecpar.width, 43 | height: vidStream.codecpar.height, 44 | pixelFormat: vidStream.codecpar.format, 45 | timeBase: vidStream.time_base, 46 | pixelAspect: vidStream.sample_aspect_ratio 47 | }, 48 | { 49 | name: 'in1:v', 50 | width: vidStream.codecpar.width, 51 | height: vidStream.codecpar.height, 52 | pixelFormat: vidStream.codecpar.format, 53 | timeBase: vidStream.time_base, 54 | pixelAspect: vidStream.sample_aspect_ratio 55 | } 56 | ], 57 | outputParams: [ 58 | { 59 | name: 'out0:v', 60 | pixelFormat: 'yuv422p' 61 | } 62 | ], 63 | filterSpec: '[in0:v] scale=1280:720 [left]; [in1:v] scale=640:360 [right]; [left][right] overlay=format=auto:x=640 [out0:v]' 64 | }); 65 | // console.log(filterer.graph); 66 | // console.log(util.inspect(filterer.graph, {depth: null})); 67 | console.log(filterer.graph.dump()); 68 | 69 | // width = '1000'; 70 | // const scaleFilter = filterer.graph.filters.find(f => 'scale' === f.filter.name); 71 | // scaleFilter.priv = { width: 1000 }; 72 | // console.log(util.inspect(scaleFilter, {depth: null})); 73 | 74 | // const overlayFilter = filterer.graph.filters.find(f => 'overlay' === f.filter.name); 75 | // overlayFilter.priv = { x: 100, y: 100 }; 76 | // console.log(util.inspect(overlayFilter, {depth: null})); 77 | 78 | let encParams = { 79 | name: 'libx264', 80 | width: 1280, 81 | height: 720, 82 | // bit_rate: 10000000, 83 | time_base: [1, 25], 84 | framerate: [25, 1], 85 | // gop_size: 50, 86 | // max_b_frames: 1, 87 | pix_fmt: 'yuv422p', 88 | priv_data: { 89 | crf: 23 90 | // preset: 'slow', 91 | // profile: 'high422', 92 | // level: '4.2' 93 | } 94 | }; 95 | 96 | let encoder = beamcoder.encoder(encParams); 97 | // console.log(encoder); 98 | 99 | let outFile = fs.createWriteStream('wibble.h264'); 100 | 101 | // await demuxer.seek({ frame: 4200, stream_index: 0}); 102 | 103 | let packet = {}; 104 | for ( let x = 0 ; x < 10 && packet !== null; x++ ) { 105 | packet = await demuxer.read(); 106 | if (packet.stream_index == 0) { 107 | // console.log(packet); 108 | let frames = await decoder.decode(packet); 109 | // console.log(frames); 110 | let filtFrames = await filterer.filter([ 111 | { name: 'in0:v', frames: frames.frames }, 112 | { name: 'in1:v', frames: frames.frames }, 113 | ]); 114 | // console.log(filtFrames); 115 | 116 | let packets = await encoder.encode(filtFrames[0].frames[0]); 117 | // console.log(x, packets.totalTime); 118 | packets.packets.forEach(x => outFile.write(x.data)); 119 | } 120 | } 121 | let frames = await decoder.flush(); 122 | console.log('flush', frames.total_time, frames.frames.length); 123 | 124 | demuxerStream.destroy(); 125 | } 126 | 127 | run().catch(console.error); 128 | -------------------------------------------------------------------------------- /types/Demuxer.d.ts: -------------------------------------------------------------------------------- 1 | import { Packet } from "./Packet" 2 | import { InputFormat, FormatContext } from "./FormatContext" 3 | 4 | export interface SeekOptions { 5 | /** 6 | * The stream where to seek 7 | * Use in conjunction with property frame or timestamp 8 | */ 9 | stream_index?: number 10 | /** 11 | * Seek by the number of frames into a given stream 12 | * Use in conjunction with stream_index 13 | */ 14 | frame?: number 15 | /** 16 | * Seek forward to a keyframe in a given stream or file at a given timestamp 17 | * The timestamp is the presentation timestamp of the packet measured in the timebase of the stream 18 | * Use in conjunction with stream_index 19 | */ 20 | timestamp?: number 21 | /** 22 | * seek based on elapsed time from the beginning of the primary stream 23 | * (as determined by FFmpeg, normally the first video stream where available) 24 | */ 25 | time?: number 26 | /** 27 | * byte offset position into the file 28 | */ 29 | pos?: number 30 | /** 31 | * The backward Boolean-valued property is interpreted as: 32 | * true: find the nearest key frame before the timestamp 33 | * false: find the nearest keyframe after the timestamp 34 | */ 35 | backward?: boolean 36 | /** 37 | * The any Boolean-valued property enables seeking to both key and non-key frames 38 | */ 39 | any?: boolean 40 | } 41 | 42 | /** 43 | * The process of demuxing (de-multiplexing) extracts time-labelled packets of data 44 | * contained in a media stream or file. 45 | */ 46 | export interface Demuxer extends Omit { 50 | /** Object name. */ 51 | readonly type: 'demuxer' 52 | readonly iformat: InputFormat 53 | readonly url: string 54 | readonly duration: number 55 | 56 | /** 57 | * Beam coder offers FFmpeg's many options for seeking a particular frame in a file, 58 | * either by time reference, frame count or file position. 59 | * https://github.com/Streampunk/beamcoder#seeking 60 | * @param options an object that specifies details on how the seek is to be calculated. 61 | * @returns a promise that resolves when the seek has completed 62 | */ 63 | seek(options: SeekOptions): Promise 64 | /** 65 | * Read the next blob of data from the file or stream at the current position, 66 | * where that data could be from any of the streams. 67 | * Typically, a packet is one frame of video data or a data blob representing 68 | * a codec-dependent number of audio samples. 69 | * Use the stream_index property of returned packet to find out which stream it is 70 | * associated with and dimensions including height, width or audio sample rate. 71 | * https://github.com/Streampunk/beamcoder#reading-data-packets 72 | * @returns a promise that resolves to a Packet when the read has completed 73 | */ 74 | read(): Promise 75 | } 76 | 77 | /** 78 | * Provides a list and details of all the available demuxer input formats 79 | * @returns an object with details of all the available demuxer input formats 80 | */ 81 | export function demuxers(): { [key: string]: InputFormat } 82 | 83 | /** 84 | * Create a demuxer to read from a URL or filename 85 | * @param url a string describing the source to be read from (may contain %d for a sequence of numbered files). 86 | * @returns a promise that resolves to a Demuxer when it has determined sufficient 87 | * format details by consuming data from the source. The promise will wait indefinitely 88 | * until sufficient source data has been read. 89 | */ 90 | export function demuxer(url: string): Promise 91 | 92 | /** Object to provide additional metadata on Demuxer creation */ 93 | export interface DemuxerCreateOptions { 94 | /** String describing the source to be read from (may contain %d for a sequence of numbered files). */ 95 | url?: string 96 | /** Object that provides format details */ 97 | iformat?: InputFormat 98 | /** Object allowing additional information to be provided */ 99 | options?: { [key: string]: any } 100 | } 101 | /** 102 | * For formats that require additional metadata, such as the rawvideo format, 103 | * it may be necessary to pass additional information such as image size or pixel format to Demuxer creation. 104 | * @param options a DemuxerCreateOptions object 105 | * @returns a promise that resolves to a Demuxer when it has determined sufficient 106 | * format details by consuming data from the source. The promise will wait indefinitely 107 | * until sufficient source data has been read. 108 | */ 109 | export function demuxer(options: DemuxerCreateOptions): Promise 110 | -------------------------------------------------------------------------------- /test/codecParamsSpec.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const test = require('tape'); 23 | const beamcoder = require('../index.js'); 24 | 25 | test('Creating codec parameters', t => { 26 | let cps = beamcoder.codecParameters(); 27 | t.ok(cps, 'is truthy.'); 28 | t.equal(cps.name, 'none', 'has no name.'); 29 | t.equal(cps.codec_type, 'data', 'has data type.'); 30 | t.equal(cps.codec_id, 0, 'zero coded id.'); 31 | cps = beamcoder.codecParameters({ name: 'aac' }); 32 | t.equal(cps.name, 'aac', 'has expected name aac.'); 33 | t.equal(cps.codec_type, 'audio', 'has expected type.'); 34 | t.equal(cps.codec_id, 86018, 'zero coded id.'); 35 | cps = beamcoder.codecParameters({ name: 'h264', width: 1920 }); 36 | t.equal(cps.width, 1920, 'constructor parameter set ok.'); 37 | t.end(); 38 | }); 39 | 40 | test('Minimal JSON serialization', t => { 41 | let cp = beamcoder.codecParameters(); 42 | let cps = JSON.stringify(cp); 43 | t.equal(typeof cps, 'string', 'stringify creates a string.'); 44 | let cpj = JSON.parse(cps); 45 | t.deepEqual(cpj, { type: 'CodecParameters', codec_type: 'data', 46 | codec_id: 0, name: 'none'}, 'is minimal.'); 47 | let rcp = beamcoder.codecParameters(cps); 48 | t.ok(rcp, 'roundtrip parameters are truthy.'); 49 | t.deepEqual(rcp, { type: 'CodecParameters', 50 | codec_type: 'data', 51 | codec_id: 0, 52 | name: 'none', 53 | codec_tag: 0, 54 | extradata: null, 55 | format: null, 56 | bit_rate: 0, 57 | bits_per_coded_sample: 0, 58 | bits_per_raw_sample: 0, 59 | profile: -99, 60 | level: -99, 61 | width: 0, 62 | height: 0, 63 | sample_aspect_ratio: [ 0, 1 ], 64 | field_order: 'unknown', 65 | color_range: 'unknown', 66 | color_primaries: 'unknown', 67 | color_trc: 'unknown', 68 | color_space: 'unknown', 69 | chroma_location: 'unspecified', 70 | video_delay: 0, 71 | channel_layout: '0 channels', 72 | channels: 0, 73 | sample_rate: 0, 74 | block_align: 0, 75 | frame_size: 0, 76 | initial_padding: 0, 77 | trailing_padding: 0, 78 | seek_preroll: 0 }, 'has expected value.'); 79 | t.end(); 80 | }); 81 | 82 | test('Maximal JSON serialization', t=> { 83 | let cp = beamcoder.codecParameters({ 84 | codec_type: 'video', 85 | codec_id: 27, 86 | codec_tag: 'avc1', 87 | name: 'h264', 88 | extradata: Buffer.from('wibble'), 89 | format: 'yuv422p', 90 | bit_rate: 12345, 91 | bits_per_coded_sample: 42, 92 | bits_per_raw_sample: 43, 93 | profile: 3, 94 | level: 4, 95 | width: 44, 96 | height: 45, 97 | sample_aspect_ratio: [ 46, 47 ], 98 | field_order: 'progressive', 99 | color_range: 'pc', 100 | color_primaries: 'bt709', 101 | color_trc: 'bt709', 102 | color_space: 'bt709', 103 | chroma_location: 'left', 104 | video_delay: 48, 105 | channel_layout: 'stereo', 106 | channels: 2, 107 | sample_rate: 48000, 108 | block_align: 8, 109 | frame_size: 1920, 110 | initial_padding: 48, 111 | trailing_padding: 49, 112 | seek_preroll: 50 }); 113 | let cps = JSON.stringify(cp); 114 | t.equal(typeof cps, 'string', 'stringify makes a string.'); 115 | let rcp = beamcoder.codecParameters(cps); 116 | t.ok(rcp, 'roundtrip value is truthy.'); 117 | t.deepEqual(rcp, { type: 'CodecParameters', 118 | codec_type: 'video', 119 | codec_id: 27, 120 | codec_tag: 'avc1', 121 | name: 'h264', 122 | extradata: Buffer.from('wibble'), 123 | format: 'yuv422p', 124 | bit_rate: 12345, 125 | bits_per_coded_sample: 42, 126 | bits_per_raw_sample: 43, 127 | profile: 3, 128 | level: 4, 129 | width: 44, 130 | height: 45, 131 | sample_aspect_ratio: [ 46, 47 ], 132 | field_order: 'progressive', 133 | color_range: 'pc', 134 | color_primaries: 'bt709', 135 | color_trc: 'bt709', 136 | color_space: 'bt709', 137 | chroma_location: 'left', 138 | video_delay: 48, 139 | channel_layout: 'stereo', 140 | channels: 2, 141 | sample_rate: 48000, 142 | block_align: 8, 143 | frame_size: 1920, 144 | initial_padding: 48, 145 | trailing_padding: 49, 146 | seek_preroll: 50 }, 'has expected value.'); 147 | t.end(); 148 | }); 149 | -------------------------------------------------------------------------------- /test/frameSpec.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const test = require('tape'); 23 | const beamcoder = require('../index.js'); 24 | const util = require('util'); 25 | 26 | test('Create a frame', t => { 27 | let fr = beamcoder.frame(); 28 | t.ok(fr, 'is truthy.'); 29 | t.end(); 30 | }); 31 | 32 | test('Minimal JSON serialization', t => { 33 | let fr = beamcoder.frame({}); 34 | let fp = JSON.stringify(fr); 35 | t.ok(fp, 'JSON serialization is truthy.'); 36 | let pfp = JSON.parse(fp); 37 | t.deepEqual(pfp, { type: 'Frame', linesize: [], reordered_opaque: 0 }, 'makes minimal JSON.'); 38 | let rf = beamcoder.frame(fp); 39 | t.ok(rf, 'roundtrip is truthy.'); 40 | t.equal(util.inspect(rf), util.inspect(beamcoder.frame()), 'same as a new frame.'); 41 | t.end(); 42 | }); 43 | 44 | test('Maximal JSON serialization', t => { 45 | let fr = beamcoder.frame({ type: 'Frame', 46 | linesize: [42], 47 | width: 43, 48 | height: 44, 49 | nb_samples: 45, 50 | format: 'fltp', 51 | key_frame: false, 52 | pict_type: 'I', 53 | sample_aspect_ratio: [ 16, 9 ], 54 | pts: 46, 55 | pkt_dts: 47, 56 | coded_picture_number: 48, 57 | display_picture_number: 49, 58 | quality: 50, 59 | repeat_pict: 51, 60 | interlaced_frame: true, 61 | top_field_first: true, 62 | palette_has_changed: true, 63 | reordered_opaque: 52, 64 | sample_rate: 48000, 65 | channel_layout: 'stereo', 66 | data: [ Buffer.from('wibble wobble wibble wobble jelly on a place at least 42 chars and some more')], 67 | side_data: { replaygain: Buffer.from('wibble') }, 68 | flags: { CORRUPT: true, DISCARD: false }, 69 | color_range: 'tv', 70 | color_primaries: 'bt709', 71 | color_trc: 'bt709', 72 | colorspace: 'bt709', 73 | chroma_location: 'top', 74 | best_effort_timestamp: 53, 75 | pkt_pos: 54, 76 | pkt_duration: 55, 77 | metadata: { fred: 'ginger' }, 78 | decode_error_flags: { INVALID_BITSTREAM: true, MISSING_REFERENCE: false }, 79 | channels: 2, 80 | pkt_size: 56, 81 | crop_top: 57, 82 | crop_bottom: 58, 83 | crop_left: 59, 84 | crop_right: 60 }); 85 | t.ok(fr, 'frame is truthy.'); 86 | let fs = JSON.stringify(fr); 87 | t.equal(typeof fs, 'string', 'stringify created a string.'); 88 | let rfr = beamcoder.frame(fs); 89 | t.ok(rfr, 'roundtrip packet is truthy.'); 90 | t.deepEqual(util.inspect(rfr), util.inspect(beamcoder.frame({ 91 | linesize: [42], 92 | width: 43, 93 | height: 44, 94 | nb_samples: 45, 95 | format: 'fltp', 96 | key_frame: false, 97 | pict_type: 'I', 98 | sample_aspect_ratio: [ 16, 9 ], 99 | pts: 46, 100 | pkt_dts: 47, 101 | coded_picture_number: 48, 102 | display_picture_number: 49, 103 | quality: 50, 104 | repeat_pict: 51, 105 | interlaced_frame: true, 106 | top_field_first: true, 107 | palette_has_changed: true, 108 | reordered_opaque: 52, 109 | sample_rate: 48000, 110 | channel_layout: 'stereo', 111 | data: [], // Data does not roundtrip 112 | side_data: { replaygain: Buffer.from('wibble') }, 113 | flags: { CORRUPT: true, DISCARD: false }, 114 | color_range: 'tv', 115 | color_primaries: 'bt709', 116 | color_trc: 'bt709', 117 | colorspace: 'bt709', 118 | chroma_location: 'top', 119 | best_effort_timestamp: 53, 120 | pkt_pos: 54, 121 | pkt_duration: 55, 122 | metadata: { fred: 'ginger' }, 123 | decode_error_flags: { INVALID_BITSTREAM: true, MISSING_REFERENCE: false }, 124 | channels: 2, 125 | pkt_size: 56, 126 | crop_top: 57, 127 | crop_bottom: 58, 128 | crop_left: 59, 129 | crop_right: 60 })), 'roundtrips expected value.'); 130 | t.end(); 131 | }); 132 | 133 | test('Can delete data', t => { 134 | let f = beamcoder.frame({ width: 1920, height: 1080, format: 'yuv420p' }).alloc(); 135 | t.ok(Array.isArray(f.data), 'data is an array ...'); 136 | t.ok(f.data.every(x => Buffer.isBuffer(x)), '... of buffers.'); 137 | t.equal(f.data.length, 3, 'data buffer has length 3.'); 138 | t.deepEqual(f.data.map(x => x.length), 139 | f.linesize.map(x => x * f.height), 140 | 'buffer sizes as expected.'); 141 | f.data = null; 142 | t.ok(Array.isArray(f.data), 'After reset, data is an array ...'); 143 | t.equal(f.data.length, 0, 'of length zero.'); 144 | t.end(); 145 | }); 146 | -------------------------------------------------------------------------------- /types/Codec.d.ts: -------------------------------------------------------------------------------- 1 | import { PrivClass } from "./PrivClass" 2 | 3 | export interface Codec { 4 | /** Object name. */ 5 | readonly type: 'Codec' 6 | /** 7 | * Name of the codec implementation. 8 | * The name is globally unique among encoders and among decoders (but an 9 | * encoder and a decoder can share the same name). 10 | * This is the primary way to find a codec from the user perspective. 11 | */ 12 | readonly name: string 13 | /** Descriptive name for the codec, meant to be more human readable than name. */ 14 | readonly long_name: string 15 | /** String describing the media type */ 16 | readonly codec_type: 'unknown' | 'video' | 'audio' | 'data' | 'subtitle' | 'attachment' | 'nb' 17 | /** Number that identifies the syntax and semantics of the bitstream. */ 18 | readonly id: number 19 | /** true if codec is an decoder */ 20 | readonly decoder: boolean 21 | /** true if codec is an encoder */ 22 | readonly encoder: boolean 23 | /** Codec capabilities - see AV_CODEC_CAP_* */ 24 | readonly capabilities: { 25 | /** Decoder can use draw_horiz_band callback. */ 26 | DRAW_HORIZ_BAND: boolean 27 | /** Codec uses get_buffer() for allocating buffers and supports custom allocators. */ 28 | DR1: boolean 29 | TRUNCATED: boolean 30 | /** 31 | * Decoder requires flushing with NULL input at the end in order to 32 | * give the complete and correct output. 33 | 34 | * NOTE: If this flag is not set, the codec is guaranteed to never be fed with 35 | * with NULL data. The user can still send NULL data to the decode function, 36 | * but it will not be passed along to the codec unless this flag is set. 37 | * 38 | * The decoder has a non-zero delay and needs to be fed with avpkt->data=NULL, 39 | * avpkt->size=0 at the end to get the delayed data until the decoder no longer 40 | * returns frames. 41 | */ 42 | DELAY: boolean 43 | /** Codec can be fed a final frame with a smaller size. This can be used to prevent truncation of the last audio samples. */ 44 | SMALL_LAST_FRAME: boolean 45 | /** 46 | * Codec can output multiple frames per APacket 47 | * Normally demuxers return one frame at a time, demuxers which do not do 48 | * are connected to a parser to split what they return into proper frames. 49 | * This flag is reserved to the very rare category of codecs which have a 50 | * bitstream that cannot be split into frames without timeconsuming 51 | * operations like full decoding. Demuxers carrying such bitstreams thus 52 | * may return multiple frames in a packet. This has many disadvantages like 53 | * prohibiting stream copy in many cases thus it should only be considered 54 | * as a last resort. 55 | */ 56 | SUBFRAMES: boolean 57 | /** Codec is experimental and is thus avoided in favor of non experimental codecs */ 58 | EXPERIMENTAL: boolean 59 | /** Codec should fill in channel configuration and samplerate instead of container */ 60 | CHANNEL_CONF: boolean 61 | /** Codec supports frame-level multithreading. */ 62 | FRAME_THREADS: boolean 63 | /** Codec supports slice-based (or partition-based) multithreading. */ 64 | SLICE_THREADS: boolean 65 | /** Codec supports changed parameters at any point. */ 66 | PARAM_CHANGE: boolean 67 | /** Codec supports avctx->thread_count == 0 (auto). */ 68 | AUTO_THREADS: boolean 69 | /** Audio encoder supports receiving a different number of samples in each call. */ 70 | VARIABLE_FRAME_SIZE: boolean 71 | /** 72 | * Decoder is not a preferred choice for probing. 73 | * This indicates that the decoder is not a good choice for probing. 74 | * It could for example be an expensive to spin up hardware decoder, 75 | * or it could simply not provide a lot of useful information about 76 | * the stream. 77 | * A decoder marked with this flag should only be used as last resort 78 | * choice for probing. 79 | */ 80 | AVOID_PROBING: boolean 81 | /** Codec is intra only. */ 82 | INTRA_ONLY: boolean 83 | /** Codec is lossless. */ 84 | LOSSLESS: boolean 85 | /** Codec is backed by a hardware implementation. Typically used to identify a non-hwaccel hardware decoder. */ 86 | HARDWARE: boolean 87 | /** 88 | * Codec is potentially backed by a hardware implementation, but not necessarily. 89 | * This is used instead of the HARDWARE flag if the implementation provides some sort of internal fallback. 90 | */ 91 | HYBRID: boolean 92 | } 93 | /** Array of supported framerates (as a rational [num, den]), or null if unknown. */ 94 | readonly supported_framerates: ReadonlyArray> | null 95 | /** Array of supported pixel formats, or null if unknown. */ 96 | readonly pix_fmts: ReadonlyArray | null 97 | /** Array of supported audio samplerates, or null if unknown */ 98 | readonly supported_samplerates: ReadonlyArray | null 99 | /** Array of supported sample formats, or NULL if unknown, */ 100 | readonly sample_fmts: ReadonlyArray 101 | /** */ 102 | readonly channel_layouts: ReadonlyArray 103 | /** */ 104 | readonly max_lowres: number 105 | /** Class for private context */ 106 | readonly priv_class: PrivClass 107 | /** */ 108 | readonly profiles: ReadonlyArray | null 109 | /** */ 110 | readonly wrapper_name?: string 111 | /** */ 112 | readonly descriptor: { 113 | INTRA_ONLY: boolean 114 | LOSSY: boolean 115 | LOSSLESS: boolean 116 | REORDER: boolean 117 | BITMAP_SUB: boolean 118 | TEXT_SUB: boolean 119 | } 120 | } 121 | 122 | /** List the available codecs */ 123 | export function codecs(): { [key: string]: { encoder?: Codec, decoder?: Codec }} 124 | -------------------------------------------------------------------------------- /types/Muxer.d.ts: -------------------------------------------------------------------------------- 1 | import { Packet } from "./Packet" 2 | import { Frame } from "./Frame" 3 | import { Stream } from "./Stream" 4 | import { OutputFormat, FormatContext } from "./FormatContext" 5 | 6 | export interface Muxer extends Omit { 14 | /** Object name. */ 15 | type: 'muxer' 16 | 17 | /** 18 | * Open the output file or stream - requires that a filename or URL has been provided with the 19 | * creation of the muxer using the filename property 20 | * @returns Promise that resolves to _undefined_ on success 21 | */ 22 | openIO(): Promise 23 | /** 24 | * Open the output file or stream by passing in an object containing the filename or url. 25 | * @param openOptions An object containing the filename or url. The object can also contain an options 26 | * object to further configure the protocol with private data and a flags parameter to configure bytestream AVIO flags. 27 | * @returns Promise that resolves to _undefined_ on success or to an object with an unset property detailing 28 | * which of the properties could not be set. 29 | */ 30 | openIO(openOptions: { 31 | url?: string 32 | filename?: string 33 | options?: { [key: string]: any } 34 | flags?: { 35 | READ: boolean 36 | WRITE: boolean 37 | NONBLOCK: boolean 38 | DIRECT: boolean 39 | } 40 | }): Promise 41 | 42 | /** 43 | * In some cases, it is necessary to initialize the structures of the muxer before writing the header. 44 | * Allows passing in of private data to set private options of the muxer. 45 | * @returns Promise that resolves to an object that indicates whether the stream parameters were 46 | * intialised in writeHeader or initOutput, together with an unset property if any properties could not be set 47 | */ 48 | initOutput(options?: { [key:string]: any }) : Promise<{ 49 | INIT_IN: 'WRITE_HEADER' | 'INIT_OUTPUT' 50 | unset?: {[key: string]: any} 51 | }> 52 | /** 53 | * Write the header to the file, optionally passing in private data to set private options of the muxer. 54 | * This must be done even for formats that don't have a header as part of the internal structure 55 | * as this step also initializes the internal data structures for writing. 56 | * @returns Promise that resolves to an object that indicates whether the stream parameters were 57 | * intialised in writeHeader or initOutput, together with an unset property if any properties could not be set 58 | */ 59 | writeHeader(options?: { [key:string]: any }) : Promise<{ 60 | INIT_IN: 'WRITE_HEADER' | 'INIT_OUTPUT' 61 | unset?: {[key: string]: any} 62 | }> 63 | 64 | /** 65 | * Write media data to the file by sending a packet containing data for a media stream. 66 | * @param packet Packet of compressed data, must contain the stream index and timestamps measured in the 67 | * `time_base` of the stream. 68 | * @returns Promise that resolves to _undefined_ on success 69 | */ 70 | writeFrame(packet: Packet) : Promise 71 | /** 72 | * Write media data to the file by sending a packet containing data for a media stream. 73 | * @param options Object containing a packet property of a compressed data Packet, must contain the 74 | * stream index and timestamps measured in the `time_base` of the stream. 75 | * @returns Promise that resolves to _undefined_ on success 76 | */ 77 | writeFrame(options: { packet: Packet }) : Promise 78 | /** 79 | * Write media data to the file by sending a packet containing data for a media stream. 80 | * @param options Object containing a stream index property and a frame property of an 81 | * uncompressed Frame, which must contain the timestamps measured in the `time_base` of the stream. 82 | * @returns Promise that resolves to _undefined_ on success 83 | */ 84 | writeFrame(options: { frame: Frame, stream_index: number }) : Promise 85 | 86 | /** 87 | * Write the trailer at the end of the file or stream. It is written after the muxer has drained its 88 | * buffers of all remaining packets and frames. Writing the trailer also closes the file or stream. 89 | * @returns Promise that resolves to _undefined_ on success 90 | */ 91 | writeTrailer(): Promise 92 | 93 | /** 94 | * Abandon the muxing process and forcibly close the file or stream without completing it 95 | */ 96 | forceClose(): undefined 97 | } 98 | 99 | /** 100 | * Provides a list and details of all the available muxer output formats 101 | * @returns an object with details of all the available muxer output formats 102 | */ 103 | export function muxers(): { [key: string]: OutputFormat } 104 | 105 | /** Object to provide additional metadata on Muxer creation */ 106 | export interface MuxerCreateOptions { 107 | /** The name of a chosen OutputFormat */ 108 | name?: string 109 | format_name?: string 110 | /** String describing the destinatione to be written to (may contain %d for a sequence of numbered files). */ 111 | filename?: string 112 | /** Object that provides format details */ 113 | oformat?: OutputFormat 114 | /** Object allowing additional information to be provided */ 115 | [key: string]: any 116 | } 117 | /** 118 | * Create a muxer to write to a URL or filename 119 | * @param options a MuxerCreateOptions object 120 | * @returns A Muxer object 121 | */ 122 | export function muxer(options: MuxerCreateOptions): Muxer 123 | -------------------------------------------------------------------------------- /src/adaptor.h: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | #ifndef ADAPTOR_H 23 | #define ADAPTOR_H 24 | 25 | #include "node_api.h" 26 | #include 27 | #include 28 | #include 29 | 30 | template 31 | class Queue { 32 | public: 33 | Queue(uint32_t maxQueue) : mActive(true), mMaxQueue(maxQueue), qu(), m(), cv() {} 34 | ~Queue() {} 35 | 36 | void enqueue(T t) { 37 | std::unique_lock lk(m); 38 | while(mActive && (qu.size() >= mMaxQueue)) { 39 | cv.wait(lk); 40 | } 41 | qu.push(t); 42 | cv.notify_one(); 43 | } 44 | 45 | T dequeue() { 46 | std::unique_lock lk(m); 47 | while(mActive && qu.empty()) { 48 | cv.wait(lk); 49 | } 50 | T val = 0; 51 | if (!qu.empty()) { 52 | val = qu.front(); 53 | qu.pop(); 54 | cv.notify_one(); 55 | } 56 | return val; 57 | } 58 | 59 | size_t size() const { 60 | std::lock_guard lk(m); 61 | return qu.size(); 62 | } 63 | 64 | void quit() { 65 | std::lock_guard lk(m); 66 | mActive = false; 67 | if ((0 == qu.size()) || (qu.size() >= mMaxQueue)) { 68 | // ensure release of any blocked thread 69 | cv.notify_all(); 70 | } 71 | } 72 | 73 | private: 74 | bool mActive; 75 | uint32_t mMaxQueue; 76 | std::queue qu; 77 | mutable std::mutex m; 78 | std::condition_variable cv; 79 | }; 80 | 81 | class Chunk { 82 | public: 83 | Chunk(napi_ref bufRef, void *buf, size_t bufLen) 84 | : mBufRef(bufRef), mBuf(buf), mLen(bufLen), mLocalAlloc(nullptr == bufRef) {} 85 | ~Chunk() { if (mLocalAlloc) free(mBuf); } 86 | 87 | napi_ref buf_ref() const { return mBufRef; } 88 | const void *buf() const { return mBuf; } 89 | size_t len() const { return mLen; } 90 | 91 | private: 92 | const napi_ref mBufRef; 93 | void *mBuf; 94 | const size_t mLen; 95 | const bool mLocalAlloc; 96 | }; 97 | 98 | class Adaptor { 99 | public: 100 | Adaptor(uint32_t queueLen) 101 | : mQueue(new Queue(queueLen)), mCurChunk(nullptr), mChunkPos(0), m(), mBuf(1024) {} 102 | ~Adaptor() { 103 | delete mQueue; 104 | std::unique_lock lk(m); 105 | while (mDone.size()) { 106 | Chunk *chunk = mDone.back(); 107 | mDone.pop_back(); 108 | delete chunk; 109 | } 110 | mDone.clear(); 111 | } 112 | 113 | int write(const uint8_t *buf, int bufSize) { 114 | uint8_t *qBuf = (uint8_t *)malloc(bufSize); 115 | memcpy(qBuf, buf, bufSize); 116 | mQueue->enqueue(new Chunk(nullptr, qBuf, bufSize)); 117 | return bufSize; 118 | } 119 | 120 | void *read(size_t numBytes, size_t *bytesRead) { 121 | uint8_t *buf = (uint8_t *)malloc(numBytes); 122 | *bytesRead = fillBuf(buf, numBytes); 123 | if (numBytes != *bytesRead) { 124 | if (0 == *bytesRead) { 125 | free(buf); 126 | buf = nullptr; 127 | } 128 | else 129 | buf = (uint8_t *)realloc(buf, *bytesRead); 130 | } 131 | return buf; 132 | } 133 | 134 | void write(napi_ref bufRef, void *buf, size_t bufLen) { 135 | mQueue->enqueue(new Chunk(bufRef, buf, bufLen)); 136 | } 137 | 138 | int read(uint8_t *buf, int bufSize) { 139 | return fillBuf(buf, bufSize); 140 | } 141 | 142 | void finish() { mQueue->quit(); } 143 | 144 | napi_status finaliseBufs(napi_env env) { 145 | napi_status status = napi_ok; 146 | std::unique_lock lk(m); 147 | while (mDone.size()) { 148 | Chunk *chunk = mDone.back(); 149 | mDone.pop_back(); 150 | if (chunk->buf_ref()) 151 | status = napi_delete_reference(env, chunk->buf_ref()); 152 | delete chunk; 153 | if (napi_ok != status) break; 154 | } 155 | return status; 156 | } 157 | 158 | // convenience buffer for avio_alloc_context 159 | // - it shouldn't be needed but avformat_write_header crashes if no buffer is provided 160 | unsigned char *buf() { return &mBuf[0]; } 161 | int bufLen() const { return (int)mBuf.size(); } 162 | 163 | private: 164 | Queue *mQueue; 165 | std::vector mDone; 166 | Chunk *mCurChunk; 167 | size_t mChunkPos; 168 | mutable std::mutex m; 169 | std::vector mBuf; 170 | 171 | int fillBuf(uint8_t *buf, size_t numBytes) { 172 | int bufOff = 0; 173 | while (numBytes) { 174 | if (!mCurChunk || (mCurChunk && mCurChunk->len() == mChunkPos)) 175 | if (!nextChunk()) 176 | break; 177 | 178 | int curSize = FFMIN(numBytes, mCurChunk->len() - mChunkPos); 179 | void *srcBuf = (uint8_t *)mCurChunk->buf() + mChunkPos; 180 | memcpy(buf + bufOff, srcBuf, curSize); 181 | 182 | bufOff += curSize; 183 | mChunkPos += curSize; 184 | numBytes -= curSize; 185 | } 186 | 187 | return bufOff; 188 | } 189 | 190 | bool nextChunk() { 191 | if (mCurChunk) { 192 | std::unique_lock lk(m); 193 | mDone.push_back(mCurChunk); 194 | } 195 | 196 | mCurChunk = mQueue->dequeue(); 197 | mChunkPos = 0; 198 | return nullptr != mCurChunk; 199 | } 200 | }; 201 | 202 | #endif 203 | -------------------------------------------------------------------------------- /types/Stream.d.ts: -------------------------------------------------------------------------------- 1 | import { CodecPar } from "./CodecPar"; 2 | import { Packet } from "./Packet" 3 | 4 | export interface Disposition { 5 | DEFAULT?: boolean 6 | DUB?: boolean 7 | ORIGINAL?: boolean 8 | COMMENT?: boolean 9 | LYRICS?: boolean 10 | KARAOKE?: boolean 11 | /** 12 | * Track should be used during playback by default. 13 | * Useful for subtitle track that should be displayed 14 | * even when user did not explicitly ask for subtitles. 15 | */ 16 | FORCED?: boolean 17 | /** Stream for hearing impaired audiences */ 18 | HEARING_IMPAIRED?: boolean 19 | /** Stream for visual impaired audiences */ 20 | VISUAL_IMPAIRED?: boolean 21 | /** Stream without voice */ 22 | CLEAN_EFFECTS?: boolean 23 | /** 24 | * The stream is stored in the file as an attached picture/"cover art" (e.g. 25 | * APIC frame in ID3v2). The first (usually only) packet associated with it 26 | * will be returned among the first few packets read from the file unless 27 | * seeking takes place. It can also be accessed at any time in 28 | * Stream.attached_pic. 29 | */ 30 | ATTACHED_PIC?: boolean 31 | /** 32 | * The stream is sparse, and contains thumbnail images, often corresponding 33 | * to chapter markers. Only ever used with Disposition ATTACHED_PIC. 34 | */ 35 | TIMED_THUMBNAILS?: boolean 36 | /** To specify text track kind (different from subtitles default). */ 37 | CAPTIONS?: boolean 38 | DESCRIPTIONS?: boolean 39 | METADATA?: boolean 40 | /** Dependent audio stream (mix_type=0 in mpegts) */ 41 | DEPENDENT?: boolean 42 | /** Still images in video stream (still_picture_flag=1 in mpegts) */ 43 | STILL_IMAGE?: boolean 44 | } 45 | 46 | export interface EventFlags { 47 | METADATA_UPDATED?: boolean 48 | } 49 | 50 | /** 51 | * Stream describes the properties of a stream. 52 | */ 53 | export interface Stream { 54 | /** Object name. */ 55 | readonly type: 'Stream' 56 | /** The stream index in the container. */ 57 | readonly index: number 58 | /** 59 | * Format-specific stream ID. 60 | * decoding: set by beamcoder 61 | * encoding: set by the user, replaced by beamcoder if left unset 62 | */ 63 | id: number 64 | /** 65 | * This is the fundamental unit of time (in seconds) in terms 66 | * of which frame timestamps are represented. 67 | * 68 | * decoding: set by beamcoder 69 | * encoding: May be set by the caller before writeHeader() to 70 | * provide a hint to the muxer about the desired timebase. In 71 | * writeHeader(), the muxer will overwrite this field 72 | * with the timebase that will actually be used for the timestamps 73 | * written into the file (which may or may not be related to the 74 | * user-provided one, depending on the format). 75 | */ 76 | time_base: Array 77 | /** 78 | * Decoding: pts of the first frame of the stream in presentation order, in stream time base. 79 | * Only set this if you are absolutely 100% sure that the value you set 80 | * it to really is the pts of the first frame. 81 | * This may be undefined (AV_NOPTS_VALUE). 82 | * @note The ASF header does NOT contain a correct start_time the ASF 83 | * demuxer must NOT set this. 84 | */ 85 | start_time: number | null 86 | /** 87 | * Decoding: duration of the stream, in stream time base. 88 | * If a source file does not specify a duration, but does specify 89 | * a bitrate, this value will be estimated from bitrate and file size. 90 | * 91 | * Encoding: May be set by the caller before writeHeader() to 92 | * provide a hint to the muxer about the estimated duration. 93 | */ 94 | duration: number | null 95 | /** Number of frames in this stream if known or 0 */ 96 | nb_frames: number 97 | disposition: Disposition 98 | /** Selects which packets can be discarded at will and do not need to be demuxed. */ 99 | discard: 'none' | 'default' | 'nonref' | 'bidir' | 'nonintra' | 'nonkey' | 'all' 100 | /** 101 | * sample aspect ratio (0 if unknown) 102 | * - encoding: Set by user. 103 | * - decoding: Set by beamcoder. 104 | */ 105 | sample_aspect_ratio: Array 106 | 107 | metadata: Array 108 | /** 109 | * Average framerate 110 | * 111 | * - demuxing: May be set by beamcoder when creating the stream 112 | * - muxing: May be set by the caller before writeHeader(). 113 | */ 114 | avg_frame_rate: Array 115 | /** 116 | * For streams with AV_DISPOSITION_ATTACHED_PIC disposition, this packet 117 | * will contain the attached picture. 118 | * 119 | * decoding: set by beamcoder, must not be modified by the caller. 120 | * encoding: unused 121 | */ 122 | readonly attached_pic: Packet | null 123 | /** 124 | * An array of side data that applies to the whole stream (i.e. the 125 | * container does not allow it to change between packets). 126 | * 127 | * There may be no overlap between the side data in this array and side data 128 | * in the packets. I.e. a given side data is either exported by the muxer 129 | * (demuxing) / set by the caller (muxing) in this array, then it never 130 | * appears in the packets, or the side data is exported / sent through 131 | * the packets (always in the first packet where the value becomes known or 132 | * changes), then it does not appear in this array. 133 | * 134 | * - demuxing: Set by beamcoder when the stream is created. 135 | * - muxing: May be set by the caller before writeHeader(). 136 | */ 137 | side_data: { 138 | type: 'PacketSideData' 139 | [key: string]: Buffer | string 140 | } 141 | /** 142 | * Flags for the user to detect events happening on the stream. Flags must 143 | * be cleared by the user once the event has been handled. 144 | */ 145 | event_flags: EventFlags 146 | /** 147 | * Real base framerate of the stream. 148 | * This is the lowest framerate with which all timestamps can be 149 | * represented accurately (it is the least common multiple of all 150 | * framerates in the stream). Note, this value is just a guess! 151 | * For example, if the time base is 1/90000 and all frames have either 152 | * approximately 3600 or 1800 timer ticks, then r_frame_rate will be 50/1. 153 | */ 154 | r_frame_rate: Array 155 | /** 156 | * Codec parameters associated with this stream. 157 | * 158 | * - demuxing: filled by beamcoder on stream creation 159 | * - muxing: filled by the caller before writeHeader() 160 | */ 161 | codecpar: CodecPar 162 | 163 | /** Retun a JSON string containing the object properties. */ 164 | toJSON(): string 165 | } 166 | -------------------------------------------------------------------------------- /types/Decoder.d.ts: -------------------------------------------------------------------------------- 1 | import { CodecPar } from "./CodecPar" 2 | import { Packet } from "./Packet" 3 | import { Frame } from "./Frame" 4 | import { Codec } from "./Codec" 5 | import { CodecContext } from "./CodecContext" 6 | import { Demuxer } from "./Demuxer" 7 | 8 | /** The DecodedFrames object is returned as the result of a decode operation */ 9 | export interface DecodedFrames { 10 | /** Object name. */ 11 | readonly type: 'frames' 12 | /** 13 | * Decoded frames that are now available. If the array is empty, the decoder has buffered 14 | * the packet as part of the process of producing future frames 15 | */ 16 | readonly frames: Array 17 | /** Total time in microseconds that the decode operation took to complete */ 18 | readonly total_time: number 19 | } 20 | 21 | export interface Decoder extends Omit { 35 | readonly type: 'decoder' 36 | readonly time_base: Array 37 | readonly sample_aspect_ratio: Array 38 | readonly intra_matrix: Array | null 39 | readonly inter_matrix: Array | null 40 | readonly intra_dc_precision: number 41 | readonly refs: number 42 | readonly color_primaries?: string 43 | readonly color_trc: string 44 | readonly colorspace: string 45 | readonly color_range: string 46 | readonly chroma_sample_location: 'unspecified' | 'left' | 'center' | 'topleft' | 'top' | 'bottomleft' | 'bottom' 47 | readonly field_order: 'progressive' | 48 | 'top coded first, top displayed first' | 49 | 'bottom coded first, bottom displayed first' | 50 | 'top coded first, bottom displayed first' | 51 | 'bottom coded first, top displayed first' | 52 | 'unknown' 53 | readonly sample_fmt: string | null 54 | readonly audio_service_type: 'main' | 'effects' | 'visually-impaired' | 'hearing-impaired' | 'dialogue' | 55 | 'commentary' | 'emergency' | 'voice-over' | 'karaoke' | 'nb' 56 | readonly bits_per_raw_sample: number 57 | readonly profile: string | number 58 | readonly level: number 59 | readonly subtitle_header: Buffer | null 60 | readonly framerate: Array 61 | 62 | /** 63 | * Decode an encoded data packet or array of packets and create an uncompressed frame 64 | * or frames (may be a frames-worth of audio). 65 | * Decoders may need more than one packet to produce a frame and may subsequently 66 | * produce more than one frame per packet. This is particularly the case for long-GOP formats. 67 | * @param packet A packet or an array of packets to be decoded 68 | * @returns a promise that resolves to a DecodedFrames object when the decode has completed successfully 69 | */ 70 | decode(packet: Packet | Packet[]): Promise 71 | /** 72 | * Decode a number of packets passed as separate parameters and create uncompressed frames 73 | * (may be a frames-worth of audio). 74 | * Decoders may need more than one packet to produce a frame and may subsequently 75 | * produce more than one frame per packet. This is particularly the case for long-GOP formats. 76 | * @param packets An arbitrary number of packets to be decoded 77 | * @returns a promise that resolves to a DecodedFrames object when the decode has completed successfully 78 | */ 79 | decode(...packets: Packet[]): Promise 80 | /** 81 | * Once all packets have been passed to the decoder, it is necessary to call its 82 | * asynchronous flush() method. If any frames are yet to be delivered by the decoder 83 | * they will be provided in the resolved value. 84 | * 85 | * Call the flush operation once and do not use the decoder for further decoding once it has 86 | * been flushed. The resources held by the decoder will be cleaned up as part of the Javascript 87 | * garbage collection process, so make sure that the reference to the decoder goes out of scope. 88 | * @returns a promise that resolves to a DecodedFrames object when the flush has completed successfully 89 | */ 90 | flush(): Promise 91 | /** 92 | * Extract the CodecPar object for the Decoder 93 | * @returns A CodecPar object 94 | */ 95 | extractParams(): any 96 | /** 97 | * Initialise the decoder with parameters from a CodecPar object 98 | * @param param The CodecPar object that is to be used to override the current Decoder parameters 99 | * @returns the modified Decoder object 100 | */ 101 | useParams(params: CodecPar): Decoder 102 | } 103 | 104 | /** 105 | * Provides a list and details of all the available decoders 106 | * @returns an object with name and details of each of the available decoders 107 | */ 108 | export function decoders(): { [key: string]: Codec } 109 | /** 110 | * Create a decoder by name 111 | * @param name The codec name required 112 | * @param ... Any non-readonly parameters from the Decoder object as required 113 | * @returns A Decoder object - note creation is synchronous 114 | */ 115 | export function decoder(options: { name: string, [key: string]: any }): Decoder 116 | /** 117 | * Create a decoder by codec_id 118 | * @param codec_id The codec ID from AV_CODEC_ID_xxx 119 | * @param ... Any non-readonly parameters from the Decoder object as required 120 | * @returns A Decoder object - note creation is synchronous 121 | */ 122 | export function decoder(options: { codec_id: number, [key: string]: any }): Decoder 123 | /** 124 | * Create a decoder from a demuxer and a stream_index 125 | * @param demuxer An initialised Demuxer object 126 | * @param stream_index The stream number of the demuxer object to be used to initialise the decoder 127 | * @param ... Any non-readonly parameters from the Decoder object as required 128 | * @returns A Decoder object - note creation is synchronous 129 | */ 130 | export function decoder(options: { demuxer: Demuxer, stream_index: number, [key: string]: any }): Decoder 131 | /** 132 | * Create a decoder from a CodecPar object 133 | * @param params CodecPar object whose codec name or id will be used to initialise the decoder 134 | * @param ... Any non-readonly parameters from the Decoder object as required 135 | * @returns A Decoder object - note creation is synchronous 136 | */ 137 | export function decoder(options: { params: CodecPar, [key: string]: any }): Decoder 138 | -------------------------------------------------------------------------------- /install_ffmpeg.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings to FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const os = require('os'); 23 | const fs = require('fs'); 24 | const util = require('util'); 25 | const https = require('https'); 26 | const cp = require('child_process'); 27 | const [ mkdir, access, rename, execFile, exec ] = // eslint-disable-line 28 | [ fs.mkdir, fs.access, fs.rename, cp.execFile, cp.exec ].map(util.promisify); 29 | 30 | async function get(ws, url, name) { 31 | let received = 0; 32 | let totalLength = 0; 33 | return new Promise((comp, err) => { 34 | https.get(url, res => { 35 | res.pipe(ws); 36 | if (totalLength == 0) { 37 | totalLength = +res.headers['content-length']; 38 | } 39 | res.on('end', () => { 40 | process.stdout.write(`Downloaded 100% of '${name}'. Total length ${received} bytes.\n`); 41 | comp(); 42 | }); 43 | res.on('error', err); 44 | res.on('data', x => { 45 | received += x.length; 46 | process.stdout.write(`Downloaded ${received * 100/ totalLength | 0 }% of '${name}'.\r`); 47 | }); 48 | }).on('error', err); 49 | }); 50 | } 51 | 52 | async function inflate(rs, folder, name) { 53 | const unzip = require('unzipper'); 54 | 55 | return new Promise((comp, err) => { 56 | console.log(`Unzipping '${folder}/${name}'.`); 57 | rs.pipe(unzip.Extract({ path: folder })); 58 | rs.on('close', () => { 59 | console.log(`Unzipping of '${folder}/${name}' completed.`); 60 | comp(); 61 | }); 62 | rs.on('error', err); 63 | }); 64 | } 65 | 66 | async function win32() { 67 | console.log('Installing FFmpeg dependencies for Beam Coder on Windows.'); 68 | await exec('npm install unzipper --no-save'); 69 | 70 | await mkdir('ffmpeg').catch(e => { 71 | if (e.code === 'EEXIST') return; 72 | else throw e; 73 | }); 74 | await access('ffmpeg/ffmpeg-4.2.1-win64-shared', fs.constants.R_OK).catch(async () => { 75 | let ws_shared = fs.createWriteStream('ffmpeg/ffmpeg-4.2.1-win64-shared.zip'); 76 | await get(ws_shared, 77 | 'https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip', 78 | 'ffmpeg-4.2.1-win64-shared.zip'); 79 | let rs_shared = fs.createReadStream('ffmpeg/ffmpeg-4.2.1-win64-shared.zip'); 80 | await inflate(rs_shared, 'ffmpeg', 'ffmpeg-4.2.1-win64-shared.zip'); 81 | }); 82 | await access('ffmpeg/ffmpeg-4.2.1-win64-dev', fs.constants.R_OK).catch(async () => { 83 | let ws_dev = fs.createWriteStream('ffmpeg/ffmpeg-4.2.1-win64-dev.zip'); 84 | await get(ws_dev, 85 | 'https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip', 86 | 'ffmpeg-4.2.1-win64-dev.zip'); 87 | let rs_dev = fs.createReadStream('ffmpeg/ffmpeg-4.2.1-win64-dev.zip'); 88 | console.log(await inflate(rs_dev, 'ffmpeg', 'ffmpeg-4.2.1-win64-dev.zip')); 89 | }); 90 | } 91 | 92 | async function linux() { 93 | console.log('Checking FFmpeg dependencies for Beam Coder on Linux.'); 94 | const { stdout } = await execFile('ldconfig', ['-p']).catch(console.error); 95 | let result = 0; 96 | 97 | if (stdout.indexOf('libavcodec.so.58') < 0) { 98 | console.error('libavcodec.so.58 is not installed.'); 99 | result = 1; 100 | } 101 | if (stdout.indexOf('libavformat.so.58') < 0) { 102 | console.error('libavformat.so.58 is not installed.'); 103 | result = 1; 104 | } 105 | if (stdout.indexOf('libavdevice.so.58') < 0) { 106 | console.error('libavdevice.so.58 is not installed.'); 107 | result = 1; 108 | } 109 | if (stdout.indexOf('libavfilter.so.7') < 0) { 110 | console.error('libavfilter.so.7 is not installed.'); 111 | result = 1; 112 | } 113 | if (stdout.indexOf('libavutil.so.56') < 0) { 114 | console.error('libavutil.so.56 is not installed.'); 115 | result = 1; 116 | } 117 | if (stdout.indexOf('libpostproc.so.55') < 0) { 118 | console.error('libpostproc.so.55 is not installed.'); 119 | result = 1; 120 | } 121 | if (stdout.indexOf('libswresample.so.3') < 0) { 122 | console.error('libswresample.so.3 is not installed.'); 123 | result = 1; 124 | } 125 | if (stdout.indexOf('libswscale.so.5') < 0) { 126 | console.error('libswscale.so.5 is not installed.'); 127 | result = 1; 128 | } 129 | 130 | if (result === 1) { 131 | console.log(`Try running the following (Ubuntu/Debian): 132 | sudo add-apt-repository ppa:jonathonf/ffmpeg-4 133 | sudo apt-get install libavcodec-dev libavformat-dev libavdevice-dev libavfilter-dev libavutil-dev libpostproc-dev libswresample-dev libswscale-dev`); 134 | process.exit(1); 135 | } 136 | return result; 137 | } 138 | 139 | async function darwin() { 140 | console.log('Checking for FFmpeg dependencies via HomeBrew.'); 141 | let output; 142 | let returnMessage; 143 | 144 | try { 145 | output = await exec('brew list ffmpeg'); 146 | returnMessage = 'FFmpeg already present via Homebrew.'; 147 | } catch (err) { 148 | if (err.stderr !== 'Error: No such keg: /usr/local/Cellar/ffmpeg\n') { 149 | console.error(err); 150 | console.log('Either Homebrew is not installed or something else is wrong.\nExiting'); 151 | process.exit(1); 152 | } 153 | 154 | console.log('FFmpeg not installed. Attempting to install via Homebrew.'); 155 | try { 156 | output = await exec('brew install nasm pkg-config texi2html ffmpeg'); 157 | returnMessage = 'FFmpeg installed via Homebrew.'; 158 | } catch (err) { 159 | console.log('Failed to install ffmpeg:\n'); 160 | console.error(err); 161 | process.exit(1); 162 | } 163 | } 164 | 165 | console.log(output.stdout); 166 | console.log(returnMessage); 167 | 168 | return 0; 169 | } 170 | 171 | switch (os.platform()) { 172 | case 'win32': 173 | if (os.arch() != 'x64') { 174 | console.error('Only 64-bit platforms are supported.'); 175 | process.exit(1); 176 | } else { 177 | win32(); 178 | } 179 | break; 180 | case 'linux': 181 | if (os.arch() != 'x64') { 182 | console.error('Only 64-bit platforms are supported.'); 183 | process.exit(1); 184 | } else { 185 | linux(); 186 | } 187 | break; 188 | case 'darwin': 189 | if (os.arch() != 'x64') { 190 | console.error('Only 64-bit platforms are supported.'); 191 | process.exit(1); 192 | } else { 193 | darwin(); 194 | } 195 | break; 196 | default: 197 | console.error(`Platfrom ${os.platform()} is not supported.`); 198 | break; 199 | } 200 | -------------------------------------------------------------------------------- /src/beamcoder_util.h: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | #ifndef BEAMCODER_UTIL_H 23 | #define BEAMCODER_UTIL_H 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include "node_api.h" 31 | 32 | extern "C" { 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | } 39 | 40 | #define DECLARE_NAPI_METHOD(name, func) { name, 0, func, 0, 0, 0, napi_default, 0 } 41 | #define DECLARE_GETTER(name, getter, this) { name, 0, 0, getter, nop, nullptr, napi_enumerable, this } 42 | #define DECLARE_GETTER2(name, test, getter, this) { name, 0, 0, test ? getter : nullptr, nop, nullptr, test ? napi_enumerable : napi_default, this } 43 | #define DECLARE_GETTER3(name, test, getter, this) if ( test ) { desc[count++] = { name, 0, 0, getter, nullptr, nullptr, napi_enumerable, this }; } 44 | // Handling NAPI errors - use "napi_status status;" where used 45 | #define CHECK_STATUS if (checkStatus(env, status, __FILE__, __LINE__ - 1) != napi_ok) return nullptr 46 | #define CHECK_BAIL if (checkStatus(env, status, __FILE__, __LINE__ - 1) != napi_ok) goto bail 47 | #define PASS_STATUS if (status != napi_ok) return status 48 | #define ACCEPT_STATUS(s) if ((status != s) && (status != napi_ok)) return status 49 | 50 | napi_status checkStatus(napi_env env, napi_status status, 51 | const char * file, uint32_t line); 52 | 53 | // High resolution timing 54 | #define HR_TIME_POINT std::chrono::high_resolution_clock::time_point 55 | #define NOW std::chrono::high_resolution_clock::now() 56 | long long microTime(std::chrono::high_resolution_clock::time_point start); 57 | 58 | // Argument processing 59 | napi_status checkArgs(napi_env env, napi_callback_info info, char* methodName, 60 | napi_value* args, size_t argc, napi_valuetype* types); 61 | 62 | // Async error handling 63 | #define BEAMCODER_ERROR_START 5000 64 | #define BEAMCODER_INVALID_ARGS 5001 65 | #define BEAMCODER_ERROR_READ_FRAME 5002 66 | #define BEAMCODER_ERROR_SEEK_FRAME 5003 67 | #define BEAMCODER_ERROR_ALLOC_DECODER 5004 68 | #define BEAMCODER_ERROR_EOF 5005 69 | #define BEAMCODER_ERROR_EINVAL 5006 70 | #define BEAMCODER_ERROR_ENOMEM 5007 71 | #define BEAMCODER_ERROR_DECODE 5008 72 | #define BEAMCODER_ERROR_OUT_OF_BOUNDS 5009 73 | #define BEAMCODER_ERROR_ALLOC_ENCODER 5010 74 | #define BEAMCODER_ERROR_ENCODE 5011 75 | #define BEAMCODER_ERROR_OPENIO 5012 76 | #define BEAMCODER_DICT_ERROR 5013 77 | #define BEAMCODER_ERROR_WRITE_HEADER 5014 78 | #define BEAMCODER_ERROR_INIT_OUTPUT 5015 79 | #define BEAMCODER_ERROR_WRITE_FRAME 5016 80 | #define BEAMCODER_ERROR_WRITE_TRAILER 5017 81 | #define BEAMCODER_ERROR_FILTER_ADD_FRAME 5018 82 | #define BEAMCODER_ERROR_FILTER_GET_FRAME 5019 83 | #define BEAMCODER_SUCCESS 0 84 | 85 | struct carrier { 86 | virtual ~carrier() {} 87 | napi_ref passthru = nullptr; 88 | int32_t status = BEAMCODER_SUCCESS; 89 | std::string errorMsg; 90 | long long totalTime; 91 | napi_deferred _deferred; 92 | napi_async_work _request = nullptr; 93 | }; 94 | 95 | void tidyCarrier(napi_env env, carrier* c); 96 | int32_t rejectStatus(napi_env env, carrier* c, char* file, int32_t line); 97 | 98 | #define REJECT_STATUS if (rejectStatus(env, c, (char*) __FILE__, __LINE__) != BEAMCODER_SUCCESS) return; 99 | #define REJECT_BAIL if (rejectStatus(env, c, (char*) __FILE__, __LINE__) != BEAMCODER_SUCCESS) goto bail; 100 | #define REJECT_RETURN if (rejectStatus(env, c, (char*) __FILE__, __LINE__) != BEAMCODER_SUCCESS) return promise; 101 | #define FLOATING_STATUS if (status != napi_ok) { \ 102 | printf("Unexpected N-API status not OK in file %s at line %d value %i.\n", \ 103 | __FILE__, __LINE__ - 1, status); \ 104 | } 105 | 106 | #define NAPI_THROW_ERROR(msg) { \ 107 | char errorMsg[256]; \ 108 | sprintf(errorMsg, "%s", msg); \ 109 | napi_throw_error(env, nullptr, errorMsg); \ 110 | return nullptr; \ 111 | } 112 | 113 | #define REJECT_ERROR(msg, status) { \ 114 | c->errorMsg = msg; \ 115 | c->status = status; \ 116 | REJECT_STATUS; \ 117 | } 118 | 119 | #define REJECT_ERROR_RETURN(msg, stat) { \ 120 | c->errorMsg = msg; \ 121 | c->status = stat; \ 122 | REJECT_RETURN; \ 123 | } 124 | 125 | napi_value nop(napi_env env, napi_callback_info info); 126 | char* avErrorMsg(const char* base, int avErrorCode); 127 | 128 | napi_status beam_set_uint32(napi_env env, napi_value target, const char* name, uint32_t value); 129 | napi_status beam_get_uint32(napi_env env, napi_value target, const char* name, uint32_t* value); 130 | napi_status beam_set_int32(napi_env env, napi_value target, const char* name, int32_t value); 131 | napi_status beam_get_int32(napi_env env, napi_value target, const char* name, int32_t* value); 132 | napi_status beam_set_int64(napi_env env, napi_value target, const char* name, int64_t value); 133 | napi_status beam_get_int64(napi_env env, napi_value target, const char* name, int64_t* value); 134 | napi_status beam_set_double(napi_env env, napi_value target, const char* name, double value); 135 | napi_status beam_get_double(napi_env env, napi_value target, const char* name, double* value); 136 | napi_status beam_set_string_utf8(napi_env env, napi_value target, const char* name, const char* value); 137 | napi_status beam_get_string_utf8(napi_env env, napi_value target, const char* name, char** value); 138 | napi_status beam_set_bool(napi_env env, napi_value target, const char* name, bool value); 139 | napi_status beam_get_bool(napi_env env, napi_value target, const char* name, bool* present, bool* value); 140 | napi_status beam_set_rational(napi_env env, napi_value target, const char* name, AVRational value); 141 | napi_status beam_get_rational(napi_env env, napi_value target, const char* name, AVRational* value); 142 | napi_status beam_set_null(napi_env env, napi_value target, const char* name); 143 | napi_status beam_is_null(napi_env env, napi_value props, const char* name, bool* isNull); 144 | napi_status beam_delete_named_property(napi_env env, napi_value props, const char* name, bool* deleted); 145 | 146 | #define BEAM_ENUM_UNKNOWN -42 147 | 148 | template 149 | std::unordered_map inverse_map(std::unordered_map &map) 150 | { 151 | std::unordered_map inv; 152 | std::for_each(map.begin(), map.end(), 153 | [&inv] (const std::pair &p) 154 | { 155 | inv.insert(std::make_pair(p.second, p.first)); 156 | }); 157 | return inv; 158 | } 159 | 160 | const char* beam_lookup_name(std::unordered_map m, int value); 161 | int beam_lookup_enum(std::unordered_map m, char* value); 162 | 163 | struct beamEnum { 164 | std::unordered_map forward; 165 | std::unordered_map inverse; 166 | beamEnum(std::unordered_map fwd) : forward(fwd), inverse(inverse_map(fwd)) {}; 167 | }; 168 | 169 | napi_status beam_set_enum(napi_env env, napi_value target, char* name, 170 | const beamEnum* enumDesc, int value); 171 | napi_status beam_get_enum(napi_env env, napi_value target, char* name, 172 | const beamEnum* enumDesc, int* value); 173 | 174 | extern const beamEnum* beam_field_order; 175 | extern const beamEnum* beam_ff_cmp; 176 | extern const beamEnum* beam_ff_mb_decision; 177 | extern const beamEnum* beam_av_audio_service_type; 178 | extern const beamEnum* beam_ff_compliance; 179 | extern const beamEnum* beam_ff_dct; 180 | extern const beamEnum* beam_ff_idct; 181 | extern const beamEnum* beam_avdiscard; 182 | extern const beamEnum* beam_ff_sub_charenc_mode; 183 | extern const beamEnum* beam_avmedia_type; 184 | extern const beamEnum* beam_option_type; 185 | extern const beamEnum* beam_avoid_neg_ts; 186 | extern const beamEnum* beam_avfmt_duration2; 187 | extern const beamEnum* beam_packet_side_data_type; 188 | extern const beamEnum* beam_frame_side_data_type; 189 | 190 | napi_value makeFrame(napi_env env, napi_callback_info info); 191 | 192 | struct avBufRef { 193 | napi_env env; 194 | napi_ref ref; 195 | int64_t pts = -1; 196 | }; 197 | 198 | napi_status fromAVClass(napi_env env, const AVClass* cls, napi_value* result); 199 | napi_status makeAVDictionary(napi_env env, napi_value options, AVDictionary** dict); 200 | 201 | napi_status fromContextPrivData(napi_env env, void *privData, napi_value* result); 202 | napi_status toContextPrivData(napi_env env, napi_value params, void* priv_data); 203 | 204 | #endif // BEAMCODER_UTIL_H 205 | -------------------------------------------------------------------------------- /src/governor.cc: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | #include "governor.h" 23 | #include "adaptor.h" 24 | 25 | struct readCarrier : carrier { 26 | ~readCarrier() { } 27 | Adaptor *adaptor; 28 | uint32_t readLen; 29 | void *readBuf = nullptr; 30 | }; 31 | 32 | void readFinalizer(napi_env env, void* data, void* hint) { 33 | free(data); 34 | 35 | int64_t externalMemory; 36 | int32_t len = (int32_t)(uint64_t)hint; 37 | if (BEAMCODER_SUCCESS != napi_adjust_external_memory(env, -len, &externalMemory)) 38 | printf("Error finalising governor read buffer %p, len %d\n", data, len); 39 | } 40 | 41 | void readExecute(napi_env env, void *data) { 42 | readCarrier* c = (readCarrier*) data; 43 | size_t bytesRead = 0; 44 | c->readBuf = c->adaptor->read(c->readLen, &bytesRead); 45 | c->readLen = bytesRead; 46 | } 47 | 48 | void readComplete(napi_env env, napi_status asyncStatus, void *data) { 49 | readCarrier* c = (readCarrier*) data; 50 | napi_value result; 51 | int64_t externalMemory; 52 | 53 | if (asyncStatus != napi_ok) { 54 | c->status = asyncStatus; 55 | c->errorMsg = "governor read failed to complete."; 56 | } 57 | REJECT_STATUS; 58 | 59 | c->status = napi_create_external_buffer(env, c->readLen, c->readBuf, readFinalizer, (void*)(uint64_t)c->readLen, &result); 60 | REJECT_STATUS; 61 | 62 | c->status = napi_adjust_external_memory(env, c->readLen, &externalMemory); 63 | REJECT_STATUS; 64 | 65 | napi_status status; 66 | status = napi_resolve_deferred(env, c->_deferred, result); 67 | FLOATING_STATUS; 68 | 69 | tidyCarrier(env, c); 70 | } 71 | 72 | napi_value read(napi_env env, napi_callback_info info) { 73 | napi_value promise; 74 | readCarrier* c = new readCarrier; 75 | 76 | c->status = napi_create_promise(env, &c->_deferred, &promise); 77 | REJECT_RETURN; 78 | 79 | size_t argc = 1; 80 | napi_value args[1]; 81 | napi_value governorValue; 82 | napi_status status = napi_get_cb_info(env, info, &argc, args, &governorValue, nullptr); 83 | REJECT_RETURN; 84 | 85 | if (argc < 1) { 86 | REJECT_ERROR_RETURN("governor read requires a read length as its argument.", 87 | BEAMCODER_INVALID_ARGS); 88 | } 89 | 90 | status = napi_get_value_uint32(env, args[0], &c->readLen); 91 | CHECK_STATUS; 92 | 93 | napi_value adaptorValue; 94 | status = napi_get_named_property(env, governorValue, "_adaptor", &adaptorValue); 95 | CHECK_STATUS; 96 | 97 | status = napi_get_value_external(env, adaptorValue, (void **)&c->adaptor); 98 | CHECK_STATUS; 99 | 100 | napi_value resourceName; 101 | c->status = napi_create_string_utf8(env, "Read", NAPI_AUTO_LENGTH, &resourceName); 102 | REJECT_RETURN; 103 | c->status = napi_create_async_work(env, nullptr, resourceName, readExecute, 104 | readComplete, c, &c->_request); 105 | REJECT_RETURN; 106 | c->status = napi_queue_async_work(env, c->_request); 107 | REJECT_RETURN; 108 | 109 | return promise; 110 | } 111 | 112 | struct writeCarrier : carrier { 113 | ~writeCarrier() { } 114 | Adaptor *adaptor; 115 | napi_ref bufRef; 116 | void *buf; 117 | size_t bufLen; 118 | }; 119 | 120 | void writeExecute(napi_env env, void *data) { 121 | writeCarrier* c = (writeCarrier*) data; 122 | c->adaptor->write(c->bufRef, c->buf, c->bufLen); 123 | }; 124 | 125 | void writeComplete(napi_env env, napi_status asyncStatus, void *data) { 126 | writeCarrier* c = (writeCarrier*) data; 127 | napi_value result; 128 | if (asyncStatus != napi_ok) { 129 | c->status = asyncStatus; 130 | c->errorMsg = "governor write failed to complete."; 131 | } 132 | REJECT_STATUS; 133 | 134 | c->status = napi_get_null(env, &result); 135 | REJECT_STATUS; 136 | 137 | napi_status status; 138 | status = napi_resolve_deferred(env, c->_deferred, result); 139 | FLOATING_STATUS; 140 | 141 | tidyCarrier(env, c); 142 | }; 143 | 144 | napi_value write(napi_env env, napi_callback_info info) { 145 | napi_value promise; 146 | writeCarrier* c = new writeCarrier; 147 | 148 | c->status = napi_create_promise(env, &c->_deferred, &promise); 149 | REJECT_RETURN; 150 | 151 | size_t argc = 1; 152 | napi_value args[1]; 153 | napi_value governorValue; 154 | napi_status status = napi_get_cb_info(env, info, &argc, args, &governorValue, nullptr); 155 | REJECT_RETURN; 156 | 157 | if (argc < 1) { 158 | REJECT_ERROR_RETURN("governor write requires a buffer as its argument.", 159 | BEAMCODER_INVALID_ARGS); 160 | } 161 | 162 | bool isBuffer; 163 | c->status = napi_is_buffer(env, args[0], &isBuffer); 164 | REJECT_RETURN; 165 | if (!isBuffer) { 166 | REJECT_ERROR_RETURN("governor write expects a node buffer", 167 | BEAMCODER_INVALID_ARGS); 168 | } 169 | 170 | napi_value bufferValue = args[0]; 171 | c->status = napi_create_reference(env, bufferValue, 1, &c->bufRef); 172 | REJECT_RETURN; 173 | 174 | c->status = napi_get_buffer_info(env, bufferValue, &c->buf, &c->bufLen); 175 | REJECT_RETURN; 176 | 177 | napi_value adaptorValue; 178 | status = napi_get_named_property(env, governorValue, "_adaptor", &adaptorValue); 179 | CHECK_STATUS; 180 | 181 | status = napi_get_value_external(env, adaptorValue, (void **)&c->adaptor); 182 | CHECK_STATUS; 183 | 184 | napi_value resourceName; 185 | c->status = napi_create_string_utf8(env, "Write", NAPI_AUTO_LENGTH, &resourceName); 186 | REJECT_RETURN; 187 | c->status = napi_create_async_work(env, nullptr, resourceName, writeExecute, 188 | writeComplete, c, &c->_request); 189 | REJECT_RETURN; 190 | c->status = napi_queue_async_work(env, c->_request); 191 | REJECT_RETURN; 192 | 193 | return promise; 194 | } 195 | 196 | napi_value finish(napi_env env, napi_callback_info info) { 197 | size_t argc = 0; 198 | napi_value governorValue; 199 | napi_status status = napi_get_cb_info(env, info, &argc, nullptr, &governorValue, nullptr); 200 | CHECK_STATUS; 201 | 202 | napi_value adaptorValue; 203 | status = napi_get_named_property(env, governorValue, "_adaptor", &adaptorValue); 204 | CHECK_STATUS; 205 | 206 | Adaptor *adaptor = nullptr; 207 | status = napi_get_value_external(env, adaptorValue, (void **)&adaptor); 208 | CHECK_STATUS; 209 | 210 | adaptor->finish(); 211 | 212 | napi_value result; 213 | napi_get_undefined(env, &result); 214 | return result; 215 | } 216 | 217 | void finalizeAdaptor(napi_env env, void* data, void* hint) { 218 | Adaptor *adaptor = (Adaptor *)data; 219 | delete adaptor; 220 | } 221 | 222 | napi_value governor(napi_env env, napi_callback_info info) { 223 | napi_status status; 224 | 225 | napi_value args[1]; 226 | size_t argc = 1; 227 | status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); 228 | CHECK_STATUS; 229 | 230 | if (argc != 1) { 231 | status = napi_throw_error(env, nullptr, "Wrong number of arguments to create governer."); 232 | return nullptr; 233 | } 234 | 235 | napi_value params = args[0]; 236 | napi_valuetype t; 237 | status = napi_typeof(env, params, &t); 238 | CHECK_STATUS; 239 | if (t != napi_object) { 240 | status = napi_throw_type_error(env, nullptr, "governer parameters must be an object."); 241 | return nullptr; 242 | } 243 | 244 | napi_value highWaterMarkVal; 245 | int32_t highWaterMark = 3; 246 | status = napi_get_named_property(env, params, "highWaterMark", &highWaterMarkVal); 247 | CHECK_STATUS; 248 | status = napi_typeof(env, highWaterMarkVal, &t); 249 | CHECK_STATUS; 250 | if (t == napi_number) { 251 | status = napi_get_value_int32(env, highWaterMarkVal, &highWaterMark); 252 | CHECK_STATUS; 253 | } 254 | 255 | napi_value governorObj; 256 | status = napi_create_object(env, &governorObj); 257 | CHECK_STATUS; 258 | 259 | Adaptor *adaptor = new Adaptor(highWaterMark); 260 | 261 | napi_value adaptorValue; 262 | status = napi_create_external(env, adaptor, finalizeAdaptor, nullptr, &adaptorValue); 263 | CHECK_STATUS; 264 | status = napi_set_named_property(env, governorObj, "_adaptor", adaptorValue); 265 | CHECK_STATUS; 266 | 267 | napi_value readValue; 268 | status = napi_create_function(env, "read", NAPI_AUTO_LENGTH, read, nullptr, &readValue); 269 | CHECK_STATUS; 270 | status = napi_set_named_property(env, governorObj, "read", readValue); 271 | CHECK_STATUS; 272 | 273 | napi_value writeValue; 274 | status = napi_create_function(env, "write", NAPI_AUTO_LENGTH, write, nullptr, &writeValue); 275 | CHECK_STATUS; 276 | status = napi_set_named_property(env, governorObj, "write", writeValue); 277 | CHECK_STATUS; 278 | 279 | napi_value finishValue; 280 | status = napi_create_function(env, "finish", NAPI_AUTO_LENGTH, finish, nullptr, &finishValue); 281 | CHECK_STATUS; 282 | status = napi_set_named_property(env, governorObj, "finish", finishValue); 283 | CHECK_STATUS; 284 | 285 | return governorObj; 286 | } 287 | -------------------------------------------------------------------------------- /types/Frame.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This object describes decoded (raw) audio or video data. 3 | */ 4 | export interface Frame { 5 | /** Object name. */ 6 | readonly type: 'Frame' 7 | /** 8 | * For video, size in bytes of each picture line. 9 | * For audio, size in bytes of each plane. 10 | * 11 | * For audio, only linesize[0] may be set. For planar audio, each channel 12 | * plane must be the same size. 13 | * 14 | * For video the linesizes should be multiples of the CPUs alignment 15 | * preference, this is 16 or 32 for modern desktop CPUs. 16 | * Some code requires such alignment other code can be slower without 17 | * correct alignment, for yet other it makes no difference. 18 | * 19 | * @note The linesize may be larger than the size of usable data -- there 20 | * may be extra padding present for performance reasons. 21 | */ 22 | linesize: Array 23 | /** 24 | * Video dimensions 25 | * Video frames only. The coded dimensions (in pixels) of the video frame, 26 | * i.e. the size of the rectangle that contains some well-defined values. 27 | * 28 | * @note The part of the frame intended for display/presentation is further 29 | * restricted by the "Cropping rectangle". 30 | */ 31 | width: number 32 | height: number 33 | /** number of audio samples (per channel) described by this frame */ 34 | nb_samples: number 35 | /** format of the frame, null if unknown or unset */ 36 | format: string | null 37 | /** Whether this frame is a keyframe */ 38 | key_frame: boolean 39 | /** Picture type of the frame. */ 40 | pict_type: 'I' | 'P' | 'B' | 'S' | 'SI' | 'SP' | 'BI' | null 41 | /** Sample aspect ratio for the video frame, 0/1 if unknown/unspecified. */ 42 | sample_aspect_ratio: Array 43 | /** Presentation timestamp in time_base units (time when frame should be shown to user). */ 44 | pts: number 45 | /** 46 | * DTS copied from the Packet that triggered returning this frame. (if frame threading isn't used) 47 | * This is also the Presentation time of this Frame calculated from 48 | * only Packet.dts values without pts values. 49 | */ 50 | pkt_dts: number 51 | /** picture number in bitstream order */ 52 | coded_picture_number: number 53 | /** picture number in display order */ 54 | display_picture_number: number 55 | /** quality (between 1 (good) and FF_LAMBDA_MAX (bad)) */ 56 | quality: number 57 | /** 58 | * When decoding, this signals how much the picture must be delayed. 59 | * extra_delay = repeat_pict / (2*fps) 60 | */ 61 | repeat_pict: number 62 | /** The content of the picture is interlaced. */ 63 | interlaced_frame: boolean 64 | /** If the content is interlaced, is top field displayed first. */ 65 | top_field_first: boolean 66 | /** Tell user application that palette has changed from previous frame. */ 67 | palette_has_changed: boolean 68 | /** 69 | * reordered opaque 64 bits (generally an integer or a double precision float 70 | * PTS but can be anything). 71 | * The user sets AVCodecContext.reordered_opaque to represent the input at 72 | * that time, 73 | * the decoder reorders values as needed and sets AVFrame.reordered_opaque 74 | * to exactly one of the values provided by the user through AVCodecContext.reordered_opaque 75 | * @deprecated in favor of pkt_pts 76 | */ 77 | reordered_opaque: number | null 78 | /** Sample rate of the audio data. */ 79 | sample_rate: number 80 | /** Channel layout of the audio data. */ 81 | channel_layout: string 82 | /** 83 | * Raw data for the picture/channel planes. 84 | * 85 | * Some decoders access areas outside 0,0 - width,height, please 86 | * see avcodec_align_dimensions2(). Some filters and swscale can read 87 | * up to 16 bytes beyond the planes, if these filters are to be used, 88 | * then 16 extra bytes must be allocated. 89 | */ 90 | data: Array 91 | /** 92 | * Additional data that can be provided by the container. 93 | * Frame can contain several types of side information. 94 | */ 95 | side_data: { type: string, [key: string]: Buffer | string } | null 96 | /** Frame flags */ 97 | flags: { 98 | CORRUPT?: boolean 99 | DISCARD?: boolean 100 | } 101 | /** MPEG vs JPEG YUV range. */ 102 | color_range: string 103 | /** Chromaticity coordinates of the source primaries. */ 104 | color_primaries?: string 105 | /** Color Transfer Characteristic. */ 106 | color_trc: string 107 | /** YUV colorspace type. */ 108 | colorspace: string 109 | /** 110 | * Location of chroma samples. 111 | * 112 | * Illustration showing the location of the first (top left) chroma sample of the 113 | * image, the left shows only luma, the right 114 | * shows the location of the chroma sample, the 2 could be imagined to overlay 115 | * each other but are drawn separately due to limitations of ASCII 116 | *``` 117 | * 1st 2nd 1st 2nd horizontal luma sample positions 118 | * v v v v 119 | * ______ ______ 120 | *1st luma line > |X X ... |3 4 X ... X are luma samples, 121 | *. | |1 2 1-6 are possible chroma positions 122 | *2nd luma line > |X X ... |5 6 X ... 0 is undefined/unknown position 123 | *``` 124 | */ 125 | chroma_location: 'unspecified' | 'left' | 'center' | 'topleft' | 'top' | 'bottomleft' | 'bottom' 126 | /** frame timestamp estimated using various heuristics, in stream time base */ 127 | best_effort_timestamp: number 128 | /** reordered pos from the last AVPacket that has been input into the decoder */ 129 | pkt_pos: number 130 | /** duration of the corresponding packet, expressed in Stream->time_base units, 0 if unknown. */ 131 | pkt_duration: number 132 | metadata: { [key: string]: string } 133 | /** 134 | * decode error flags of the frame, set if the decoder produced a frame, but there 135 | * were errors during the decoding. 136 | */ 137 | decode_error_flags: { 138 | INVALID_BITSTREAM: boolean 139 | MISSING_REFERENCE: boolean 140 | } 141 | /** number of audio channels, only used for audio. */ 142 | channels: number 143 | /** 144 | * size of the corresponding packet containing the compressed frame. 145 | * It is set to a negative value if unknown. 146 | */ 147 | pkt_size: number 148 | 149 | /** 150 | * Video frames only. The number of pixels to discard from the the 151 | * top/bottom/left/right border of the frame to obtain the sub-rectangle of 152 | * the frame intended for presentation. 153 | */ 154 | crop_top: number 155 | crop_bottom: number 156 | crop_left: number 157 | crop_right: number 158 | 159 | /** 160 | * Beam coder exposes some of FFmpeg's ability to calculate the size of data buffers. 161 | * If you pass width, height and format properties for video frames, or channels/channel_layout, 162 | * sample_rate and format for audio frames, as options to the frame constructor then the linesize 163 | * array (number of bytes per line per plane) is computed. For video, multiply each value by the 164 | * height to get the minimum buffer size for the plane. For audio, the first element of the array 165 | * is the buffer size for each plane. 166 | * 167 | * To use the linesize numbers to automatically allocate buffers of the correct size, 168 | * call alloc() after the factory method. For example: 169 | * `let f = beamcoder.frame({ width: 1920, height: 1080, format: 'yuv422p' }).alloc()` 170 | */ 171 | alloc(): Frame 172 | } 173 | 174 | /** 175 | * Create a frame for encoding or filtering 176 | * Set parameters as required from the Frame object 177 | */ 178 | export function frame(options: { [key: string]: any, data?: Array }): Frame 179 | 180 | /** Pixel format description */ 181 | export interface PixelFormat { 182 | name: string 183 | /** The number of components each pixel has, (1-4) */ 184 | nb_components: number 185 | /** 186 | * Amount to shift the luma height right to find the chroma height. 187 | * For YV12 this is 1 for example. 188 | * chroma_height= AV_CEIL_RSHIFT(luma_height, log2_chroma_h) 189 | * The note above is needed to ensure rounding up. 190 | * This value only refers to the chroma components. 191 | */ 192 | log2_chroma_h: number 193 | /** 194 | * Amount to shift the luma width right to find the chroma width. 195 | * For YV12 this is 1 for example. 196 | * chroma_width = AV_CEIL_RSHIFT(luma_width, log2_chroma_w) 197 | * The note above is needed to ensure rounding up. 198 | * This value only refers to the chroma components. 199 | */ 200 | log2_chroma_w: number 201 | flags: { 202 | /** Pixel format is big-endian. */ 203 | BE: boolean 204 | /** Pixel format has a palette in data[1], values are indexes in this palette. */ 205 | PAL: boolean 206 | /** All values of a component are bit-wise packed end to end. */ 207 | BITSTREAM: boolean 208 | /** Pixel format is an HW accelerated format. */ 209 | HWACCEL: boolean 210 | /** At least one pixel component is not in the first data plane. */ 211 | PLANAR: boolean 212 | /** The pixel format contains RGB-like data (as opposed to YUV/grayscale). */ 213 | RGB: boolean 214 | /** 215 | * The pixel format is "pseudo-paletted". This means that it contains a 216 | * fixed palette in the 2nd plane but the palette is fixed/constant for each 217 | * PIX_FMT. This allows interpreting the data as if it was PAL8, which can 218 | * in some cases be simpler. Or the data can be interpreted purely based on 219 | * the pixel format without using the palette. 220 | * An example of a pseudo-paletted format is AV_PIX_FMT_GRAY8 221 | * @deprecated This flag is deprecated, and will be removed. 222 | */ 223 | PSEUDOPAL: boolean 224 | /** 225 | * The pixel format has an alpha channel. This is set on all formats that 226 | * support alpha in some way, including AV_PIX_FMT_PAL8. The alpha is always 227 | * straight, never pre-multiplied. 228 | 229 | * If a codec or a filter does not support alpha, it should set all alpha to 230 | * opaque, or use the equivalent pixel formats without alpha component, e.g. 231 | * AV_PIX_FMT_RGB0 (or AV_PIX_FMT_RGB24 etc.) instead of AV_PIX_FMT_RGBA. 232 | */ 233 | ALPHA: boolean 234 | /** The pixel format is following a Bayer pattern. */ 235 | BAYER: boolean 236 | /** 237 | * The pixel format contains IEEE-754 floating point values. Precision (double, 238 | * single, or half) should be determined by the pixel size (64, 32, or 16 bits). 239 | */ 240 | FLOAT: boolean 241 | } 242 | comp: Array< { 243 | /** Code letter for the contents of the component */ 244 | code: 'R' | 'G' | 'B' | 'Y' | 'U' | 'V' | 'A' 245 | /** Which of the 4 planes contains the component. */ 246 | plane: number 247 | /** 248 | * Number of elements between 2 horizontally consecutive pixels. 249 | * Elements are bits for bitstream formats, bytes otherwise. 250 | */ 251 | step: number 252 | /** 253 | * Number of elements before the component of the first pixel. 254 | * Elements are bits for bitstream formats, bytes otherwise. 255 | */ 256 | offset: number 257 | /** Number of least significant bits that must be shifted away to get the value. */ 258 | shift: number 259 | /** Number of bits in the component. */ 260 | depth: number 261 | }> 262 | /** Alternative comma-separated names. */ 263 | alias: string 264 | } 265 | /** Format details for all supported pixel format names */ 266 | export function pix_fmts(): { [key: string]: PixelFormat } 267 | 268 | /** Audio sample formats */ 269 | export interface SampleFormat { 270 | type: 'SampleFormat' 271 | name: string 272 | /** The packed alternative form of the sample format. */ 273 | packed: string 274 | /** The planar alternative form of the sample format. */ 275 | planar: string 276 | /** Number of bytes per sample or zero if unknown. */ 277 | bytes_per_sample: number 278 | /** Whether the sample format is planar. */ 279 | is_planar: boolean 280 | } 281 | /** Format details for all supported sample format names */ 282 | export function sample_fmts(): { [key: string]: SampleFormat } 283 | 284 | /** 285 | * Note that when creating buffers from Javascript, 286 | * FFmpeg recommends that a small amount of headroom is added to the minimum length of each buffer. 287 | * The minimum amount of padding is exposed to Javascript as constant 288 | */ 289 | export const AV_INPUT_BUFFER_PADDING_SIZE: number 290 | -------------------------------------------------------------------------------- /test/formatSpec.js: -------------------------------------------------------------------------------- 1 | /* 2 | Aerostat Beam Coder - Node.js native bindings for FFmpeg. 3 | Copyright (C) 2019 Streampunk Media Ltd. 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | https://www.streampunk.media/ mailto:furnace@streampunk.media 19 | 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. 20 | */ 21 | 22 | const test = require('tape'); 23 | const beamcoder = require('../index.js'); 24 | 25 | const isExternal = o => Object.toString(o).indexOf('native code') >= 0; 26 | 27 | test('Creating a format', t => { 28 | let fmt = beamcoder.format(); 29 | t.ok(fmt, 'is truthy.'); 30 | t.equal(fmt.type, 'format', 'calls itself type format.'); 31 | t.equal(fmt.iformat, null, 'has no input format.'); 32 | t.equal(fmt.oformat, null, 'has no output format.'); 33 | t.equal(fmt.streams.length, 0, 'has no streams.'); 34 | fmt = beamcoder.format({ url: 'file:fred.wav', flags: { NONBLOCK: true }}); 35 | t.ok(fmt, 'with parameters is truthy.'); 36 | t.equal(fmt.url, 'file:fred.wav', 'has url property set as expected.'); 37 | t.ok(fmt.flags.NONBLOCK, 'NONBLOCK flag has been set OK.'); 38 | t.ok(fmt.flags.AUTO_BSF, 'AUTOBSF flag is still set.'); 39 | t.end(); 40 | }); 41 | 42 | const stripNewStream = ({ newStream, ...others }) => ({ ...others }); // eslint-disable-line no-unused-vars 43 | 44 | test('Minimal JSON serialization', t => { 45 | let fmt = beamcoder.format(); 46 | let fmts = JSON.stringify(fmt); 47 | t.equal(typeof fmts, 'string', 'stringify creates a string.'); 48 | let fmtj = JSON.parse(fmts); 49 | t.deepEqual(fmtj, { type: 'format', streams: [], interleaved: true }, 50 | 'is minimal.'); 51 | let rfmt = beamcoder.format(fmts); 52 | t.ok(rfmt, 'roundtrip format is truthy.'); 53 | console.log(stripNewStream(rfmt)); 54 | t.deepEqual(stripNewStream(rfmt), { 55 | type: 'format', 56 | oformat: null, 57 | iformat: null, 58 | priv_data: null, 59 | ctx_flags: { NOHEADER: false, UNSEEKABLE: false }, 60 | streams: [], 61 | url: '', 62 | start_time: 0, 63 | duration: 0, 64 | bit_rate: 0, 65 | packet_size: 0, 66 | max_delay: -1, 67 | flags: 68 | { GENPTS: false, 69 | IGNIDX: false, 70 | NONBLOCK: false, 71 | IGNDTS: false, 72 | NOFILLIN: false, 73 | NOPARSE: false, 74 | NOBUFFER: false, 75 | CUSTOM_IO: false, 76 | DISCARD_CORRUPT: false, 77 | FLUSH_PACKETS: false, 78 | BITEXACT: false, 79 | SORT_DTS: false, 80 | PRIV_OPT: false, 81 | FAST_SEEK: false, 82 | SHORTEST: false, 83 | AUTO_BSF: true }, 84 | probesize: 5000000, 85 | max_analyze_duration: 0, 86 | key: null, 87 | programs: [], 88 | max_index_size: 1048576, 89 | max_picture_buffer: 3041280, 90 | metadata: {}, 91 | start_time_realtime: null, 92 | fps_probe_size: -1, 93 | error_recognition: 1, 94 | debug: { TS: false }, 95 | max_interleave_delta: 10000000, 96 | strict_std_compliance: 'normal', 97 | event_flags: { METADATA_UPDATED: false }, 98 | max_ts_probe: 50, 99 | avoid_negative_ts: 'auto', 100 | audio_preload: 0, 101 | max_chunk_duration: 0, 102 | max_chunk_size: 0, 103 | use_wallclock_as_timestamps: false, 104 | avio_flags: 105 | { READ: false, WRITE: false, NONBLOCK: false, DIRECT: false }, 106 | duration_estimation_method: 'from_pts', 107 | skip_initial_bytes: 0, 108 | correct_ts_overflow: true, 109 | seek2any: false, 110 | flush_packets: -1, 111 | probe_score: 0, 112 | format_probesize: 1048576, 113 | codec_whitelist: null, 114 | format_whitelist: null, 115 | io_repositioned: false, 116 | metadata_header_padding: -1, 117 | output_ts_offset: 0, 118 | dump_separator: ', ', 119 | protocol_whitelist: null, 120 | protocol_blacklist: null, 121 | max_streams: 1000, 122 | skip_estimate_duration_from_pts: false, 123 | interleaved: true }, 'has expected value.'); 124 | t.end(); 125 | }); 126 | 127 | test('Maximal JSON serialization', t => { 128 | let fmt = beamcoder.format({ 129 | type: 'format', 130 | oformat: null, 131 | iformat: null, 132 | priv_data: null, 133 | ctx_flags: { NOHEADER: true, UNSEEKABLE: false }, 134 | streams: [], 135 | url: 'file:test.wav', 136 | start_time: 42, 137 | duration: 43, 138 | bit_rate: 44, 139 | packet_size: 45, 140 | max_delay: 46, 141 | flags: 142 | { GENPTS: true, 143 | IGNIDX: false, 144 | NONBLOCK: true, 145 | IGNDTS: false, 146 | NOFILLIN: true, 147 | NOPARSE: false, 148 | NOBUFFER: true, 149 | CUSTOM_IO: false, 150 | DISCARD_CORRUPT: true, 151 | FLUSH_PACKETS: false, 152 | BITEXACT: true, 153 | SORT_DTS: false, 154 | PRIV_OPT: true, 155 | FAST_SEEK: false, 156 | SHORTEST: true, 157 | AUTO_BSF: true }, 158 | probesize: 6000000, 159 | max_analyze_duration: 47, 160 | key: Buffer.from('Unlocker'), 161 | programs: [], 162 | max_index_size: 48, 163 | max_picture_buffer: 49, 164 | metadata: { wibble: 'wobble', otherwise: 50 }, 165 | start_time_realtime: 50, 166 | fps_probe_size: 51, 167 | error_recognition: 2, 168 | debug: { TS: true }, 169 | max_interleave_delta: 52, 170 | strict_std_compliance: 'strict', 171 | event_flags: { METADATA_UPDATED: true }, 172 | max_ts_probe: 53, 173 | avoid_negative_ts: 'make_zero', 174 | audio_preload: 54, 175 | max_chunk_duration: 55, 176 | max_chunk_size: 56, 177 | use_wallclock_as_timestamps: true, 178 | avio_flags: 179 | { READ: true, WRITE: false, NONBLOCK: true, DIRECT: false }, 180 | duration_estimation_method: 'from_stream', 181 | skip_initial_bytes: 57, 182 | correct_ts_overflow: false, 183 | seek2any: true, 184 | flush_packets: 58, 185 | probe_score: 59, 186 | format_probesize: 60, 187 | codec_whitelist: 'h264,hevc', 188 | format_whitelist: 'mxf,avi', 189 | io_repositioned: true, 190 | metadata_header_padding: 61, 191 | output_ts_offset: 62, 192 | dump_separator: '::', 193 | protocol_whitelist: 'http,rtp', 194 | protocol_blacklist: 'rtmp', 195 | max_streams: 53, 196 | skip_estimate_duration_from_pts: true, 197 | interleaved: false }); 198 | let fmts = JSON.stringify(fmt, null, 2); 199 | t.equal(typeof fmts, 'string', 'stringify creates a string.'); 200 | let fmtj = JSON.parse(fmts); 201 | t.ok(fmtj, 'parsed JSON is truthy.'); 202 | let rfmt = beamcoder.format(fmts); 203 | t.ok(rfmt, 'roundtrip format is truthy.'); 204 | t.deepEqual(stripNewStream(rfmt), { 205 | type: 'format', 206 | oformat: null, 207 | iformat: null, 208 | priv_data: null, 209 | ctx_flags: { NOHEADER: false, UNSEEKABLE: false }, // set by libav 210 | streams: [], 211 | url: 'file:test.wav', 212 | start_time: 0, // Always set by libav 213 | duration: 43, 214 | bit_rate: 44, 215 | packet_size: 45, 216 | max_delay: 46, 217 | flags: 218 | { GENPTS: true, 219 | IGNIDX: false, 220 | NONBLOCK: true, 221 | IGNDTS: false, 222 | NOFILLIN: true, 223 | NOPARSE: false, 224 | NOBUFFER: true, 225 | CUSTOM_IO: false, 226 | DISCARD_CORRUPT: true, 227 | FLUSH_PACKETS: false, 228 | BITEXACT: true, 229 | SORT_DTS: false, 230 | PRIV_OPT: true, 231 | FAST_SEEK: false, 232 | SHORTEST: true, 233 | AUTO_BSF: true }, 234 | probesize: 6000000, 235 | max_analyze_duration: 47, 236 | key: Buffer.from('Unlocker'), 237 | programs: [], 238 | max_index_size: 48, 239 | max_picture_buffer: 49, 240 | metadata: { wibble: 'wobble', otherwise: '50' }, // numbers become strings 241 | start_time_realtime: 50, 242 | fps_probe_size: 51, 243 | error_recognition: 2, 244 | debug: { TS: true }, 245 | max_interleave_delta: 52, 246 | strict_std_compliance: 'strict', 247 | event_flags: { METADATA_UPDATED: true }, 248 | max_ts_probe: 53, 249 | avoid_negative_ts: 'make_zero', 250 | audio_preload: 54, 251 | max_chunk_duration: 55, 252 | max_chunk_size: 56, 253 | use_wallclock_as_timestamps: true, 254 | avio_flags: 255 | { READ: true, WRITE: false, NONBLOCK: true, DIRECT: false }, 256 | duration_estimation_method: 'from_pts', // Read only, set by libav 257 | skip_initial_bytes: 57, 258 | correct_ts_overflow: false, 259 | seek2any: true, 260 | flush_packets: 58, 261 | probe_score: 0, // Read only, set by libav 262 | format_probesize: 60, 263 | codec_whitelist: 'h264,hevc', 264 | format_whitelist: 'mxf,avi', 265 | io_repositioned: false, // Set by libav 266 | metadata_header_padding: 61, 267 | output_ts_offset: 62, 268 | dump_separator: '::', 269 | protocol_whitelist: 'http,rtp', 270 | protocol_blacklist: 'rtmp', 271 | max_streams: 53, 272 | skip_estimate_duration_from_pts: true, 273 | interleaved: false }, 'has expected value.'); 274 | t.end(); 275 | }); 276 | 277 | test('Test minimal JSON stream', t => { 278 | let fmt = beamcoder.format(); 279 | let s1 = fmt.newStream('h264'); 280 | let ss = JSON.stringify(s1, null, 2); 281 | t.equal(typeof ss, 'string', 'stringify creates a string.'); 282 | console.log(ss); 283 | t.deepEqual(JSON.parse(ss), { 284 | type: 'Stream', index: 0, id: 0, time_base: [0,0], 285 | codecpar: { type: 'CodecParameters', codec_type: 'video', codec_id: 27, name: 'h264' } }); 286 | 287 | let s2 = fmt.newStream(ss); 288 | t.ok(s2, 'creation of new stream from string is truthy.'); 289 | console.log(s2.toJSON()); 290 | t.deepEqual(JSON.parse(JSON.stringify(s2)), { 291 | type: 'Stream', index: 1, id: 0, time_base: [0,0], 292 | codecpar: { type: 'CodecParameters', codec_type: 'video', codec_id: 27, name: 'h264' } }); 293 | t.ok(s2.codecpar && s2.codecpar._codecPar && isExternal(s2.codecpar._codecPar), 294 | 's2 has external codec parameters.'); 295 | t.ok(isExternal(s2._stream), 's2 has external _stream.'); 296 | 297 | let s3 = fmt.newStream(JSON.parse(ss)); 298 | t.ok(s3, 'creation of new stream from parsed object is truthy.'); 299 | t.deepEqual(JSON.parse(JSON.stringify(s3)), { 300 | type: 'Stream', index: 2, id: 0, time_base: [0,0], 301 | codecpar: { type: 'CodecParameters', codec_type: 'video', codec_id: 27, name: 'h264' } }); 302 | t.ok(s3.codecpar && s3.codecpar._codecPar && isExternal(s3.codecpar._codecPar), 303 | 's3 has external codec parameters.'); 304 | t.ok(isExternal(s3._stream), 's3 has external _stream.'); 305 | 306 | let fmt2 = beamcoder.format(JSON.stringify(fmt)); 307 | t.ok(fmt2, 'construction of new format form JSON is truthy.'); 308 | t.equal(fmt2.streams.length, 3, 'has expected number of streams.'); 309 | t.deepEqual(fmt2.streams.map(JSON.stringify).map(JSON.parse), 310 | [s1, s2, s3].map(JSON.stringify).map(JSON.parse), 'has expected streams.'); 311 | 312 | t.throws(() => fmt2.streams = [], /construction/, 313 | 'cannot set streams after construction.'); 314 | t.end(); 315 | }); 316 | 317 | test('Can set IO formats on construction', t => { 318 | let ifmt = beamcoder.format({ iformat: 'wav' }); 319 | t.ok(ifmt.iformat, 'iformat has become truthy.'); 320 | t.ok(ifmt.priv_data, 'private data has been created.'); 321 | t.equal(ifmt.type, 'demuxer', 'has turned into a demuxer.'); 322 | t.equal(ifmt.iformat.name, 'wav', 'iformat has the expected name.'); 323 | 324 | ifmt.iformat = null; 325 | t.equal(ifmt.iformat, null, 'can be set back to null.'); 326 | t.equal(ifmt.type, 'format', 'changing the name back to format.'); 327 | t.equal(ifmt.priv_data, null, 'resetting the priv_data.'); 328 | 329 | let ofmt = beamcoder.format({ oformat: 'hevc' }); 330 | t.ok(ofmt.oformat, 'oformat has become truthy.'); 331 | t.equal(ofmt.priv_data, null, 'private data is not set.'); 332 | t.equal(ofmt.type, 'muxer', 'has turned into a muxer.'); 333 | t.equal(ofmt.oformat.name, 'hevc', 'oformat has the expected name.'); 334 | 335 | ofmt.oformat = 'wav'; 336 | t.equal(ofmt.oformat.name, 'wav', 'oformat has the expected name.'); 337 | t.ok(ofmt.priv_data, 'has private data.'); 338 | t.equal(typeof ofmt.priv_data.write_bext, 'boolean', 'private data appears as expected.'); 339 | 340 | t.throws(() => { ifmt.iformat = 'wibble'; }, /Unable/, 'bad iformat name throws.'); 341 | t.throws(() => { ofmt.oformat = 'wibble'; }, /Unable/, 'bad oformat name throws.'); 342 | t.end(); 343 | }); 344 | -------------------------------------------------------------------------------- /types/Filter.d.ts: -------------------------------------------------------------------------------- 1 | import { Frame } from "./Frame" 2 | import { PrivClass } from "./PrivClass" 3 | 4 | export interface Filter { 5 | readonly type: 'Filter' 6 | /** Filter name. Must be non-NULL and unique among filters. */ 7 | readonly name: string 8 | /** A description of the filter. May be NULL. */ 9 | readonly description: string 10 | /** 11 | * List of inputs. 12 | * 13 | * NULL if there are no (static) inputs. Instances of filters with 14 | * AVFILTER_FLAG_DYNAMIC_INPUTS set may have more inputs than present in 15 | * this list. 16 | */ 17 | readonly inputs: ReadonlyArray 18 | /** 19 | * List of outputs. 20 | * 21 | * NULL if there are no (static) outputs. Instances of filters with 22 | * AVFILTER_FLAG_DYNAMIC_OUTPUTS set may have more outputs than present in 23 | * this list. 24 | */ 25 | readonly outputs: ReadonlyArray 26 | /** 27 | * A class for the private data, used to declare filter private AVOptions. 28 | * This field is NULL for filters that do not declare any options. 29 | */ 30 | readonly priv_class: PrivClass | null 31 | /** A combination of AVFILTER_FLAG_* */ 32 | readonly flags: { 33 | /** 34 | * The number of the filter inputs is not determined just by AVFilter.inputs. 35 | * The filter might add additional inputs during initialization depending on the 36 | * options supplied to it. 37 | */ 38 | DYNAMIC_INPUTS: boolean 39 | /** 40 | * The number of the filter outputs is not determined just by AVFilter.outputs. 41 | * The filter might add additional outputs during initialization depending on 42 | * the options supplied to it. 43 | */ 44 | DYNAMIC_OUTPUTS: boolean 45 | /** 46 | * The filter supports multithreading by splitting frames into multiple parts and 47 | * processing them concurrently. 48 | */ 49 | SLICE_THREADS: boolean 50 | /** 51 | * Some filters support a generic "enable" expression option that can be used 52 | * to enable or disable a filter in the timeline. Filters supporting this 53 | * option have this flag set. When the enable expression is false, the default 54 | * no-op filter_frame() function is called in place of the filter_frame() 55 | * callback defined on each input pad, thus the frame is passed unchanged to 56 | * the next filters. 57 | */ 58 | SUPPORT_TIMELINE_GENERIC: boolean 59 | /** 60 | * Same as AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC, except that the filter will 61 | * have its filter_frame() callback(s) called as usual even when the enable 62 | * expression is false. The filter will disable filtering within the 63 | * filter_frame() callback(s) itself, for example executing code depending on 64 | * the AVFilterContext->is_disabled value. 65 | */ 66 | SUPPORT_TIMELINE_INTERNAL: boolean 67 | /** 68 | * Handy mask to test whether the filter supports or no the timeline feature 69 | * (internally or generically). 70 | */ 71 | SUPPORT_TIMELINE: boolean 72 | } 73 | } 74 | 75 | export type MediaType = 'unknown' | 'video' | 'audio' | 'data' | 'subtitle' | 'attachment' | 'nb' 76 | 77 | export interface FilterPad { 78 | name: string 79 | media_type: MediaType 80 | } 81 | 82 | export interface FilterLink { 83 | /** source filter name */ 84 | readonly src: string 85 | /** output pad on the source filter */ 86 | readonly srcpad: string 87 | /** dest filter name */ 88 | readonly dst: string 89 | /** input pad on the dest filter */ 90 | readonly dstpad: string 91 | /** filter media type */ 92 | readonly type: MediaType 93 | /** video only - agreed upon image width */ 94 | readonly w?: number 95 | /** video only - agreed upon image height */ 96 | readonly h?: number 97 | /** video only - agreed upon sample aspect ratio */ 98 | readonly sample_aspect_ratio?: ReadonlyArray 99 | /** audio only - number of channels in the channel layout. */ 100 | readonly channel_count?: number 101 | /** audio only - channel layout of current buffer */ 102 | readonly channel_layout?: string 103 | /** audio only - samples per second */ 104 | readonly sample_rate?: number 105 | /** agreed upon media format */ 106 | readonly format: string 107 | /** 108 | * Define the time base used by the PTS of the frames/samples which will pass through this link. 109 | * During the configuration stage, each filter is supposed to change only the output timebase, 110 | * while the timebase of the input link is assumed to be an unchangeable property. 111 | */ 112 | readonly time_base: ReadonlyArray 113 | } 114 | 115 | export interface FilterContext { 116 | readonly type: 'FilterContext' 117 | /** the AVFilter of which this is an instance */ 118 | readonly filter: Filter 119 | /** name of this filter instance */ 120 | readonly name: string 121 | /** array of input pads */ 122 | readonly input_pads: ReadonlyArray 123 | /** array of pointers to input links */ 124 | readonly inputs: ReadonlyArray | null 125 | /** array of output pads */ 126 | readonly output_pads: ReadonlyArray 127 | /** array of pointers to output links */ 128 | readonly outputs: ReadonlyArray | null 129 | /** private data for use by the filter */ 130 | priv: { [key: string]: any } | null 131 | /** 132 | * Type of multithreading being allowed/used. A combination of 133 | * AVFILTER_THREAD_* flags. 134 | * 135 | * May be set by the caller before initializing the filter to forbid some 136 | * or all kinds of multithreading for this filter. The default is allowing 137 | * everything. 138 | * 139 | * When the filter is initialized, this field is combined using bit AND with 140 | * AVFilterGraph.thread_type to get the final mask used for determining 141 | * allowed threading types. I.e. a threading type needs to be set in both 142 | * to be allowed. 143 | * 144 | * After the filter is initialized, libavfilter sets this field to the 145 | * threading type that is actually used (0 for no multithreading). 146 | */ 147 | readonly thread_type: number 148 | /** 149 | * Max number of threads allowed in this filter instance. 150 | * If <= 0, its value is ignored. 151 | * Overrides global number of threads set per filter graph. 152 | */ 153 | readonly nb_threads: number 154 | /** 155 | * Ready status of the filter. 156 | * A non-0 value means that the filter needs activating, 157 | * a higher value suggests a more urgent activation. 158 | */ 159 | readonly ready: number 160 | /** 161 | * Sets the number of extra hardware frames which the filter will 162 | * allocate on its output links for use in following filters or by 163 | * the caller. 164 | * 165 | * Some hardware filters require all frames that they will use for 166 | * output to be defined in advance before filtering starts. For such 167 | * filters, any hardware frame pools used for output must therefore be 168 | * of fixed size. The extra frames set here are on top of any number 169 | * that the filter needs internally in order to operate normally. 170 | * 171 | * This field must be set before the graph containing this filter is 172 | * configured. 173 | */ 174 | readonly extra_hw_frames: number 175 | } 176 | 177 | export interface FilterGraph { 178 | readonly type: 'FilterGraph' 179 | 180 | readonly filters: ReadonlyArray 181 | /** sws options to use for the auto-inserted scale filters */ 182 | readonly scale_sws_opts: string | null 183 | /** 184 | * Type of multithreading allowed for filters in this graph. A combination of AVFILTER_THREAD_* flags. 185 | * May be set by the caller at any point, the setting will apply to all filters initialized after that. 186 | * The default is allowing everything. 187 | * 188 | * When a filter in this graph is initialized, this field is combined using bit AND with 189 | * AVFilterContext.thread_type to get the final mask used for determining allowed threading types. 190 | * I.e. a threading type needs to be set in both to be allowed. 191 | */ 192 | readonly thread_type: number 193 | /** 194 | * Maximum number of threads used by filters in this graph. May be set by 195 | * the caller before adding any filters to the filtergraph. Zero (the 196 | * default) means that the number of threads is determined automatically. 197 | */ 198 | readonly nb_threads: number 199 | /** 200 | * Dump a graph into a human-readable string representation. 201 | * @returns: String representation of the filter graph 202 | */ 203 | dump(): string 204 | } 205 | 206 | export interface FiltererResult { 207 | /** Output pad name in the filterSpec string used in the filterer setup. */ 208 | readonly name: string 209 | /** Array of output frames for the pad */ 210 | readonly frames: Array 211 | } 212 | 213 | export interface Filterer { 214 | readonly type: 'Filterer' 215 | readonly graph: FilterGraph 216 | 217 | /** 218 | * Filter an array of frames 219 | * For a filter that has only one input pass an array of frame objects directly 220 | * and the filter input will have a default name applied. 221 | * This name will match a filter specification that doesn't name its inputs. 222 | * @param frames Array of Frame objects to be applied to the single input pad 223 | * @returns Array of objects containing Frame arrays for each output pad of the filter 224 | */ 225 | filter(frames: Array): Promise & { total_time: number }> 226 | /** 227 | * Filter an array of frames 228 | * Pass an array of objects, one per filter input, each with a name string property 229 | * and a frames property that contains an array of frame objects 230 | * The name must match the input name in the filter specification 231 | * @param framesArr Array of objects with name and Frame array for each input pad 232 | * @returns Array of objects containing Frame arrays for each output pad of the filter 233 | */ 234 | filter(framesArr: Array<{ name: string, frames: Array }>): Promise & { total_time: number }> 235 | } 236 | 237 | /** 238 | * Provides a list and details of all the available filters 239 | * @returns an object with name and details of each of the available filters 240 | */ 241 | export function filters(): { [key: string]: Filter } 242 | 243 | /** List the available bitstream filters */ 244 | export function bsfs(): { 245 | [key: string]: { 246 | name: string 247 | codec_ids: Array 248 | priv_class: PrivClass | null } 249 | } 250 | 251 | /** The required parameters for setting up filter inputs */ 252 | export interface InputParam { 253 | /** 254 | * Input pad name that matches the filter specification string 255 | * For a single input filter without a name in the filter specification the name can be omitted 256 | */ 257 | name?: string 258 | /** Define the time base used by the PTS of the frames/samples for this filter. */ 259 | timeBase: Array 260 | } 261 | /** The required parameters for setting up video filter inputs */ 262 | export interface VideoInputParam extends InputParam { 263 | width: number 264 | height: number 265 | pixelFormat: string 266 | pixelAspect: Array 267 | } 268 | /** The required parameters for setting up audio filter inputs */ 269 | export interface AudioInputParam extends InputParam { 270 | sample_rate: number 271 | sample_format: string 272 | channel_layout: string 273 | } 274 | /** The required parameters for setting up filter inputs */ 275 | export interface OutputParam { 276 | /** 277 | * Output pad name that matches the filter specification string 278 | * For a single output filter without a name in the filter specification the name can be omitted 279 | */ 280 | name?: string 281 | } 282 | /** The required parameters for setting up video filter outputs */ 283 | export interface VideoOutputParam extends OutputParam { 284 | pixelFormat: string 285 | } 286 | /** The required parameters for setting up audio filter outputs */ 287 | export interface AudioOutputParam extends OutputParam { 288 | sample_rate: number 289 | sample_format: string 290 | channel_layout: string 291 | } 292 | 293 | export interface FiltererOptions { 294 | /** The filter type - video or audio */ 295 | filterType: MediaType 296 | filterSpec: string 297 | } 298 | 299 | export interface FiltererVideoOptions extends FiltererOptions { 300 | /** Video filter type */ 301 | filterType: 'video' 302 | /** Video input parameters for the filter */ 303 | inputParams: Array 304 | /** Video output parameters for the filter */ 305 | outputParams: Array 306 | } 307 | export interface FiltererAudioOptions extends FiltererOptions { 308 | /** Audio filter type */ 309 | filterType: 'audio' 310 | /** Audio input parameters for the filter */ 311 | inputParams: Array 312 | /** Audio output parameters for the filter */ 313 | outputParams: Array 314 | } 315 | 316 | /** 317 | * Create a filterer 318 | * @param options parameters to set up the type, inputs, outputs and spec of the filter 319 | * @returns Promise that resolve to a Filterer on success 320 | */ 321 | export function filterer(options: FiltererVideoOptions | FiltererAudioOptions): Promise 322 | --------------------------------------------------------------------------------