├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── docs └── images │ └── combining-example.jpg ├── lib ├── examples │ ├── simple.js │ └── simple.js.map ├── index.js ├── index.js.map ├── modules │ ├── CommandExecutor.js │ ├── CommandExecutor.js.map │ ├── Media.js │ ├── Media.js.map │ ├── Sequence.js │ ├── Sequence.js.map │ ├── SequenceStep.js │ ├── SequenceStep.js.map │ ├── User.js │ ├── User.js.map │ └── layouts │ │ ├── GridLayout.js │ │ ├── GridLayout.js.map │ │ ├── MosaicLayout.js │ │ ├── MosaicLayout.js.map │ │ ├── PresenterLayout.js │ │ ├── PresenterLayout.js.map │ │ ├── index.js │ │ └── index.js.map └── types │ ├── Types.js │ └── Types.js.map ├── package-lock.json ├── package.json ├── src ├── examples │ └── simple.ts ├── index.ts ├── modules │ ├── CommandExecutor.ts │ ├── Media.ts │ ├── Sequence.ts │ ├── SequenceStep.ts │ ├── User.ts │ └── layouts │ │ ├── GridLayout.ts │ │ ├── MosaicLayout.ts │ │ ├── PresenterLayout.ts │ │ └── index.ts └── types │ └── Types.ts ├── tsconfig.json ├── tslint.json └── videos ├── vid1.mp4 ├── vid2.mp4 └── vid3.mp4 /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | dist/ 4 | videos/ 5 | test 6 | 7 | test2 8 | 9 | test3 10 | 11 | output.txt 12 | 13 | out.txt 14 | 15 | class_diagram.uxf 16 | 17 | Annotation 2020-08-18 173007.png 18 | 19 | app-backup.ts 20 | 21 | lib/app-backup.js 22 | 23 | lib/app-backup.js.map 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dirk Vanbeveren 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video Conference Stitcher 2 | 3 | Combine separately recorded recordings from a video conference into a single video file. 4 | 5 | ## How it works 6 | 7 | This project is intended to combine separately recorded videos from a live video conference into a single recording. All this while having control of how the layout is. The input videos can start any time in the conference and the layout will change dynamically. 8 | 9 | The minimal information needed is: 10 | 11 | - start time of the recording (relative to the beginning of the call) 12 | - file of the recording (audio and video may or may not be separate) 13 | 14 | 15 | ![Alt text](docs/images/combining-example.jpg?raw=true "Encoding example") 16 | 17 | ## Requirements 18 | 19 | - [ffmpeg](https://ffmpeg.org/download.html) 20 | - linux system (windows not supported at this time, [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) is a good option for running on windows) 21 | 22 | ## Installation 23 | 24 | Install with npm with the following command: 25 | 26 | `npm install --save video-conference-stitcher` 27 | 28 | ## Getting started 29 | 30 | ### Run the example 31 | 32 | To run the example run the following command in the main folder: 33 | 34 | `npm run example` 35 | 36 | ### Example script 37 | 38 | ```typescript 39 | const path = require('path') 40 | const fs = require('fs') 41 | 42 | // import the classes 43 | const {User, Layouts, Sequence, Media} = require('video-conference-stitcher') 44 | const {PresenterLayout, GridLayout, MosaicLayout} = Layouts 45 | 46 | 47 | // variable for folders to get the videos from 48 | const videoFolder = path.join(__dirname, 'videos') 49 | fs.mkdir(videoFolder) 50 | 51 | // creating a lists of media (audio+video / audio / video) for each user 52 | const userMedia1:Media[] = [ 53 | new Media(path.join(videoFolder,'vid1.mp4'), 0, true, true), 54 | ] 55 | const userMedia2:Media[] = [ 56 | new Media(path.join(videoFolder,'vid2.mp4'), 2000, true, true) 57 | ] 58 | 59 | // Create users with their id, name and media files 60 | const users:User[] =[ 61 | new User('user1', userMedia1, 'KEVIN'), 62 | new User('user2', userMedia2, 'JEFF') 63 | ] 64 | 65 | // Select a layout to use 66 | const videoLayout:VideoLayout = new PresenterLayout() 67 | 68 | // Destination file 69 | const outputMedia: Media = new Media(path.join(videoFolder, 'basicOutput.mp4'), -1, true, true) 70 | 71 | // encoding options 72 | const encodingOptions = { 73 | crf: 20, 74 | loglevel: 'verbose', 75 | size:{ 76 | w: 1280, 77 | h: 720 78 | } 79 | } 80 | 81 | // Create a sequence with given settings 82 | const sequence = new Sequence(0, users,outputMedia, videoLayout, encodingOptions) 83 | 84 | // Encode the sequence, now the output file with the generated combined video is generated 85 | sequence.encode().then(comm => { 86 | console.log(comm) 87 | }) 88 | ``` 89 | 90 | 91 | 92 | ## Documentation 93 | 94 | ### Classes 95 | 96 | ### Media(path: string, startTime:number, hasVideo:boolean, hasAudio:boolean) 97 | 98 | Defines a single video / audio file on the file system. The file can contain audio and video at the same time or separately. 99 | 100 | ##### Parameters 101 | 102 | - **path**: Path to the media file as string. 103 | - **startTime**: Relative time when the media starts in ms. The time is the time elapsed from the start of the video conference (which is 0ms). 104 | - **hasVideo**: If the media file contains a video stream. 105 | - **hasAudio**: If the media file contains an audio stream. 106 | 107 | ##### Usage 108 | 109 | ```js 110 | const {Media} = require('video-conference-stitcher') 111 | const mediaObject = new Media('/home/user/vid.mp4', 2000, true, true) 112 | ``` 113 | 114 |   115 |   116 | 117 | ### User(id:string|number,media:Media[], name?:string|undefined) 118 | 119 | A user in the video conference. May have multiple streams. 120 | 121 | ##### Parameters 122 | 123 | - **id**: string / number with the id of the user. 124 | - **media**: List of media objects for the user. 125 | - **name**(optional): Display name of the user. 126 | 127 | ##### Usage 128 | 129 | ```js 130 | const {User Media} = require('video-conference-stitcher') 131 | 132 | const mediaList = [new Media(...), new Media(...)] 133 | 134 | const user = new User('user123', mediaList, 'Bob') 135 | OR 136 | const user = new User('user123', mediaList) 137 | ``` 138 | 139 |   140 |   141 | 142 | ### Layouts 143 | 144 | Objects that can be used for different output layouts or in other words change the arrangement of the separate video streams on the screen. 145 | 146 | Available: 147 | 148 | - Layouts.GridLayout 149 | - Layouts.PresenterLayout 150 | - Layouts.MosaicLayout 151 | 152 | ##### Usage 153 | 154 | ```js 155 | const {Layouts} = require('video-conference-stitcher') 156 | const videoLayout = new Layouts.GridLayout() 157 | const seq = new Sequence(.., ..,.., videoLayout, ..) 158 | ``` 159 | 160 |   161 |   162 | 163 | ### Sequence (users:User[], outputVideo:Media, layout:VideoLayout, encOpt?: EncodingOptions) 164 | 165 | ##### Parameters 166 | 167 | - **users**(Users[]): List of user objects to be encoded. 168 | - **outputVideo**: Media object for the output file. 169 | - **layout**: Layout of the combined videos. 170 | - **encOpt**(optional): Extra options for encoding. 171 | 172 | ##### Functions 173 | 174 | - **encode()**: Encodes the separate files into a single file with the given commands. 175 | - **generateCommand()**: Returns the ffmpeg command that will run as a string. 176 | 177 | ##### Usage 178 | 179 | ```js 180 | const {Sequence, User, Layouts} = require('video-conference-stitcher') 181 | 182 | const encodingOptions = {...} 183 | const videoLayout = new GridLayout() // other layouts possible 184 | const outputMedia = new Media(...) 185 | const users = [new User(...), new User(...)] 186 | 187 | const seq = new Sequence(users,outputMedia, videoLayout, encodingOptions) 188 | OR 189 | const seq = new Sequence(users,outputMedia, videoLayout) 190 | 191 | ``` 192 | 193 |   194 | 195 | ### Types 196 | 197 | ### EncodingOptions 198 | 199 | Object containing encoding options for encoding the sequence. 200 | 201 | ##### Properties 202 | 203 | - **crf**(optional): Quality parameter for encoding, more documentation [here](https://trac.ffmpeg.org/wiki/Encode/H.264). 204 | - default: 22 205 | - type: number 206 | - **bitrate**(optional): Setting output bitrate. Will be overridden if crf is also defined. 207 | - type: string 208 | - **size**: Resolution of the output video. 209 | - type: object -> {w: number, h: number} 210 | - **loglevel**(optional): Log level for ffmpeg. More documentation for loglevel in [ffmpeg documentation](https://ffmpeg.org/ffmpeg.html#toc-Generic-options) 211 | - options: number| 'quiet' | 'panic' | 'fatal' | 'error' | 'warning' | 'info' | 'verbose' | 'debug' | 'trace' 212 | 213 | ##### Usage 214 | 215 | ```js 216 | const encodingOptions = { 217 | crf: 20, 218 | size: { 219 | w: 1280, 220 | h: 720 221 | }, 222 | loglevel: 'quiet' 223 | } 224 | 225 | -------------------------- 226 | const encodingOptions = { 227 | bitrate: '2000M', 228 | size: { 229 | w: 1000, 230 | h: 500 231 | } 232 | } 233 | ``` 234 | 235 | 236 | 237 | ## Implementation in [mediasoup](https://mediasoup.org) 238 | 239 | A call can be recorded by connecting mediasoup to a recorder like **ffmpeg** or **gstreamer**. There is a good example for recording in **[THIS PROJECT](https://github.com/ethand91/mediasoup3-record-demo)**. 240 | 241 | The files should be saved with the timestamp of when each individual stream has started recording. 242 | 243 | These separate files can be used to combined with the video-conference-stitcher tool. 244 | 245 | 246 | 247 | ## TODO 248 | 249 | - [x] link user to video audio file 250 | - [x] user with audio, no video, but active speaker -> placeholder 251 | - [ ] implement custom user priority map 252 | - [ ] video cover option for layout boxes (now only fit ) 253 | 254 | 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/images/combining-example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dirvann/video-conference-stitcher/31d61457ba523009278112864c42fe0c3abbb36e/docs/images/combining-example.jpg -------------------------------------------------------------------------------- /lib/examples/simple.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const path_1 = __importDefault(require("path")); 7 | const index_1 = require("../index"); 8 | const { PresenterLayout, GridLayout, MosaicLayout } = index_1.Layouts; 9 | function basicEncode(encode = true) { 10 | // GET LIST OF MEDIA PER USER 11 | const videoFolder = path_1.default.join(__dirname, '../../videos'); 12 | const user1Media = [ 13 | new index_1.Media(path_1.default.join(videoFolder, 'vid1.mp4'), 0, true, true), 14 | new index_1.Media(path_1.default.join(videoFolder, 'vid2.mp4'), 1000, true, true), 15 | new index_1.Media(path_1.default.join(videoFolder, 'vid3.mp4'), 2000, true, true), 16 | new index_1.Media(path_1.default.join(videoFolder, 'vid1.mp4'), 2000, true, true), 17 | new index_1.Media(path_1.default.join(videoFolder, 'vid2.mp4'), 3000, true, true) 18 | ]; 19 | const user2Media = [ 20 | new index_1.Media(path_1.default.join(videoFolder, 'vid3.mp4'), 3500, true, true), 21 | new index_1.Media(path_1.default.join(videoFolder, 'vid1.mp4'), 7000, true, true), 22 | new index_1.Media(path_1.default.join(videoFolder, 'vid2.mp4'), 6000, true, true), 23 | new index_1.Media(path_1.default.join(videoFolder, 'vid3.mp4'), 10000, true, true) 24 | ]; 25 | // CREATE USERS WITH THEIR MEDIA FILES 26 | const users = [ 27 | new index_1.User('user1', user1Media, 'John'), 28 | new index_1.User('user2', user2Media, 'Kevin') 29 | ]; 30 | // CREATE SEQUENCE SETTINGS 31 | const videoLayout = new PresenterLayout(); 32 | const outputMedia = new index_1.Media(path_1.default.join(videoFolder, 'basicOutput.mp4'), -1, true, true); 33 | const encodingOptions = { 34 | crf: 20, 35 | loglevel: 'verbose', 36 | size: { 37 | w: 1280, 38 | h: 720 39 | } 40 | }; 41 | // CREATE A SEQUENCE WITH GIVEN SETTINGS 42 | const sequence = new index_1.Sequence(users, outputMedia, videoLayout, encodingOptions); 43 | // ENCODE THE SEQUENCE 44 | sequence.encode().then(comm => { 45 | console.log(comm); 46 | }); 47 | } 48 | basicEncode(); 49 | //# sourceMappingURL=simple.js.map -------------------------------------------------------------------------------- /lib/examples/simple.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"simple.js","sourceRoot":"","sources":["../../src/examples/simple.ts"],"names":[],"mappings":";;;;;AAAA,gDAAuB;AACvB,oCAAuD;AACvD,MAAM,EAAC,eAAe,EAAE,UAAU,EAAE,YAAY,EAAC,GAAG,eAAO,CAAA;AAE3D,SAAS,WAAW,CAAC,SAAe,IAAI;IACtC,6BAA6B;IAC7B,MAAM,WAAW,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,CAAA;IAExD,MAAM,UAAU,GAAW;QACzB,IAAI,aAAK,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAC,UAAU,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC;QAC3D,IAAI,aAAK,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAC,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;QAC9D,IAAI,aAAK,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAC,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;QAC9D,IAAI,aAAK,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAC,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;QAC9D,IAAI,aAAK,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAC,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;KAC/D,CAAA;IAED,MAAM,UAAU,GAAU;QACxB,IAAI,aAAK,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAC,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;QAC9D,IAAI,aAAK,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAC,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;QAC9D,IAAI,aAAK,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAC,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;QAC9D,IAAI,aAAK,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAC,UAAU,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC;KAChE,CAAA;IACD,sCAAsC;IACtC,MAAM,KAAK,GAAU;QACnB,IAAI,YAAI,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC;QACrC,IAAI,YAAI,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC;KACvC,CAAA;IAED,2BAA2B;IAC3B,MAAM,WAAW,GAAe,IAAI,eAAe,EAAE,CAAA;IACrD,MAAM,WAAW,GAAU,IAAI,aAAK,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;IAC/F,MAAM,eAAe,GAAoB;QACvC,GAAG,EAAE,EAAE;QACP,QAAQ,EAAE,SAAS;QACnB,IAAI,EAAC;YACH,CAAC,EAAE,IAAI;YACP,CAAC,EAAE,GAAG;SACP;KACF,CAAA;IAED,wCAAwC;IACxC,MAAM,QAAQ,GAAa,IAAI,gBAAQ,CAAC,KAAK,EAAC,WAAW,EAAE,WAAW,EAAE,eAAe,CAAC,CAAA;IAExF,sBAAsB;IACtB,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAC5B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IACnB,CAAC,CAAC,CAAA;AACJ,CAAC;AAGD,WAAW,EAAE,CAAA"} -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.Layouts = exports.Sequence = exports.User = exports.Media = void 0; 7 | const Media_1 = __importDefault(require("./modules/Media")); 8 | exports.Media = Media_1.default; 9 | const Sequence_1 = __importDefault(require("./modules/Sequence")); 10 | exports.Sequence = Sequence_1.default; 11 | const User_1 = __importDefault(require("./modules/User")); 12 | exports.User = User_1.default; 13 | const layouts_1 = __importDefault(require("./modules/layouts")); 14 | exports.Layouts = layouts_1.default; 15 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /lib/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,4DAAmC;AAK3B,gBALD,eAAK,CAKC;AAJb,kEAAyC;AAItB,mBAJZ,kBAAQ,CAIY;AAH3B,0DAAiC;AAGnB,eAHP,cAAI,CAGO;AAFlB,gEAAuC;AAEV,kBAFtB,iBAAO,CAEsB"} -------------------------------------------------------------------------------- /lib/modules/CommandExecutor.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const child_process_1 = require("child_process"); 4 | const stream_1 = require("stream"); 5 | exports.default = { 6 | execute(command, log = false) { 7 | return new Promise(function (resolve, reject) { 8 | if (log) 9 | console.log('\n----- COMMAND -----\n', command.replace(/;/g, ';\\\n').replace(/color/g, '\ncolor') + '\n\n---- END COMMAND -----'); 10 | const ls = child_process_1.spawn(command, [], { shell: true }); 11 | ls.stdout.on('data', data => { 12 | if (log) 13 | console.log(`stdout: ${data}`); 14 | }); 15 | ls.stderr.on('data', data => { 16 | if (log) 17 | console.log(`stderr: ${data}`); 18 | }); 19 | ls.on('error', (error) => { 20 | if (log) 21 | console.log(`error: ${error.message}`); 22 | reject(); 23 | }); 24 | ls.on('close', code => { 25 | if (log) 26 | console.log(`child process exited with code ${code}`); 27 | resolve(); 28 | }); 29 | }); 30 | }, 31 | /** 32 | * pipes the given value to the process run by the command 33 | * @param value 34 | * string to pipe 35 | * @param command 36 | * command the value will be piped to 37 | * @param log 38 | */ 39 | pipeExec(value, command, log = false) { 40 | return new Promise(function (resolve, reject) { 41 | // Pretty printing the command in the terminal 42 | if (log) 43 | console.log('\n----- COMMAND -----\n', command 44 | .replace('-filter_complex_script', '-filter_complex') 45 | .replace('pipe:0', value) 46 | .replace(/;/g, ';\\\n') 47 | .replace(/color/g, '\ncolor') 48 | .replace(/-i/g, '\\\n-i') + '\n---- END COMMAND -----\n'); 49 | const process = child_process_1.spawn(command, [], { shell: true }); 50 | const stream = new stream_1.Readable(); 51 | // tslint:disable-next-line:no-empty 52 | stream._read = () => { }; 53 | stream.push(value); 54 | stream.push(null); 55 | stream.resume(); 56 | stream.pipe(process.stdin); 57 | process.stdout.on('data', data => { 58 | if (log) 59 | console.log(`stdout: ${data}`); 60 | }); 61 | process.stderr.on('data', data => { 62 | if (log) 63 | console.log(`stderr: ${data}`); 64 | }); 65 | process.on('error', (error) => { 66 | if (log) 67 | console.log(`error: ${error.message}`); 68 | reject(); 69 | }); 70 | process.on('close', code => { 71 | if (log) 72 | console.log(`child process exited with code ${code}`); 73 | resolve(); 74 | }); 75 | }); 76 | } 77 | }; 78 | //# sourceMappingURL=CommandExecutor.js.map -------------------------------------------------------------------------------- /lib/modules/CommandExecutor.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"CommandExecutor.js","sourceRoot":"","sources":["../../src/modules/CommandExecutor.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /lib/modules/Media.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | const child_process_1 = require("child_process"); 13 | class Media { 14 | /** 15 | * 16 | * @param path 17 | * @param startTime time in milliseconds 18 | * @param hasVideo 19 | * @param hasAudio 20 | */ 21 | constructor(path, startTime, hasVideo, hasAudio) { 22 | this.user = null; 23 | this.id = -1; 24 | this.duration = -1; 25 | this.audioChannels = -1; 26 | this.initialized = false; 27 | this.path = path; 28 | if (!(hasAudio || hasVideo)) 29 | throw new Error('media must contain audio or video'); 30 | this.hasAudio = hasAudio; 31 | this.hasVideo = hasVideo; 32 | this.startTime = startTime; 33 | } 34 | init() { 35 | // TODO not looking for stream channels if doesn't contain audio. 36 | // Would it work with just audio files? 37 | return new Promise((resolve, reject) => { 38 | Promise.all([this.getEntry('format=duration'), this.hasAudio ? this.getEntry('stream=channels') : '-1']) 39 | .then(([duration, channels]) => { 40 | this.duration = Math.round(parseFloat(duration) * 1000); 41 | this.audioChannels = parseInt(channels, 10); 42 | this.initialized = true; 43 | resolve(); 44 | }) 45 | .catch((err) => { 46 | console.error('error loading video file at ', this.path, err); 47 | reject(err); 48 | }); 49 | }); 50 | } 51 | /** 52 | * @return time in milliseconds 53 | */ 54 | getEntry(entry, log = false) { 55 | return __awaiter(this, void 0, void 0, function* () { 56 | return new Promise((resolve, reject) => { 57 | const command = `ffprobe -v error -show_entries ${entry} -of default=noprint_wrappers=1:nokey=1 "${this.path}"`; 58 | const ls = child_process_1.spawn(command, [], { shell: true }); 59 | ls.stdout.on('data', data => { 60 | if (log) 61 | console.log(`stdout: ${data}`); 62 | resolve(data); 63 | }); 64 | ls.stderr.on('data', data => { 65 | if (log) 66 | console.log(`stderr: ${data}`); 67 | reject(data); 68 | }); 69 | ls.on('error', (error) => { 70 | if (log) 71 | console.log(`error: ${error.message}`); 72 | reject(error); 73 | }); 74 | ls.on('close', code => { 75 | if (log) 76 | console.log(`child process exited with code ${code}`); 77 | }); 78 | }); 79 | }); 80 | } 81 | setId(id) { 82 | this.id = id; 83 | } 84 | } 85 | exports.default = Media; 86 | //# sourceMappingURL=Media.js.map -------------------------------------------------------------------------------- /lib/modules/Media.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"Media.js","sourceRoot":"","sources":["../../src/modules/Media.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /lib/modules/Sequence.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | const SequenceStep_1 = __importDefault(require("./SequenceStep")); 16 | const CommandExecutor_1 = __importDefault(require("./CommandExecutor")); 17 | class Sequence { 18 | constructor(users = [], outputVideo, layout, encOpt) { 19 | this.sequenceSteps = []; 20 | this.mediaList = []; 21 | users.forEach(user => { 22 | this.mediaList.push(...user.media); 23 | }); 24 | const defaultEncodingOptions = { 25 | size: { w: 1280, h: 720 }, 26 | crf: 22 27 | }; 28 | if (encOpt && encOpt.crf && encOpt.bitrate) 29 | throw new Error('cannot use bitrate and crf simultaneously'); 30 | const encoding = { 31 | size: encOpt ? encOpt.size : defaultEncodingOptions.size, 32 | loglevel: encOpt === null || encOpt === void 0 ? void 0 : encOpt.loglevel 33 | }; 34 | if (!(encOpt === null || encOpt === void 0 ? void 0 : encOpt.crf) && !(encOpt === null || encOpt === void 0 ? void 0 : encOpt.bitrate)) { 35 | encoding.crf = defaultEncodingOptions.crf; 36 | } 37 | else { 38 | encoding.crf = encOpt === null || encOpt === void 0 ? void 0 : encOpt.crf; 39 | encoding.bitrate = encOpt === null || encOpt === void 0 ? void 0 : encOpt.bitrate; 40 | } 41 | this.encodingOptions = encoding; 42 | this.outputVideo = outputVideo; 43 | this.layout = layout; 44 | } 45 | addVideo(video) { 46 | this.mediaList.push(video); 47 | } 48 | encode() { 49 | console.log('start encoding'); 50 | return this.generateCommand().then(([filter, command]) => { 51 | return CommandExecutor_1.default.pipeExec(filter, command, true); 52 | }); 53 | } 54 | createSequenceSteps() { 55 | // check videos 56 | return this.mediaList 57 | .reduce((p, med) => __awaiter(this, void 0, void 0, function* () { return p.then(() => med.initialized ? Promise.resolve() : med.init()); }), Promise.resolve()) 58 | .catch(err => { 59 | console.log('error initializing video files', err); 60 | throw err; 61 | }).then(() => { 62 | // Order videos 63 | this.mediaList 64 | .sort((a, b) => a.startTime > b.startTime ? 1 : (a.startTime === b.startTime ? 0 : -1)) 65 | .forEach((vid, index) => vid.setId(index)); 66 | const queue = []; 67 | this.mediaList.forEach(vid => { 68 | queue.push({ 69 | start_point: true, 70 | time: vid.startTime, 71 | media_id: vid.id 72 | }); 73 | queue.push({ 74 | start_point: false, 75 | time: vid.startTime + vid.duration, 76 | media_id: vid.id 77 | }); 78 | }); 79 | queue.sort((a, b) => a.time < b.time ? 1 : (a.time === b.time ? 0 : -1)); 80 | console.log(`\n---- sort queue -----\n`, queue); 81 | // building sequences 82 | let prevTime = -1; 83 | const currentVideos = []; 84 | this.sequenceSteps = []; 85 | while (queue.length > 0) { 86 | // @ts-ignore 87 | const point = queue.pop(); 88 | if ((queue.length === 0 || point.time !== prevTime) && prevTime !== -1 && currentVideos.length >= 0) { 89 | const step = new SequenceStep_1.default(`Seq${this.sequenceSteps.length}`, [...currentVideos], prevTime, point.time, this.encodingOptions.size, this.layout); 90 | this.sequenceSteps.push(step); 91 | } 92 | if (point.start_point) { 93 | currentVideos.push(this.mediaList[point.media_id]); 94 | } 95 | else { 96 | const index = currentVideos.findIndex(vid => vid.id === point.media_id); 97 | currentVideos.splice(index, 1); 98 | } 99 | prevTime = point.time; 100 | } 101 | console.log('\n---- Videos ----'); 102 | this.mediaList.forEach(vid => console.log('id', vid.id, 'start', vid.startTime, 'len', vid.duration, 'achan', vid.audioChannels, vid.path)); 103 | console.log('output:', this.outputVideo.path); 104 | console.log('\n---- Sequences ----'); 105 | this.sequenceSteps.forEach(step => { 106 | console.log(step.id, 'v:', '[' + step.mediaList.map(vid => vid.id.toString()).join(',') + ']', 'start', step.startTime, 'end', step.startTime + step.duration, 'len', step.duration); 107 | }); 108 | }); 109 | } 110 | generateCommand() { 111 | return __awaiter(this, void 0, void 0, function* () { 112 | yield this.createSequenceSteps(); 113 | const command = []; 114 | const logging = this.encodingOptions.loglevel ? `-v ${this.encodingOptions.loglevel}` : `-v quiet -stats`; 115 | command.push(`ffmpeg ${logging} `); 116 | command.push(this.mediaList.map(video => `-i "${video.path}"`).join(' ') + ' '); 117 | command.push(`-filter_complex_script `); 118 | command.push('pipe:0 '); 119 | const quality = this.encodingOptions.crf ? `-crf ${this.encodingOptions.crf}` : `-b:v ${this.encodingOptions.bitrate}`; 120 | command.push(`-c:v libx264 ${quality} -preset fast -map [vid] -map [aud] -y "${this.outputVideo.path}"`); 121 | const filter = []; 122 | filter.push(`${this.sequenceSteps.map(step => step.generateFilter()).join('')}`); 123 | filter.push(`${this.sequenceSteps.map(step => `[${step.id}_out_v][${step.id}_out_a]`).join('')}concat=n=${this.sequenceSteps.length}:v=1:a=1[vid][aud]`); 124 | return Promise.all([filter.join(''), command.join('')]); 125 | }); 126 | } 127 | } 128 | exports.default = Sequence; 129 | //# sourceMappingURL=Sequence.js.map -------------------------------------------------------------------------------- /lib/modules/Sequence.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"Sequence.js","sourceRoot":"","sources":["../../src/modules/Sequence.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /lib/modules/SequenceStep.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class SequenceStep { 4 | constructor(id, mediaList, startTime, endTime, size, layout) { 5 | this.id = id; 6 | this.mediaList = mediaList; 7 | this.startTime = startTime; 8 | this.duration = endTime - startTime; 9 | this.size = size; 10 | this.layout = layout; 11 | if (mediaList.length === 0) 12 | throw new Error('At least one video must be added to the sequence'); 13 | } 14 | generateFilter() { 15 | // All generated videos. Audio without linked video and video files 16 | const videoList = this.mediaList.filter(media => media.hasVideo || 17 | (media.hasAudio && 18 | !media.hasVideo && 19 | !this.mediaList.some(other => { var _a, _b; return other.hasVideo && media.user && ((_a = other.user) === null || _a === void 0 ? void 0 : _a.id) === ((_b = media.user) === null || _b === void 0 ? void 0 : _b.id); }))); 20 | // TODO I assume videos are sorted by their id small to big 21 | const boxes = this.layout.getBoxes(videoList.length, this.size); 22 | // if(this.getDuration() < 30) return `nullsrc=s=${this.size.w}x${this.size.h}:d=${this.getDuration()/1000}[${this.id}_out_v];anullsrc,atrim=0:${this.getDuration()/1000}[${this.id}_out_a];` 23 | const out = []; 24 | out.push(`color=s=${this.size.w}x${this.size.h},trim=0:${this.duration / 1000}[${this.id}_bg];`); 25 | // --------------- TRIM/SCALE VIDEOS ----------------------- // 26 | let lastBoxIndex = 0; 27 | videoList.forEach((vid, ind) => { 28 | var _a; 29 | const box = boxes[ind]; 30 | lastBoxIndex = ind + 1; 31 | // Trim video 32 | if (vid.hasVideo) { 33 | out.push(`[${vid.id}:v]trim=${(this.startTime - vid.startTime) / 1000}:${(this.duration + this.startTime - vid.startTime) / 1000},setpts=PTS-STARTPTS,`); 34 | } 35 | else { 36 | out.push(`color=s=${this.size.w}x${this.size.h}:c=green@1.0,trim=0:${this.duration / 1000},drawtext=text='${(_a = vid.user) === null || _a === void 0 ? void 0 : _a.name}':x=(w-tw)/2:y=((h-th)/2):fontcolor=black:fontsize=55,`); 37 | } 38 | // scale fit in box 39 | out.push(`scale=w='if(gt(iw/ih,${box.w}/(${box.h})),${box.w},-2)':h='if(gt(iw/ih,${box.w}/(${box.h})),-2,${box.h})':eval=init[${this.id}_${vid.id}_v];`); 40 | }); 41 | // ---------------- OVERLAY VIDEOS ----------------------- // 42 | let prevVideoId = -1; 43 | videoList.forEach((vid, ind) => { 44 | const box = boxes[ind]; 45 | let keyOut; 46 | // set as output of sequence step if last video in list 47 | if (ind + 1 === videoList.length) { 48 | keyOut = `${this.id}_out_v`; 49 | } 50 | else { 51 | keyOut = `${this.id}_overlay_${vid.id}`; 52 | } 53 | // set input background if first video and link other videos to their previous 54 | let keyIn; 55 | if (prevVideoId === -1) { 56 | keyIn = `${this.id}_bg`; 57 | } 58 | else { 59 | keyIn = `${this.id}_overlay_${prevVideoId}`; 60 | } 61 | out.push(`[${keyIn}][${this.id}_${vid.id}_v]overlay=x='(${box.w}-w)/2+${box.x}':y='(${box.h}-h)/2+${box.y}':eval=init${prevVideoId === -1 ? ':shortest=1' : ''}[${keyOut}];`); 62 | prevVideoId = vid.id; 63 | }); 64 | // ----------- TRIM AUDIO ---------------- // 65 | const audioList = this.mediaList.filter(media => media.hasAudio); 66 | audioList.forEach(vid => { 67 | out.push(`[${vid.id}:a]atrim=${(this.startTime - vid.startTime) / 1000}:${(this.duration + this.startTime - vid.startTime) / 1000},asetpts=PTS-STARTPTS[${this.id}_${vid.id}_a];`); 68 | }); 69 | // ----------- MIX AUDIO ------------ // 70 | const inputList = audioList.map(vid => `[${this.id}_${vid.id}_a]`).join(''); 71 | let c0 = ''; 72 | let c1 = ''; 73 | let currentIndex = 0; 74 | audioList.forEach((vid, ind) => { 75 | const plus = ind === audioList.length - 1 ? '' : '+'; 76 | if (vid.audioChannels === 6) { 77 | c0 += `0.4*c${currentIndex}+0.6*c${currentIndex + 2}${plus}`; 78 | c1 += `0.4*c${currentIndex + 1}+0.6*c${currentIndex + 2}${plus}`; 79 | } 80 | else { 81 | c0 += `c${currentIndex}${plus}`; 82 | c1 += `c${currentIndex + 1}${plus}`; 83 | } 84 | currentIndex += vid.audioChannels; 85 | }); 86 | if (audioList.length > 0) { 87 | out.push(`${inputList}amerge=inputs=${audioList.length},pan='stereo|c0<${c0}|c1<${c1}'[${this.id}_out_a];`); 88 | } 89 | else { 90 | // TODO what sample rate to choose? Maybe need to convert all sample rates of files before concat 91 | out.push(`anullsrc=r=48000:cl=stereo,atrim=0:${this.duration / 1000},asetpts=PTS-STARTPTS[${this.id}_out_a];`); 92 | } 93 | return out.join(''); 94 | } 95 | } 96 | exports.default = SequenceStep; 97 | //# sourceMappingURL=SequenceStep.js.map -------------------------------------------------------------------------------- /lib/modules/SequenceStep.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"SequenceStep.js","sourceRoot":"","sources":["../../src/modules/SequenceStep.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /lib/modules/User.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class User { 4 | constructor(id, media, name) { 5 | this.id = id; 6 | this.name = name || id.toString(); 7 | media.forEach(med => { 8 | med.user = this; 9 | }); 10 | this.media = media; 11 | } 12 | } 13 | exports.default = User; 14 | //# sourceMappingURL=User.js.map -------------------------------------------------------------------------------- /lib/modules/User.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"User.js","sourceRoot":"","sources":["../../src/modules/User.ts"],"names":[],"mappings":";;AAEA,MAAqB,IAAI;IAIvB,YAAY,EAAgB,EAAC,KAAa,EAAE,IAAsB;QAChE,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;QACZ,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAA;QACjC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;YAClB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAA;QACjB,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF;AAZD,uBAYC"} -------------------------------------------------------------------------------- /lib/modules/layouts/GridLayout.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class GridLayout { 4 | getBoxes(n, size) { 5 | const side = n <= 9 ? 3 : Math.ceil(Math.sqrt(n)); 6 | const out = []; 7 | for (let y = 0; y < side; y++) { 8 | for (let x = 0; x < side; x++) { 9 | out.push({ 10 | w: size.w / side, 11 | h: size.h / side, 12 | x: x * (size.w / side), 13 | y: y * (size.h / side) 14 | }); 15 | } 16 | } 17 | return out; 18 | } 19 | } 20 | exports.default = GridLayout; 21 | //# sourceMappingURL=GridLayout.js.map -------------------------------------------------------------------------------- /lib/modules/layouts/GridLayout.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"GridLayout.js","sourceRoot":"","sources":["../../../src/modules/layouts/GridLayout.ts"],"names":[],"mappings":";;AAAA,MAAqB,UAAU;IAC7B,QAAQ,CAAC,CAAS,EAAE,IAAU;QAE5B,MAAM,IAAI,GAAU,CAAC,IAAI,CAAC,CAAA,CAAC,CAAA,CAAC,CAAA,CAAC,CAAA,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;QAEpD,MAAM,GAAG,GAAe,EAAE,CAAA;QAE1B,KAAI,IAAI,CAAC,GAAC,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE;YAC1B,KAAI,IAAI,CAAC,GAAC,CAAC,EAAE,CAAC,GAAG,IAAI,EAAC,CAAC,EAAE,EAAE;gBACzB,GAAG,CAAC,IAAI,CAAC;oBACP,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,IAAI;oBAChB,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,IAAI;oBAChB,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;oBACtB,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;iBACvB,CAAC,CAAA;aACH;SACF;QAED,OAAO,GAAG,CAAA;IACZ,CAAC;CACF;AApBD,6BAoBC"} -------------------------------------------------------------------------------- /lib/modules/layouts/MosaicLayout.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class MosaicLayout { 4 | getBoxes(n, size) { 5 | const list = [1]; 6 | let ind = 1; 7 | while (ind < n) { 8 | for (let i = 0; i < list.length; i++) { 9 | if (i < list.length - 1 && list[i] + 1 === list[i + 1]) { 10 | list[i]++; 11 | break; 12 | } 13 | if (list[i] - 1 === list.length && i === list.length - 1) { 14 | list.push(1); 15 | break; 16 | } 17 | if (i === list.length - 1) { 18 | list[i]++; 19 | break; 20 | } 21 | } 22 | ind++; 23 | } 24 | const out = []; 25 | list.forEach((wSplit, yInd) => { 26 | for (let xInd = 0; xInd < wSplit; xInd++) { 27 | out.push({ 28 | w: size.w / wSplit, 29 | h: size.h / list.length, 30 | x: xInd * (size.w / wSplit), 31 | y: yInd * (size.h / list.length) 32 | }); 33 | } 34 | }); 35 | return out; 36 | } 37 | } 38 | exports.default = MosaicLayout; 39 | //# sourceMappingURL=MosaicLayout.js.map -------------------------------------------------------------------------------- /lib/modules/layouts/MosaicLayout.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"MosaicLayout.js","sourceRoot":"","sources":["../../../src/modules/layouts/MosaicLayout.ts"],"names":[],"mappings":";;AAAA,MAAqB,YAAY;IAC/B,QAAQ,CAAC,CAAS,EAAE,IAAU;QAE5B,MAAM,IAAI,GAAa,CAAC,CAAC,CAAC,CAAA;QAC1B,IAAI,GAAG,GAAG,CAAC,CAAA;QACX,OAAO,GAAG,GAAG,CAAC,EAAE;YACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;gBAEpC,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE;oBACtD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;oBACT,MAAK;iBACN;gBAED,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE;oBACxD,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;oBACZ,MAAK;iBACN;gBAED,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE;oBACzB,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;oBACT,MAAK;iBACN;aACF;YACD,GAAG,EAAE,CAAA;SACN;QAED,MAAM,GAAG,GAAe,EAAE,CAAA;QAE1B,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE;YAC5B,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,MAAM,EAAE,IAAI,EAAE,EAAE;gBACxC,GAAG,CAAC,IAAI,CAAC;oBACP,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,MAAM;oBAClB,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM;oBACvB,CAAC,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC;oBAC3B,CAAC,EAAE,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;iBACjC,CAAC,CAAA;aACH;QACH,CAAC,CAAC,CAAA;QAEF,OAAO,GAAG,CAAA;IACZ,CAAC;CACF;AAzCD,+BAyCC"} -------------------------------------------------------------------------------- /lib/modules/layouts/PresenterLayout.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class PresenterLayout { 4 | getBoxes(n, size) { 5 | if (n === 1) { 6 | return [{ 7 | w: size.w, 8 | h: size.h, 9 | x: 0, 10 | y: 0 11 | }]; 12 | } 13 | const out = []; 14 | out.push({ 15 | w: size.w, 16 | h: size.h / 2, 17 | x: 0, 18 | y: 0 19 | }); 20 | const side = n - 1 <= 4 ? 2 : Math.ceil(Math.sqrt(n - 1)); 21 | for (let y = 0; y < side; y++) { 22 | for (let x = 0; x < side; x++) { 23 | out.push({ 24 | w: size.w / side, 25 | h: size.h / side / 2, 26 | x: x * (size.w / side), 27 | y: y * (size.h / side / 2) + size.h / 2 28 | }); 29 | } 30 | } 31 | return out; 32 | } 33 | } 34 | exports.default = PresenterLayout; 35 | //# sourceMappingURL=PresenterLayout.js.map -------------------------------------------------------------------------------- /lib/modules/layouts/PresenterLayout.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"PresenterLayout.js","sourceRoot":"","sources":["../../../src/modules/layouts/PresenterLayout.ts"],"names":[],"mappings":";;AAAA,MAAqB,eAAe;IAClC,QAAQ,CAAC,CAAS,EAAE,IAAU;QAC5B,IAAG,CAAC,KAAK,CAAC,EAAE;YACV,OAAO,CAAC;oBACN,CAAC,EAAC,IAAI,CAAC,CAAC;oBACR,CAAC,EAAC,IAAI,CAAC,CAAC;oBACR,CAAC,EAAC,CAAC;oBACH,CAAC,EAAC,CAAC;iBACJ,CAAC,CAAA;SACH;QACD,MAAM,GAAG,GAAe,EAAE,CAAA;QAE1B,GAAG,CAAC,IAAI,CAAC;YACP,CAAC,EAAC,IAAI,CAAC,CAAC;YACR,CAAC,EAAC,IAAI,CAAC,CAAC,GAAC,CAAC;YACV,CAAC,EAAC,CAAC;YACH,CAAC,EAAC,CAAC;SACJ,CAAC,CAAA;QAEF,MAAM,IAAI,GAAU,CAAC,GAAC,CAAC,IAAG,CAAC,CAAA,CAAC,CAAA,CAAC,CAAA,CAAC,CAAA,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAC,CAAC,CAAC,CAAC,CAAA;QAIvD,KAAI,IAAI,CAAC,GAAC,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE;YAC1B,KAAI,IAAI,CAAC,GAAC,CAAC,EAAE,CAAC,GAAG,IAAI,EAAC,CAAC,EAAE,EAAE;gBACzB,GAAG,CAAC,IAAI,CAAC;oBACP,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,IAAI;oBAChB,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,IAAI,GAAC,CAAC;oBAClB,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;oBACtB,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,GAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAC,CAAC;iBACpC,CAAC,CAAA;aACH;SACF;QAED,OAAO,GAAG,CAAA;IAEZ,CAAC;CAEF;AAtCD,kCAsCC"} -------------------------------------------------------------------------------- /lib/modules/layouts/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const GridLayout_1 = __importDefault(require("./GridLayout")); 7 | const MosaicLayout_1 = __importDefault(require("./MosaicLayout")); 8 | const PresenterLayout_1 = __importDefault(require("./PresenterLayout")); 9 | exports.default = { GridLayout: GridLayout_1.default, MosaicLayout: MosaicLayout_1.default, PresenterLayout: PresenterLayout_1.default }; 10 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /lib/modules/layouts/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/modules/layouts/index.ts"],"names":[],"mappings":";;;;;AAAA,8DAAqC;AACrC,kEAAyC;AACzC,wEAA+C;AAG/C,kBAAe,EAAC,UAAU,EAAV,oBAAU,EAAE,YAAY,EAAZ,sBAAY,EAAE,eAAe,EAAf,yBAAe,EAAC,CAAA"} -------------------------------------------------------------------------------- /lib/types/Types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | //# sourceMappingURL=Types.js.map -------------------------------------------------------------------------------- /lib/types/Types.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"Types.js","sourceRoot":"","sources":["../../src/types/Types.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-conference-stitcher", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.10.4", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", 10 | "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.10.4" 14 | } 15 | }, 16 | "@babel/helper-validator-identifier": { 17 | "version": "7.10.4", 18 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", 19 | "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", 20 | "dev": true 21 | }, 22 | "@babel/highlight": { 23 | "version": "7.10.4", 24 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", 25 | "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", 26 | "dev": true, 27 | "requires": { 28 | "@babel/helper-validator-identifier": "^7.10.4", 29 | "chalk": "^2.0.0", 30 | "js-tokens": "^4.0.0" 31 | } 32 | }, 33 | "@types/body-parser": { 34 | "version": "1.19.0", 35 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", 36 | "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", 37 | "dev": true, 38 | "requires": { 39 | "@types/connect": "*", 40 | "@types/node": "*" 41 | } 42 | }, 43 | "@types/connect": { 44 | "version": "3.4.33", 45 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", 46 | "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", 47 | "dev": true, 48 | "requires": { 49 | "@types/node": "*" 50 | } 51 | }, 52 | "@types/express": { 53 | "version": "4.17.7", 54 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz", 55 | "integrity": "sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ==", 56 | "dev": true, 57 | "requires": { 58 | "@types/body-parser": "*", 59 | "@types/express-serve-static-core": "*", 60 | "@types/qs": "*", 61 | "@types/serve-static": "*" 62 | } 63 | }, 64 | "@types/express-serve-static-core": { 65 | "version": "4.17.9", 66 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.9.tgz", 67 | "integrity": "sha512-DG0BYg6yO+ePW+XoDENYz8zhNGC3jDDEpComMYn7WJc4mY1Us8Rw9ax2YhJXxpyk2SF47PQAoQ0YyVT1a0bEkA==", 68 | "dev": true, 69 | "requires": { 70 | "@types/node": "*", 71 | "@types/qs": "*", 72 | "@types/range-parser": "*" 73 | } 74 | }, 75 | "@types/mime": { 76 | "version": "2.0.3", 77 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", 78 | "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", 79 | "dev": true 80 | }, 81 | "@types/node": { 82 | "version": "14.0.27", 83 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.27.tgz", 84 | "integrity": "sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==", 85 | "dev": true 86 | }, 87 | "@types/qs": { 88 | "version": "6.9.4", 89 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.4.tgz", 90 | "integrity": "sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ==", 91 | "dev": true 92 | }, 93 | "@types/range-parser": { 94 | "version": "1.2.3", 95 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", 96 | "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", 97 | "dev": true 98 | }, 99 | "@types/serve-static": { 100 | "version": "1.13.5", 101 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz", 102 | "integrity": "sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ==", 103 | "dev": true, 104 | "requires": { 105 | "@types/express-serve-static-core": "*", 106 | "@types/mime": "*" 107 | } 108 | }, 109 | "ansi-styles": { 110 | "version": "3.2.1", 111 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 112 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 113 | "dev": true, 114 | "requires": { 115 | "color-convert": "^1.9.0" 116 | } 117 | }, 118 | "argparse": { 119 | "version": "1.0.10", 120 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 121 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 122 | "dev": true, 123 | "requires": { 124 | "sprintf-js": "~1.0.2" 125 | } 126 | }, 127 | "balanced-match": { 128 | "version": "1.0.0", 129 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 130 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 131 | "dev": true 132 | }, 133 | "brace-expansion": { 134 | "version": "1.1.11", 135 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 136 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 137 | "dev": true, 138 | "requires": { 139 | "balanced-match": "^1.0.0", 140 | "concat-map": "0.0.1" 141 | } 142 | }, 143 | "builtin-modules": { 144 | "version": "1.1.1", 145 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", 146 | "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", 147 | "dev": true 148 | }, 149 | "chalk": { 150 | "version": "2.4.2", 151 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 152 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 153 | "dev": true, 154 | "requires": { 155 | "ansi-styles": "^3.2.1", 156 | "escape-string-regexp": "^1.0.5", 157 | "supports-color": "^5.3.0" 158 | } 159 | }, 160 | "color-convert": { 161 | "version": "1.9.3", 162 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 163 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 164 | "dev": true, 165 | "requires": { 166 | "color-name": "1.1.3" 167 | } 168 | }, 169 | "color-name": { 170 | "version": "1.1.3", 171 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 172 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 173 | "dev": true 174 | }, 175 | "commander": { 176 | "version": "2.20.3", 177 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 178 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 179 | "dev": true 180 | }, 181 | "concat-map": { 182 | "version": "0.0.1", 183 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 184 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 185 | "dev": true 186 | }, 187 | "diff": { 188 | "version": "4.0.2", 189 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 190 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 191 | "dev": true 192 | }, 193 | "doctrine": { 194 | "version": "0.7.2", 195 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz", 196 | "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=", 197 | "dev": true, 198 | "requires": { 199 | "esutils": "^1.1.6", 200 | "isarray": "0.0.1" 201 | }, 202 | "dependencies": { 203 | "isarray": { 204 | "version": "0.0.1", 205 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 206 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", 207 | "dev": true 208 | } 209 | } 210 | }, 211 | "escape-string-regexp": { 212 | "version": "1.0.5", 213 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 214 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 215 | "dev": true 216 | }, 217 | "esprima": { 218 | "version": "4.0.1", 219 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 220 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 221 | "dev": true 222 | }, 223 | "esutils": { 224 | "version": "1.1.6", 225 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz", 226 | "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=", 227 | "dev": true 228 | }, 229 | "fs.realpath": { 230 | "version": "1.0.0", 231 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 232 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 233 | "dev": true 234 | }, 235 | "glob": { 236 | "version": "7.1.6", 237 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 238 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 239 | "dev": true, 240 | "requires": { 241 | "fs.realpath": "^1.0.0", 242 | "inflight": "^1.0.4", 243 | "inherits": "2", 244 | "minimatch": "^3.0.4", 245 | "once": "^1.3.0", 246 | "path-is-absolute": "^1.0.0" 247 | } 248 | }, 249 | "has-flag": { 250 | "version": "3.0.0", 251 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 252 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 253 | "dev": true 254 | }, 255 | "inflight": { 256 | "version": "1.0.6", 257 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 258 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 259 | "dev": true, 260 | "requires": { 261 | "once": "^1.3.0", 262 | "wrappy": "1" 263 | } 264 | }, 265 | "inherits": { 266 | "version": "2.0.3", 267 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 268 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 269 | "dev": true 270 | }, 271 | "js-tokens": { 272 | "version": "4.0.0", 273 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 274 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 275 | "dev": true 276 | }, 277 | "js-yaml": { 278 | "version": "3.14.0", 279 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", 280 | "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", 281 | "dev": true, 282 | "requires": { 283 | "argparse": "^1.0.7", 284 | "esprima": "^4.0.0" 285 | } 286 | }, 287 | "minimatch": { 288 | "version": "3.0.4", 289 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 290 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 291 | "dev": true, 292 | "requires": { 293 | "brace-expansion": "^1.1.7" 294 | } 295 | }, 296 | "minimist": { 297 | "version": "1.2.5", 298 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 299 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", 300 | "dev": true 301 | }, 302 | "mkdirp": { 303 | "version": "0.5.5", 304 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 305 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 306 | "dev": true, 307 | "requires": { 308 | "minimist": "^1.2.5" 309 | } 310 | }, 311 | "once": { 312 | "version": "1.4.0", 313 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 314 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 315 | "dev": true, 316 | "requires": { 317 | "wrappy": "1" 318 | } 319 | }, 320 | "path-is-absolute": { 321 | "version": "1.0.1", 322 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 323 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 324 | "dev": true 325 | }, 326 | "path-parse": { 327 | "version": "1.0.6", 328 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 329 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 330 | "dev": true 331 | }, 332 | "resolve": { 333 | "version": "1.17.0", 334 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", 335 | "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", 336 | "dev": true, 337 | "requires": { 338 | "path-parse": "^1.0.6" 339 | } 340 | }, 341 | "semver": { 342 | "version": "5.7.1", 343 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 344 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 345 | "dev": true 346 | }, 347 | "sprintf-js": { 348 | "version": "1.0.3", 349 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 350 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 351 | "dev": true 352 | }, 353 | "supports-color": { 354 | "version": "5.5.0", 355 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 356 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 357 | "dev": true, 358 | "requires": { 359 | "has-flag": "^3.0.0" 360 | } 361 | }, 362 | "tslib": { 363 | "version": "1.13.0", 364 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", 365 | "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", 366 | "dev": true 367 | }, 368 | "tslint": { 369 | "version": "6.1.3", 370 | "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", 371 | "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", 372 | "dev": true, 373 | "requires": { 374 | "@babel/code-frame": "^7.0.0", 375 | "builtin-modules": "^1.1.1", 376 | "chalk": "^2.3.0", 377 | "commander": "^2.12.1", 378 | "diff": "^4.0.1", 379 | "glob": "^7.1.1", 380 | "js-yaml": "^3.13.1", 381 | "minimatch": "^3.0.4", 382 | "mkdirp": "^0.5.3", 383 | "resolve": "^1.3.2", 384 | "semver": "^5.3.0", 385 | "tslib": "^1.13.0", 386 | "tsutils": "^2.29.0" 387 | } 388 | }, 389 | "tslint-config-prettier": { 390 | "version": "1.18.0", 391 | "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz", 392 | "integrity": "sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg==", 393 | "dev": true 394 | }, 395 | "tslint-eslint-rules": { 396 | "version": "5.4.0", 397 | "resolved": "https://registry.npmjs.org/tslint-eslint-rules/-/tslint-eslint-rules-5.4.0.tgz", 398 | "integrity": "sha512-WlSXE+J2vY/VPgIcqQuijMQiel+UtmXS+4nvK4ZzlDiqBfXse8FAvkNnTcYhnQyOTW5KFM+uRRGXxYhFpuBc6w==", 399 | "dev": true, 400 | "requires": { 401 | "doctrine": "0.7.2", 402 | "tslib": "1.9.0", 403 | "tsutils": "^3.0.0" 404 | }, 405 | "dependencies": { 406 | "tslib": { 407 | "version": "1.9.0", 408 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", 409 | "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", 410 | "dev": true 411 | }, 412 | "tsutils": { 413 | "version": "3.17.1", 414 | "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", 415 | "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", 416 | "dev": true, 417 | "requires": { 418 | "tslib": "^1.8.1" 419 | } 420 | } 421 | } 422 | }, 423 | "tsutils": { 424 | "version": "2.29.0", 425 | "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", 426 | "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", 427 | "dev": true, 428 | "requires": { 429 | "tslib": "^1.8.1" 430 | } 431 | }, 432 | "typescript": { 433 | "version": "3.9.7", 434 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", 435 | "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==" 436 | }, 437 | "wrappy": { 438 | "version": "1.0.2", 439 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 440 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 441 | "dev": true 442 | } 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-conference-stitcher", 3 | "version": "0.1.3", 4 | "description": "Combine separately recorded recordings from a video conference into a single video file.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "example": "node lib/examples/simple.js", 9 | "tsc": "tsc", 10 | "prebuild": "tslint -c tslint.json -p tsconfig.json --fix" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/Dirvann/video-conference-stitcher.git" 15 | }, 16 | "keywords": [ 17 | "rtp", 18 | "webrtc", 19 | "recording", 20 | "nodejs", 21 | "ffmpeg" 22 | ], 23 | "author": "Dirk Vanbeveren", 24 | "contributors": [ 25 | { 26 | "name": "Dirk Vanbeveren", 27 | "email": "dirvann.dev@gmail.com", 28 | "url": "" 29 | } 30 | ], 31 | "license": "MIT", 32 | "dependencies": { 33 | "typescript": "^3.9.7" 34 | }, 35 | "devDependencies": { 36 | "@types/express": "^4.17.7", 37 | "@types/node": "^14.0.27", 38 | "tslint": "^6.1.3", 39 | "tslint-config-prettier": "^1.18.0", 40 | "tslint-eslint-rules": "^5.4.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/examples/simple.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import {User, Layouts, Sequence, Media} from '../index' 3 | const {PresenterLayout, GridLayout, MosaicLayout} = Layouts 4 | 5 | function basicEncode(encode:boolean=true) { 6 | // GET LIST OF MEDIA PER USER 7 | const videoFolder = path.join(__dirname, '../../videos') 8 | 9 | const user1Media:Media[] = [ 10 | new Media(path.join(videoFolder,'vid1.mp4'), 0, true, true), 11 | new Media(path.join(videoFolder,'vid2.mp4'), 1000, true, true), 12 | new Media(path.join(videoFolder,'vid3.mp4'), 2000, true, true), 13 | new Media(path.join(videoFolder,'vid1.mp4'), 2000, true, true), 14 | new Media(path.join(videoFolder,'vid2.mp4'), 3000, true, true) 15 | ] 16 | 17 | const user2Media:Media[]= [ 18 | new Media(path.join(videoFolder,'vid3.mp4'), 3500, true, true), 19 | new Media(path.join(videoFolder,'vid1.mp4'), 7000, true, true), 20 | new Media(path.join(videoFolder,'vid2.mp4'), 6000, true, true), 21 | new Media(path.join(videoFolder,'vid3.mp4'), 10000, true, true) 22 | ] 23 | // CREATE USERS WITH THEIR MEDIA FILES 24 | const users:User[] = [ 25 | new User('user1', user1Media, 'John'), 26 | new User('user2', user2Media, 'Kevin') 27 | ] 28 | 29 | // CREATE SEQUENCE SETTINGS 30 | const videoLayout:VideoLayout = new PresenterLayout() 31 | const outputMedia: Media = new Media(path.join(videoFolder, 'basicOutput.mp4'), -1, true, true) 32 | const encodingOptions: EncodingOptions = { 33 | crf: 20, 34 | loglevel: 'verbose', 35 | size:{ 36 | w: 1280, 37 | h: 720 38 | } 39 | } 40 | 41 | // CREATE A SEQUENCE WITH GIVEN SETTINGS 42 | const sequence: Sequence = new Sequence(users,outputMedia, videoLayout, encodingOptions) 43 | 44 | // ENCODE THE SEQUENCE 45 | sequence.encode().then(comm => { 46 | console.log(comm) 47 | }) 48 | } 49 | 50 | 51 | basicEncode() 52 | 53 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Media from './modules/Media' 2 | import Sequence from './modules/Sequence' 3 | import User from './modules/User' 4 | import Layouts from './modules/layouts' 5 | 6 | export {Media,User,Sequence, Layouts} 7 | -------------------------------------------------------------------------------- /src/modules/CommandExecutor.ts: -------------------------------------------------------------------------------- 1 | import {spawn} from 'child_process' 2 | import {Readable} from 'stream' 3 | 4 | export default { 5 | execute(command:string, log:boolean=false):Promise { 6 | return new Promise(function(resolve, reject){ 7 | 8 | if(log)console.log( 9 | '\n----- COMMAND -----\n', 10 | command.replace(/;/g,';\\\n').replace(/color/g,'\ncolor') + '\n\n---- END COMMAND -----') 11 | const ls = spawn(command, [], {shell: true}) 12 | 13 | ls.stdout.on('data', data => { 14 | if(log)console.log(`stdout: ${data}`) 15 | }) 16 | 17 | ls.stderr.on('data', data => { 18 | if(log)console.log(`stderr: ${data}`) 19 | }) 20 | 21 | ls.on('error', (error) => { 22 | if(log)console.log(`error: ${error.message}`) 23 | reject() 24 | }) 25 | 26 | ls.on('close', code => { 27 | if(log)console.log(`child process exited with code ${code}`) 28 | resolve() 29 | }) 30 | }) 31 | }, 32 | /** 33 | * pipes the given value to the process run by the command 34 | * @param value 35 | * string to pipe 36 | * @param command 37 | * command the value will be piped to 38 | * @param log 39 | */ 40 | pipeExec(value:string, command:string, log:boolean=false) { 41 | return new Promise(function(resolve, reject){ 42 | 43 | // Pretty printing the command in the terminal 44 | if(log)console.log( 45 | '\n----- COMMAND -----\n', 46 | command 47 | .replace('-filter_complex_script', '-filter_complex') 48 | .replace('pipe:0', value) 49 | .replace(/;/g,';\\\n') 50 | .replace(/color/g,'\ncolor') 51 | .replace(/-i/g,'\\\n-i')+ '\n---- END COMMAND -----\n') 52 | 53 | 54 | const process = spawn(command, [], {shell: true}) 55 | 56 | const stream = new Readable() 57 | // tslint:disable-next-line:no-empty 58 | stream._read = () => {} 59 | stream.push(value) 60 | stream.push(null) 61 | stream.resume() 62 | stream.pipe(process.stdin) 63 | 64 | process.stdout.on('data', data => { 65 | if(log)console.log(`stdout: ${data}`) 66 | }) 67 | 68 | process.stderr.on('data', data => { 69 | if(log)console.log(`stderr: ${data}`) 70 | }) 71 | 72 | process.on('error', (error) => { 73 | if(log)console.log(`error: ${error.message}`) 74 | reject() 75 | }) 76 | 77 | process.on('close', code => { 78 | if(log)console.log(`child process exited with code ${code}`) 79 | resolve() 80 | }) 81 | }) 82 | } 83 | } -------------------------------------------------------------------------------- /src/modules/Media.ts: -------------------------------------------------------------------------------- 1 | import {spawn} from 'child_process' 2 | import User from './User' 3 | 4 | export default class Media { 5 | public readonly path: string 6 | public readonly hasAudio: boolean 7 | public readonly hasVideo: boolean 8 | public readonly startTime: number 9 | public user: User|null = null 10 | public id: number = -1 11 | public duration:number = -1 12 | public audioChannels: number = -1 13 | public initialized:boolean = false 14 | 15 | /** 16 | * 17 | * @param path 18 | * @param startTime time in milliseconds 19 | * @param hasVideo 20 | * @param hasAudio 21 | */ 22 | constructor(path: string, startTime:number, hasVideo:boolean, hasAudio:boolean) { 23 | this.path = path 24 | if(!(hasAudio || hasVideo)) throw new Error('media must contain audio or video') 25 | this.hasAudio = hasAudio 26 | this.hasVideo = hasVideo 27 | this.startTime = startTime 28 | } 29 | 30 | init():PromiseLike { 31 | 32 | // TODO not looking for stream channels if doesn't contain audio. 33 | // Would it work with just audio files? 34 | return new Promise((resolve, reject) => { 35 | Promise.all([this.getEntry('format=duration'), this.hasAudio?this.getEntry('stream=channels'):'-1']) 36 | .then(([duration ,channels]) => { 37 | this.duration = Math.round(parseFloat(duration)*1000) 38 | this.audioChannels = parseInt(channels, 10) 39 | this.initialized = true 40 | resolve() 41 | }) 42 | .catch((err: any) => { 43 | console.error('error loading video file at ',this.path, err) 44 | reject(err) 45 | }) 46 | }) 47 | } 48 | 49 | /** 50 | * @return time in milliseconds 51 | */ 52 | async getEntry(entry:string, log:boolean=false):Promise { 53 | return new Promise((resolve, reject) => { 54 | const command = `ffprobe -v error -show_entries ${entry} -of default=noprint_wrappers=1:nokey=1 "${this.path}"` 55 | const ls = spawn(command, [], {shell: true}) 56 | ls.stdout.on('data', data => { 57 | if(log)console.log(`stdout: ${data}`) 58 | resolve(data) 59 | }) 60 | 61 | ls.stderr.on('data', data => { 62 | if(log)console.log(`stderr: ${data}`) 63 | reject(data) 64 | }) 65 | 66 | ls.on('error', (error) => { 67 | if(log)console.log(`error: ${error.message}`) 68 | reject(error) 69 | }) 70 | 71 | ls.on('close', code => { 72 | if(log)console.log(`child process exited with code ${code}`) 73 | }) 74 | }) 75 | } 76 | 77 | setId(id:number):void{ 78 | this.id = id 79 | } 80 | } -------------------------------------------------------------------------------- /src/modules/Sequence.ts: -------------------------------------------------------------------------------- 1 | import Media from './Media' 2 | import SequenceStep from './SequenceStep' 3 | import CommandExecutor from './CommandExecutor' 4 | import User from './User' 5 | 6 | 7 | export default class Sequence { 8 | public mediaList: Media[] 9 | public sequenceSteps:SequenceStep[] = [] 10 | public outputVideo: Media 11 | public layout: VideoLayout 12 | public encodingOptions: EncodingOptions 13 | constructor(users:User[]=[], outputVideo:Media, layout:VideoLayout, encOpt?: EncodingOptions) { 14 | this.mediaList = [] 15 | users.forEach(user => { 16 | this.mediaList.push(...user.media) 17 | }) 18 | 19 | const defaultEncodingOptions:EncodingOptions = { 20 | size:{w:1280,h:720}, 21 | crf:22 22 | } 23 | if(encOpt && encOpt.crf && encOpt.bitrate) throw new Error('cannot use bitrate and crf simultaneously') 24 | const encoding:EncodingOptions = { 25 | size: encOpt?encOpt.size:defaultEncodingOptions.size, 26 | loglevel: encOpt?.loglevel 27 | } 28 | if(!encOpt?.crf && !encOpt?.bitrate) { 29 | encoding.crf = defaultEncodingOptions.crf 30 | } else { 31 | encoding.crf = encOpt?.crf 32 | encoding.bitrate = encOpt?.bitrate 33 | } 34 | 35 | this.encodingOptions = encoding 36 | 37 | this.outputVideo = outputVideo 38 | this.layout = layout 39 | } 40 | 41 | addVideo(video:Media):void { 42 | this.mediaList.push(video) 43 | } 44 | 45 | encode():Promise { 46 | console.log('start encoding') 47 | return this.generateCommand().then(([filter,command]) => { 48 | return CommandExecutor.pipeExec(filter,command,true) 49 | }) 50 | } 51 | 52 | private createSequenceSteps():Promise { 53 | 54 | // check videos 55 | return this.mediaList 56 | .reduce(async (p: Promise, med: Media) => p.then(() => med.initialized?Promise.resolve():med.init()), Promise.resolve()) 57 | .catch(err => { 58 | console.log('error initializing video files', err) 59 | throw err 60 | }).then(() => { 61 | // Order videos 62 | this.mediaList 63 | .sort((a,b) => a.startTime > b.startTime?1:(a.startTime===b.startTime?0:-1)) 64 | .forEach((vid, index) => vid.setId(index)) 65 | 66 | interface MediaPoint { 67 | start_point: boolean 68 | time: number 69 | media_id: number 70 | } 71 | 72 | const queue:MediaPoint[] = [] 73 | this.mediaList.forEach(vid => { 74 | queue.push({ 75 | start_point: true, 76 | time: vid.startTime, 77 | media_id: vid.id 78 | }) 79 | queue.push({ 80 | start_point: false, 81 | time: vid.startTime + vid.duration, 82 | media_id: vid.id 83 | }) 84 | }) 85 | 86 | queue.sort((a:MediaPoint,b:MediaPoint) => a.time < b.time?1:(a.time===b.time?0:-1)) 87 | 88 | console.log(`\n---- sort queue -----\n`, queue) 89 | 90 | // building sequences 91 | 92 | let prevTime:number = -1 93 | const currentVideos:Media[] = [] 94 | this.sequenceSteps = [] 95 | while(queue.length > 0) { 96 | // @ts-ignore 97 | const point:MediaPoint = queue.pop() 98 | if((queue.length === 0 || point.time !== prevTime) && prevTime !== -1 && currentVideos.length >= 0) { 99 | const step:SequenceStep = new SequenceStep(`Seq${this.sequenceSteps.length}`,[...currentVideos],prevTime, point.time,this.encodingOptions.size, this.layout) 100 | this.sequenceSteps.push(step) 101 | } 102 | if(point.start_point) { 103 | currentVideos.push(this.mediaList[point.media_id]) 104 | } else { 105 | const index:number = currentVideos.findIndex(vid=> vid.id===point.media_id) 106 | currentVideos.splice(index,1) 107 | } 108 | prevTime = point.time 109 | } 110 | console.log('\n---- Videos ----') 111 | this.mediaList.forEach(vid => console.log('id', vid.id, 'start', vid.startTime, 'len', vid.duration, 'achan', vid.audioChannels, vid.path)) 112 | console.log('output:',this.outputVideo.path) 113 | console.log('\n---- Sequences ----') 114 | this.sequenceSteps.forEach(step => { 115 | console.log(step.id, 'v:', '[' + step.mediaList.map(vid => vid.id.toString()).join(',') + ']', 'start', step.startTime,'end', step.startTime + step.duration, 'len',step.duration) 116 | }) 117 | }) 118 | } 119 | 120 | async generateCommand():Promise { 121 | await this.createSequenceSteps() 122 | 123 | const command:string[] = [] 124 | 125 | const logging:string = this.encodingOptions.loglevel?`-v ${this.encodingOptions.loglevel}`:`-v quiet -stats` 126 | 127 | command.push(`ffmpeg ${logging} `) 128 | command.push(this.mediaList.map(video => `-i "${video.path}"`).join(' ') + ' ') 129 | command.push(`-filter_complex_script `) 130 | command.push('pipe:0 ') 131 | const quality:string = this.encodingOptions.crf?`-crf ${this.encodingOptions.crf}`:`-b:v ${this.encodingOptions.bitrate}` 132 | command.push(`-c:v libx264 ${quality} -preset fast -map [vid] -map [aud] -y "${this.outputVideo.path}"`) 133 | 134 | const filter:string[] = [] 135 | filter.push(`${this.sequenceSteps.map(step => step.generateFilter()).join('')}`) 136 | filter.push(`${this.sequenceSteps.map(step => `[${step.id}_out_v][${step.id}_out_a]`).join('')}concat=n=${this.sequenceSteps.length}:v=1:a=1[vid][aud]`) 137 | 138 | return Promise.all([filter.join(''),command.join('')]) 139 | } 140 | } -------------------------------------------------------------------------------- /src/modules/SequenceStep.ts: -------------------------------------------------------------------------------- 1 | import Media from './Media' 2 | 3 | export default class SequenceStep { 4 | public readonly id: string 5 | public readonly mediaList: Media[] 6 | public readonly startTime: number 7 | public readonly duration: number 8 | public readonly size: Size 9 | private readonly layout: VideoLayout 10 | 11 | constructor(id: string, mediaList: Media[], startTime: number, endTime: number, size:Size, layout:VideoLayout) { 12 | this.id = id 13 | this.mediaList = mediaList 14 | this.startTime = startTime 15 | this.duration = endTime - startTime 16 | this.size = size 17 | this.layout = layout 18 | if(mediaList.length === 0) throw new Error('At least one video must be added to the sequence') 19 | } 20 | 21 | generateFilter():string { 22 | // All generated videos. Audio without linked video and video files 23 | const videoList = this.mediaList.filter(media => media.hasVideo || 24 | (media.hasAudio && 25 | !media.hasVideo && 26 | !this.mediaList.some(other => other.hasVideo && media.user && other.user?.id === media.user?.id))) 27 | 28 | // TODO I assume videos are sorted by their id small to big 29 | const boxes:VideoBox[] = this.layout.getBoxes(videoList.length, this.size) 30 | // if(this.getDuration() < 30) return `nullsrc=s=${this.size.w}x${this.size.h}:d=${this.getDuration()/1000}[${this.id}_out_v];anullsrc,atrim=0:${this.getDuration()/1000}[${this.id}_out_a];` 31 | 32 | const out:string[] = [] 33 | out.push(`color=s=${this.size.w}x${this.size.h},trim=0:${this.duration / 1000 }[${this.id}_bg];`) 34 | 35 | 36 | // --------------- TRIM/SCALE VIDEOS ----------------------- // 37 | let lastBoxIndex = 0 38 | 39 | videoList.forEach((vid, ind) => { 40 | const box = boxes[ind] 41 | lastBoxIndex = ind+1 42 | // Trim video 43 | if(vid.hasVideo) { 44 | out.push(`[${vid.id}:v]trim=${(this.startTime - vid.startTime) / 1000}:${(this.duration + this.startTime - vid.startTime) / 1000 },setpts=PTS-STARTPTS,`) 45 | } else { 46 | out.push(`color=s=${this.size.w}x${this.size.h}:c=green@1.0,trim=0:${this.duration / 1000 },drawtext=text='${vid.user?.name}':x=(w-tw)/2:y=((h-th)/2):fontcolor=black:fontsize=55,`) 47 | } 48 | // scale fit in box 49 | out.push(`scale=w='if(gt(iw/ih,${box.w}/(${box.h})),${box.w},-2)':h='if(gt(iw/ih,${box.w}/(${box.h})),-2,${box.h})':eval=init[${this.id}_${vid.id}_v];`) 50 | }) 51 | 52 | // ---------------- OVERLAY VIDEOS ----------------------- // 53 | let prevVideoId: number = -1 54 | videoList.forEach((vid, ind) => { 55 | const box = boxes[ind] 56 | let keyOut:string 57 | // set as output of sequence step if last video in list 58 | if(ind+1 === videoList.length) { 59 | keyOut = `${this.id}_out_v` 60 | } else { 61 | keyOut = `${this.id}_overlay_${vid.id}` 62 | } 63 | // set input background if first video and link other videos to their previous 64 | let keyIn:string 65 | if(prevVideoId === -1) { 66 | keyIn = `${this.id}_bg` 67 | } else { 68 | keyIn = `${this.id}_overlay_${prevVideoId}` 69 | } 70 | out.push(`[${keyIn}][${this.id}_${vid.id}_v]overlay=x='(${box.w}-w)/2+${box.x}':y='(${box.h}-h)/2+${box.y}':eval=init${prevVideoId === -1?':shortest=1':''}[${keyOut}];`) 71 | 72 | prevVideoId = vid.id 73 | }) 74 | 75 | // ----------- TRIM AUDIO ---------------- // 76 | const audioList = this.mediaList.filter(media => media.hasAudio) 77 | audioList.forEach(vid => { 78 | out.push(`[${vid.id}:a]atrim=${(this.startTime - vid.startTime) / 1000}:${(this.duration + this.startTime - vid.startTime) / 1000 },asetpts=PTS-STARTPTS[${this.id}_${vid.id}_a];`) 79 | }) 80 | 81 | // ----------- MIX AUDIO ------------ // 82 | 83 | const inputList = audioList.map(vid => `[${this.id}_${vid.id}_a]`).join('') 84 | 85 | 86 | let c0:string = '' 87 | let c1:string = '' 88 | let currentIndex:number = 0 89 | audioList.forEach((vid, ind) => { 90 | const plus:string = ind===audioList.length -1?'':'+' 91 | if(vid.audioChannels === 6) { 92 | c0 += `0.4*c${currentIndex}+0.6*c${currentIndex+2}${plus}` 93 | c1 += `0.4*c${currentIndex+1}+0.6*c${currentIndex+2}${plus}` 94 | } else { 95 | c0 += `c${currentIndex}${plus}` 96 | c1 += `c${currentIndex+1}${plus}` 97 | } 98 | currentIndex += vid.audioChannels 99 | }) 100 | if(audioList.length > 0) { 101 | out.push(`${inputList}amerge=inputs=${audioList.length},pan='stereo|c0<${c0}|c1<${c1}'[${this.id}_out_a];`) 102 | } else { 103 | // TODO what sample rate to choose? Maybe need to convert all sample rates of files before concat 104 | out.push(`anullsrc=r=48000:cl=stereo,atrim=0:${this.duration / 1000 },asetpts=PTS-STARTPTS[${this.id}_out_a];`) 105 | } 106 | 107 | return out.join('') 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/modules/User.ts: -------------------------------------------------------------------------------- 1 | import Media from './Media' 2 | 3 | export default class User { 4 | public id: string | number 5 | public media: Media[] 6 | public readonly name: string 7 | constructor(id:string|number,media:Media[], name?:string|undefined) { 8 | this.id = id 9 | this.name = name || id.toString() 10 | media.forEach(med => { 11 | med.user = this 12 | }) 13 | this.media = media 14 | } 15 | } -------------------------------------------------------------------------------- /src/modules/layouts/GridLayout.ts: -------------------------------------------------------------------------------- 1 | export default class GridLayout implements VideoLayout { 2 | getBoxes(n: number, size: Size): VideoBox[] { 3 | 4 | const side:number = n <= 9?3:Math.ceil(Math.sqrt(n)) 5 | 6 | const out: VideoBox[] = [] 7 | 8 | for(let y=0; y < side; y++) { 9 | for(let x=0; x < side;x++) { 10 | out.push({ 11 | w: size.w / side, 12 | h: size.h / side, 13 | x: x * (size.w / side), 14 | y: y * (size.h / side) 15 | }) 16 | } 17 | } 18 | 19 | return out 20 | } 21 | } -------------------------------------------------------------------------------- /src/modules/layouts/MosaicLayout.ts: -------------------------------------------------------------------------------- 1 | export default class MosaicLayout implements VideoLayout { 2 | getBoxes(n: number, size: Size): VideoBox[] { 3 | 4 | const list: number[] = [1] 5 | let ind = 1 6 | while (ind < n) { 7 | for (let i = 0; i < list.length; i++) { 8 | 9 | if (i < list.length - 1 && list[i] + 1 === list[i + 1]) { 10 | list[i]++ 11 | break 12 | } 13 | 14 | if (list[i] - 1 === list.length && i === list.length - 1) { 15 | list.push(1) 16 | break 17 | } 18 | 19 | if (i === list.length - 1) { 20 | list[i]++ 21 | break 22 | } 23 | } 24 | ind++ 25 | } 26 | 27 | const out: VideoBox[] = [] 28 | 29 | list.forEach((wSplit, yInd) => { 30 | for (let xInd = 0; xInd < wSplit; xInd++) { 31 | out.push({ 32 | w: size.w / wSplit, 33 | h: size.h / list.length, 34 | x: xInd * (size.w / wSplit), 35 | y: yInd * (size.h / list.length) 36 | }) 37 | } 38 | }) 39 | 40 | return out 41 | } 42 | } -------------------------------------------------------------------------------- /src/modules/layouts/PresenterLayout.ts: -------------------------------------------------------------------------------- 1 | export default class PresenterLayout implements VideoLayout{ 2 | getBoxes(n: number, size: Size): VideoBox[] { 3 | if(n === 1) { 4 | return [{ 5 | w:size.w, 6 | h:size.h, 7 | x:0, 8 | y:0 9 | }] 10 | } 11 | const out: VideoBox[] = [] 12 | 13 | out.push({ 14 | w:size.w, 15 | h:size.h/2, 16 | x:0, 17 | y:0 18 | }) 19 | 20 | const side:number = n-1<= 4?2:Math.ceil(Math.sqrt(n-1)) 21 | 22 | 23 | 24 | for(let y=0; y < side; y++) { 25 | for(let x=0; x < side;x++) { 26 | out.push({ 27 | w: size.w / side, 28 | h: size.h / side/2, 29 | x: x * (size.w / side), 30 | y: y * (size.h / side/2) + size.h/2 31 | }) 32 | } 33 | } 34 | 35 | return out 36 | 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/modules/layouts/index.ts: -------------------------------------------------------------------------------- 1 | import GridLayout from './GridLayout' 2 | import MosaicLayout from './MosaicLayout' 3 | import PresenterLayout from './PresenterLayout' 4 | 5 | 6 | export default {GridLayout, MosaicLayout, PresenterLayout} -------------------------------------------------------------------------------- /src/types/Types.ts: -------------------------------------------------------------------------------- 1 | declare interface Size { 2 | w:number 3 | h:number 4 | } 5 | 6 | declare interface EncodingOptions { 7 | crf?:number 8 | bitrate?:string 9 | size:Size, 10 | loglevel?:number| 'quiet' | 'panic' | 'fatal' | 'error' | 'warning' | 'info' | 'verbose' | 'debug' | 'trace' 11 | } 12 | 13 | declare interface VideoBox { 14 | w:number 15 | h:number 16 | x:number 17 | y:number 18 | } 19 | 20 | declare interface VideoLayout { 21 | getBoxes(n:number,size:Size) :VideoBox[] 22 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "lib", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | "baseUrl": ".", /* Base directory to resolve non-absolute module names. */ 46 | "paths": { 47 | "*": [ 48 | "node_modules/*" 49 | ] 50 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 51 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 52 | "typeRoots": [ 53 | "src/types", 54 | "node_modules/@types" 55 | ], /* List of folders to include type definitions from. */ 56 | // "types": [], /* Type declaration files to be included in compilation. */ 57 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 58 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 59 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 60 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 61 | 62 | /* Source Map Options */ 63 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 64 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 65 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 66 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 67 | 68 | /* Experimental Options */ 69 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 70 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 71 | 72 | /* Advanced Options */ 73 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 74 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 75 | }, 76 | "include": [ 77 | "src/**/*" 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended", 5 | "tslint-config-prettier", 6 | "tslint-eslint-rules" 7 | ], 8 | "jsRules": { 9 | "no-console": false, 10 | "semicolon": [true, "never"], 11 | "quotemark": [true, "single"], 12 | "indent": [true,"spaces", 2] 13 | }, 14 | "rules": { 15 | "trailing-comma": [ false ], 16 | "no-console": false, 17 | "quotemark": [true,"single"], 18 | "semicolon": [true, "never"], 19 | "only-arrow-functions": [false, "allow-declarations", "allow-named-functions"], 20 | "indent": [true,"spaces", 2], 21 | "ter-indent": [true, 2] 22 | }, 23 | "rulesDirectory": [] 24 | } -------------------------------------------------------------------------------- /videos/vid1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dirvann/video-conference-stitcher/31d61457ba523009278112864c42fe0c3abbb36e/videos/vid1.mp4 -------------------------------------------------------------------------------- /videos/vid2.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dirvann/video-conference-stitcher/31d61457ba523009278112864c42fe0c3abbb36e/videos/vid2.mp4 -------------------------------------------------------------------------------- /videos/vid3.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dirvann/video-conference-stitcher/31d61457ba523009278112864c42fe0c3abbb36e/videos/vid3.mp4 --------------------------------------------------------------------------------