├── .npmignore ├── .gitignore ├── README.md ├── lerna.json ├── packages ├── mediakitchen-server │ ├── src │ │ ├── index.ts │ │ ├── ConnectionInfo.ts │ │ ├── WorkerOptions.ts │ │ ├── createWorker.ts │ │ └── server.ts │ ├── tsconfig.json │ ├── tsconfig.build.json │ └── package.json ├── mediakitchen │ ├── tsconfig.json │ ├── src │ │ ├── ConnectionInfo.ts │ │ ├── index.ts │ │ ├── Worker.ts │ │ ├── Consumer.ts │ │ ├── Producer.ts │ │ ├── Cluster.ts │ │ ├── Router.ts │ │ ├── PlainTransport.ts │ │ ├── WebRtcTransport.ts │ │ ├── PipeTransport.ts │ │ ├── model │ │ │ ├── KitchenTransportPipe.ts │ │ │ ├── KitchenTransportPlain.ts │ │ │ ├── KitchenTransportWebRTC.ts │ │ │ ├── KitchenConsumer.ts │ │ │ ├── KitchenProducer.ts │ │ │ ├── KitchenTransport.ts │ │ │ ├── KitchenCluster.ts │ │ │ ├── KitchenRouter.ts │ │ │ ├── KitchenWorker.ts │ │ │ └── KitchenApi.ts │ │ ├── WorkerApi.ts │ │ └── Stats.ts │ ├── tsconfig.build.json │ └── package.json ├── mediakitchen-common │ ├── src │ │ ├── utils │ │ │ ├── delay.ts │ │ │ ├── randomKey.ts │ │ │ ├── time.ts │ │ │ ├── backoff.ts │ │ │ └── AsyncLock.ts │ │ ├── wire │ │ │ ├── events.ts │ │ │ ├── states.ts │ │ │ ├── common.ts │ │ │ └── commands.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.build.json │ └── package.json ├── docker │ ├── package.json │ ├── Dockerfile │ └── k8s-daemon.yaml └── tests │ ├── cluster.spec.ts │ ├── model.spec.ts │ └── api.spec.ts ├── babel.config.js ├── jest.config.js ├── tsconfig.build.json ├── tsconfig.json ├── package.json └── yarn-error.log /.npmignore: -------------------------------------------------------------------------------- 1 | coverage -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mediakitchen is a drop in Mediasoup cluster 2 | 3 | LICENSE 4 | MIT 5 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "independent" 6 | } -------------------------------------------------------------------------------- /packages/mediakitchen-server/src/index.ts: -------------------------------------------------------------------------------- 1 | export { createWorker } from './createWorker'; 2 | export { ServerWorker } from './ServerWorker'; -------------------------------------------------------------------------------- /packages/mediakitchen/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/mediakitchen-common/src/utils/delay.ts: -------------------------------------------------------------------------------- 1 | export function delay(delay: number) { 2 | return new Promise((r) => setTimeout(r, delay)); 3 | } -------------------------------------------------------------------------------- /packages/mediakitchen-common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/mediakitchen-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "src/**/*" 5 | ] 6 | } -------------------------------------------------------------------------------- /packages/docker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediakitchen-docker", 3 | "dependencies": { 4 | "mediakitchen-server": "2.1.1" 5 | } 6 | } -------------------------------------------------------------------------------- /packages/mediakitchen-common/src/utils/randomKey.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | export function randomKey() { 4 | return crypto.randomBytes(16).toString('hex'); 5 | } -------------------------------------------------------------------------------- /packages/mediakitchen-server/src/ConnectionInfo.ts: -------------------------------------------------------------------------------- 1 | import * as nats from 'ts-nats'; 2 | 3 | export interface ConnectionInfo { 4 | rootTopic?: string; 5 | nc: nats.Client; 6 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/ConnectionInfo.ts: -------------------------------------------------------------------------------- 1 | import * as nats from 'ts-nats'; 2 | 3 | export interface ConnectionInfo { 4 | rootTopic?: string; 5 | nc: nats.Client; 6 | healthCheckTimeout?: number; 7 | } -------------------------------------------------------------------------------- /packages/mediakitchen/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": [ 7 | "src/**/*" 8 | ] 9 | } -------------------------------------------------------------------------------- /packages/mediakitchen-common/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": [ 7 | "src/**/*" 8 | ] 9 | } -------------------------------------------------------------------------------- /packages/mediakitchen-server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": [ 7 | "src/**/*" 8 | ] 9 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | plugins: [ 7 | '@babel/plugin-proposal-class-properties', 8 | ] 9 | }; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // transform: { "\\.ts$": ['ts-jest'] }, 3 | modulePathIgnorePatterns: [ 4 | "packages/.*/dist" 5 | ], 6 | "moduleNameMapper": { 7 | "mediakitchen-server": "/packages/mediakitchen-server/src" 8 | }, 9 | }; -------------------------------------------------------------------------------- /packages/mediakitchen-common/src/utils/time.ts: -------------------------------------------------------------------------------- 1 | let prevRes = 0; 2 | 3 | export function now() { 4 | let time = process.hrtime(); 5 | let res = time[0] * 1000 + Math.floor(time[1] / 1000000); 6 | if (res < prevRes) { 7 | res = prevRes + 1; 8 | } 9 | prevRes = res; 10 | return res; 11 | } -------------------------------------------------------------------------------- /packages/mediakitchen-server/src/WorkerOptions.ts: -------------------------------------------------------------------------------- 1 | import * as mediasoup from 'mediasoup'; 2 | import { ConnectionInfo } from './ConnectionInfo'; 3 | 4 | export interface WorkerOptions { 5 | connectionInfo: ConnectionInfo; 6 | listenIp?: mediasoup.types.TransportListenIp; 7 | settings?: Partial; 8 | } -------------------------------------------------------------------------------- /packages/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 AS stage-one 2 | 3 | # Install DEB dependencies and others. 4 | RUN \ 5 | set -x \ 6 | && apt-get update \ 7 | && apt-get install -y net-tools build-essential valgrind 8 | 9 | WORKDIR /server 10 | COPY package.json . 11 | RUN yarn install 12 | CMD ["node", "/server/node_modules/mediakitchen-server/dist/server.js"] -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "target": "es6", 5 | "module": "CommonJS", 6 | "strict": true, 7 | "lib": [ 8 | "es2016", 9 | "ES2018.AsyncIterable", 10 | "dom" 11 | ], 12 | "declaration": true, 13 | "moduleResolution": "node", 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "resolveJsonModule": true 17 | } 18 | } -------------------------------------------------------------------------------- /packages/mediakitchen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediakitchen", 3 | "version": "2.1.1", 4 | "description": "Clustered Mediasoup server", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/openland/mediakitchen.git", 7 | "author": "Steve Korshakov ", 8 | "license": "MIT", 9 | "scripts": { 10 | "clean": "rm -rf ./dist", 11 | "compile": "tsc -p tsconfig.build.json", 12 | "build": "yarn clean && yarn build" 13 | }, 14 | "dependencies": { 15 | "mediakitchen-common": "2.1.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/mediakitchen-common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediakitchen-common", 3 | "version": "2.1.0", 4 | "description": "Shared code for Mediakitchen", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/openland/mediakitchen.git", 7 | "author": "Steve Korshakov ", 8 | "license": "MIT", 9 | "scripts": { 10 | "clean": "rm -rf ./dist", 11 | "build": "tsc -p tsconfig.build.json" 12 | }, 13 | "dependencies": { 14 | "fp-ts": "^2.5.3", 15 | "io-ts": "^2.1.3", 16 | "ts-nats": "^1.2.12" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "mediakitchen": [ 7 | "packages/mediakitchen/src" 8 | ], 9 | "mediakitchen-server": [ 10 | "packages/mediakitchen-server/src" 11 | ], 12 | "mediakitchen-common": [ 13 | "packages/mediakitchen-common/src" 14 | ], 15 | "tests": [ 16 | "packages/tests" 17 | ] 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /packages/mediakitchen-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediakitchen-server", 3 | "version": "2.1.1", 4 | "description": "Clustered Mediasoup server", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/openland/mediakitchen.git", 7 | "author": "Steve Korshakov ", 8 | "license": "MIT", 9 | "scripts": { 10 | "clean": "rm -rf ./dist", 11 | "build": "tsc -p tsconfig.build.json" 12 | }, 13 | "dependencies": { 14 | "change-case": "^4.1.1", 15 | "debug": "^4.1.1", 16 | "mediakitchen-common": "2.1.0", 17 | "mediasoup": "3.7.1", 18 | "public-ip": "^4.0.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/docker/k8s-daemon.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | name: mediakitchen 5 | labels: 6 | app: mediakitchen 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: mediakitchen 11 | template: 12 | metadata: 13 | labels: 14 | app: mediakitchen 15 | spec: 16 | hostNetwork: true 17 | containers: 18 | - name: mediakitchen 19 | image: openland/mediakitchen:v4 20 | resources: 21 | limits: 22 | memory: 200Mi 23 | requests: 24 | cpu: 100m 25 | memory: 200Mi 26 | env: 27 | - name: MEDIAKITCHEN_DC 28 | value: dc1 29 | - name: MEDIAKITCHEN_DETECT_IP 30 | value: "true" 31 | -------------------------------------------------------------------------------- /packages/mediakitchen/src/index.ts: -------------------------------------------------------------------------------- 1 | export { ConnectionInfo } from './ConnectionInfo'; 2 | export { 3 | DtlsParameters, 4 | DtlsState, 5 | DtlsRole, 6 | IceCandidate, 7 | IceParameters, 8 | IceState, 9 | RtpCodecCapability, 10 | RtpParameters, 11 | RtpCodecParameters, 12 | RtpHeaderExtensionParameters, 13 | RtpHeaderExtensionCodec, 14 | RtpCapabilities, 15 | RtpEncoding, 16 | RtcpParameters, 17 | RtcpFeedback, 18 | SimpleMap 19 | } from 'mediakitchen-common'; 20 | 21 | export { connectToCluster, Cluster } from './Cluster'; 22 | export { Worker } from './Worker'; 23 | export { Router } from './Router'; 24 | export { Producer } from './Producer'; 25 | export { Consumer } from './Consumer'; 26 | export { WebRtcTransport } from './WebRtcTransport'; 27 | export { PlainTransport } from './PlainTransport'; 28 | export { ConsumerStats, ProducerStats, WebRtcTransportStats, PipeTransportStats } from './Stats'; -------------------------------------------------------------------------------- /packages/mediakitchen/src/Worker.ts: -------------------------------------------------------------------------------- 1 | import { RouterCreateCommand } from 'mediakitchen-common'; 2 | import { KitchenWorker } from './model/KitchenWorker'; 3 | 4 | export class Worker { 5 | #worker: KitchenWorker 6 | 7 | constructor(worker: KitchenWorker) { 8 | this.#worker = worker; 9 | Object.freeze(this); 10 | } 11 | 12 | get id() { 13 | return this.#worker.id; 14 | } 15 | 16 | get appData() { 17 | return this.#worker.appData; 18 | } 19 | 20 | get status() { 21 | return this.#worker.status; 22 | } 23 | 24 | get api() { 25 | return this.#worker.api; 26 | } 27 | 28 | async createRouter(args: RouterCreateCommand['args'], retryKey: string) { 29 | return (await this.#worker.createRouter(args, retryKey)).facade; 30 | } 31 | 32 | kill() { 33 | return this.#worker.kill(); 34 | } 35 | 36 | toString() { 37 | return `Worker{id:${this.id},status:${this.status},appData:${JSON.stringify(this.appData)}}`; 38 | } 39 | } -------------------------------------------------------------------------------- /packages/mediakitchen-server/src/createWorker.ts: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import * as mediasoup from 'mediasoup'; 3 | import { randomKey } from 'mediakitchen-common'; 4 | import { WorkerOptions } from './WorkerOptions'; 5 | import { ServerWorker } from './ServerWorker'; 6 | 7 | export async function createWorker(options: WorkerOptions) { 8 | 9 | // Globaly unique id of a worker 10 | let id = randomKey(); 11 | 12 | let loggerInfo = debug('mediakitchen:' + id); 13 | loggerInfo.log = console.info.bind(console); 14 | let loggerError = debug('mediakitchen:' + id + ':ERROR'); 15 | loggerError.log = console.error.bind(console); 16 | 17 | loggerInfo('Starting'); 18 | 19 | let settings: mediasoup.types.WorkerSettings = { 20 | logLevel: 'error', 21 | rtcMinPort: 10000, 22 | rtcMaxPort: 59999, 23 | appData: {}, 24 | ...options.settings 25 | }; 26 | 27 | let rawWorker = await mediasoup.createWorker(settings); 28 | 29 | loggerInfo('Raw Worker started'); 30 | 31 | return new ServerWorker(id, rawWorker, options, loggerInfo, loggerError); 32 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/Consumer.ts: -------------------------------------------------------------------------------- 1 | import { KitchenConsumer } from './model/KitchenConsumer'; 2 | export class Consumer { 3 | #consumer: KitchenConsumer 4 | 5 | constructor(consumer: KitchenConsumer) { 6 | this.#consumer = consumer; 7 | Object.freeze(this); 8 | } 9 | 10 | get id() { 11 | return this.#consumer.id; 12 | } 13 | 14 | get appData() { 15 | return this.#consumer.appData; 16 | } 17 | 18 | get kind() { 19 | return this.#consumer.kind; 20 | } 21 | 22 | get type() { 23 | return this.#consumer.type; 24 | } 25 | 26 | get paused() { 27 | return this.#consumer.paused; 28 | } 29 | 30 | get closed() { 31 | return this.#consumer.closed; 32 | } 33 | 34 | get rtpParameters() { 35 | return this.#consumer.rtpParameters; 36 | } 37 | 38 | async pause() { 39 | await this.#consumer.pause(); 40 | } 41 | 42 | async resume() { 43 | await this.#consumer.resume(); 44 | } 45 | 46 | async getStats() { 47 | return this.#consumer.getStats(); 48 | } 49 | 50 | async close() { 51 | await this.#consumer.close(); 52 | } 53 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/Producer.ts: -------------------------------------------------------------------------------- 1 | import { KitchenProducer } from './model/KitchenProducer'; 2 | export class Producer { 3 | #producer: KitchenProducer 4 | 5 | constructor(producer: KitchenProducer) { 6 | this.#producer = producer; 7 | Object.freeze(this); 8 | } 9 | 10 | get id() { 11 | return this.#producer.id; 12 | } 13 | 14 | get appData() { 15 | return this.#producer.appData; 16 | } 17 | 18 | get kind() { 19 | return this.#producer.kind; 20 | } 21 | 22 | get type() { 23 | return this.#producer.type; 24 | } 25 | 26 | get closed() { 27 | return this.#producer.closed; 28 | } 29 | 30 | get paused() { 31 | return this.#producer.paused; 32 | } 33 | 34 | get rtpParameters() { 35 | return this.#producer.rtpParameters; 36 | } 37 | 38 | async pause() { 39 | return this.#producer.pause(); 40 | } 41 | 42 | async resume() { 43 | return this.#producer.resume(); 44 | } 45 | 46 | async getStats() { 47 | return this.#producer.getStats(); 48 | } 49 | 50 | async close() { 51 | await this.#producer.close(); 52 | } 53 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/Cluster.ts: -------------------------------------------------------------------------------- 1 | import { delay } from 'mediakitchen-common'; 2 | import { ConnectionInfo } from './ConnectionInfo'; 3 | import { Worker } from './Worker'; 4 | import { KitchenCluster } from "./model/KitchenCluster"; 5 | 6 | export class Cluster { 7 | #cluster: KitchenCluster; 8 | 9 | onWorkerStatusChanged?: (worker: Worker) => void; 10 | 11 | constructor(cluster: KitchenCluster) { 12 | this.#cluster = cluster; 13 | this.#cluster.onWorkerStatusChanged = (worker) => { 14 | if (this.onWorkerStatusChanged) { 15 | this.onWorkerStatusChanged(worker.facade); 16 | } 17 | } 18 | } 19 | 20 | get closed() { 21 | return !this.#cluster.alive; 22 | } 23 | 24 | get workers() { 25 | return this.#cluster.getWorkers().map((v) => v.facade); 26 | } 27 | 28 | close() { 29 | this.#cluster.close(); 30 | } 31 | } 32 | 33 | export async function connectToCluster(connectionInfo: ConnectionInfo) { 34 | 35 | // Create cluter connection 36 | let res = new KitchenCluster(connectionInfo); 37 | 38 | // Connect to cluster 39 | await res.connect(); 40 | 41 | // Wait for cluster map population 42 | await delay(5000); 43 | 44 | // Wrap cluter 45 | return new Cluster(res); 46 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/Router.ts: -------------------------------------------------------------------------------- 1 | import { PlainTransportCreateCommand, WebRTCTransportCreateCommand, PipeTransportCreateCommand } from 'mediakitchen-common'; 2 | import { KitchenRouter } from './model/KitchenRouter'; 3 | export class Router { 4 | #router: KitchenRouter 5 | 6 | constructor(router: KitchenRouter) { 7 | this.#router = router; 8 | Object.freeze(this); 9 | } 10 | 11 | get id() { 12 | return this.#router.id; 13 | } 14 | 15 | get appData() { 16 | return this.#router.appData; 17 | } 18 | 19 | get closed() { 20 | return this.#router.closed; 21 | } 22 | 23 | async createWebRtcTransport(args: WebRTCTransportCreateCommand['args'], retryKey: string) { 24 | return (await this.#router.createWebRTCTransport(args, retryKey)).facade; 25 | } 26 | 27 | async createPlainTransport(args: PlainTransportCreateCommand['args'], retryKey: string) { 28 | return (await this.#router.createPlainTransport(args, retryKey)).facade; 29 | } 30 | 31 | async createPipeTransport(args: PipeTransportCreateCommand['args'], retryKey: string) { 32 | return (await this.#router.createPipeTransport(args, retryKey)).facade; 33 | } 34 | 35 | async close() { 36 | await this.#router.close(); 37 | } 38 | 39 | toString() { 40 | return `Router{id:${this.id},closed:${this.closed},appData:${JSON.stringify(this.appData)}}`; 41 | } 42 | } -------------------------------------------------------------------------------- /packages/mediakitchen-common/src/utils/backoff.ts: -------------------------------------------------------------------------------- 1 | import { delay } from './delay'; 2 | 3 | export function exponentialBackoffDelay(currentFailureCount: number, minDelay: number, maxDelay: number, maxFailureCount: number) { 4 | let maxDelayRet = minDelay + ((maxDelay - minDelay) / maxFailureCount) * Math.max(currentFailureCount, maxFailureCount); 5 | return Math.round(Math.random() * maxDelayRet); 6 | } 7 | 8 | export async function backoff(callback: () => Promise, 9 | opts?: { 10 | onError?: (e: any, failuresCount: number) => void, 11 | minDelay?: number, 12 | maxDelay?: number, 13 | maxFailureCount?: number 14 | }): Promise { 15 | let currentFailureCount = 0; 16 | const minDelay = opts && opts.minDelay !== undefined ? opts.minDelay : 250; 17 | const maxDelay = opts && opts.maxDelay !== undefined ? opts.maxDelay : 1000; 18 | const maxFailureCount = opts && opts.maxFailureCount !== undefined ? opts.maxFailureCount : 50; 19 | while (true) { 20 | try { 21 | return await callback(); 22 | } catch (e) { 23 | if (currentFailureCount < maxFailureCount) { 24 | currentFailureCount++; 25 | } 26 | if (opts && opts.onError) { 27 | opts.onError(e, currentFailureCount); 28 | } 29 | let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount); 30 | await delay(waitForRequest); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediakitchen-root", 3 | "version": "1.0.0", 4 | "description": "Clustered Mediasoup server (Monorepo)", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/openland/mediakitchen.git", 7 | "author": "Steve Korshakov ", 8 | "license": "MIT", 9 | "private": true, 10 | "scripts": { 11 | "clean": "lerna run clean", 12 | "build": "lerna run clean && tsc -p ./packages/mediakitchen-common/tsconfig.build.json && tsc -p ./packages/mediakitchen/tsconfig.build.json && tsc -p ./packages/mediakitchen-server/tsconfig.build.json", 13 | "release:client": "yarn build && cd packages/mediakitchen && yarn publish --no-git-tag-version", 14 | "release:common": "yarn build && cd packages/mediakitchen-common && yarn publish --no-git-tag-version", 15 | "release:server": "yarn build && cd packages/mediakitchen-server && yarn publish --no-git-tag-version", 16 | "bootstrap": "lerna bootstrap --hoist", 17 | "start": "yarn build && node ./mediakitchen-server/dist/server.js", 18 | "postinstall": "yarn bootstrap" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.14.3", 22 | "@babel/plugin-proposal-class-properties": "^7.13.0", 23 | "@babel/preset-env": "^7.14.2", 24 | "@babel/preset-typescript": "^7.13.0", 25 | "@types/debug": "^4.1.5", 26 | "@types/jest": "25.2.1", 27 | "@types/node": "13.9.8", 28 | "babel-jest": "^26.6.3", 29 | "jest": "26.6.3", 30 | "lerna": "3.20.2", 31 | "mediasoup": "3.7.1", 32 | "typescript": "3.8.3" 33 | }, 34 | "peerDependencies": { 35 | "mediasoup": "*" 36 | }, 37 | "dependencies": { 38 | "change-case": "^4.1.1", 39 | "debug": "^4.1.1", 40 | "fp-ts": "^2.5.3", 41 | "io-ts": "^2.1.3", 42 | "public-ip": "^4.0.1", 43 | "ts-nats": "^1.2.12" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/tests/cluster.spec.ts: -------------------------------------------------------------------------------- 1 | import { connect, Payload } from "ts-nats"; 2 | import { delay } from 'mediakitchen-common'; 3 | import { createWorker } from 'mediakitchen-server'; 4 | import { ConnectionInfo, connectToCluster } from 'mediakitchen'; 5 | 6 | describe('Cluster', () => { 7 | 8 | it('should detect workers', async () => { 9 | jest.setTimeout(10000); 10 | const nc = await connect({ payload: Payload.JSON }); 11 | const connectionInfo: ConnectionInfo = { nc, rootTopic: 'cluster1' }; 12 | const worker = await createWorker({ connectionInfo }); 13 | const cluster = await connectToCluster(connectionInfo); 14 | expect(cluster.closed).toBe(false); 15 | expect(cluster.workers.length).toBe(1); 16 | expect(cluster.workers[0].status).toBe('healthy'); 17 | expect(cluster.workers[0].id).toBe(worker.id); 18 | cluster.close(); 19 | worker.close(); 20 | nc.close(); 21 | }); 22 | 23 | it('should sync workers between cluster connections', async () => { 24 | jest.setTimeout(15000); 25 | const nc = await connect({ payload: Payload.JSON }); 26 | const connectionInfo: ConnectionInfo = { nc, rootTopic: 'cluster2' }; 27 | const worker = await createWorker({ connectionInfo }); 28 | const cluster1 = await connectToCluster(connectionInfo); 29 | const cluster2 = await connectToCluster(connectionInfo); 30 | expect(cluster1.closed).toBe(false); 31 | expect(cluster2.closed).toBe(false); 32 | expect(cluster1.workers[0].id).toBe(cluster2.workers[0].id); 33 | cluster1.workers[0].kill(); 34 | expect(cluster1.workers[0].status).toBe('dead'); 35 | await delay(100); 36 | expect(cluster2.workers[0].status).toBe('dead'); 37 | cluster1.close(); 38 | cluster2.close(); 39 | worker.close(); 40 | nc.close(); 41 | }); 42 | }); -------------------------------------------------------------------------------- /packages/mediakitchen-common/src/utils/AsyncLock.ts: -------------------------------------------------------------------------------- 1 | export class AsyncLock { 2 | private permits: number = 1; 3 | private promiseResolverQueue: Array<(v: boolean) => void> = []; 4 | 5 | async inLock(func: () => Promise | T): Promise { 6 | try { 7 | await this.lock(); 8 | return await func(); 9 | } finally { 10 | this.unlock(); 11 | } 12 | } 13 | 14 | private async lock() { 15 | if (this.permits > 0) { 16 | this.permits = this.permits - 1; 17 | return; 18 | } 19 | await new Promise(resolve => this.promiseResolverQueue.push(resolve)); 20 | } 21 | 22 | private unlock() { 23 | this.permits += 1; 24 | if (this.permits > 1 && this.promiseResolverQueue.length > 0) { 25 | throw new Error('this.permits should never be > 0 when there is someone waiting.'); 26 | } else if (this.permits === 1 && this.promiseResolverQueue.length > 0) { 27 | // If there is someone else waiting, immediately consume the permit that was released 28 | // at the beginning of this function and let the waiting function resume. 29 | this.permits -= 1; 30 | 31 | const nextResolver = this.promiseResolverQueue.shift(); 32 | // Resolve on the next tick 33 | if (nextResolver) { 34 | setTimeout(() => { 35 | nextResolver(true); 36 | }, 0); 37 | } 38 | } 39 | } 40 | } 41 | 42 | export class AsyncLockMap { 43 | private lockMap = new Map(); 44 | 45 | async inLock(key: string, func: () => Promise | T): Promise { 46 | let l = this.lockMap.get(key); 47 | if (!l) { 48 | l = new AsyncLock(); 49 | this.lockMap.set(key, l); 50 | } 51 | return l.inLock(func); 52 | } 53 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/PlainTransport.ts: -------------------------------------------------------------------------------- 1 | import { ProduceCommand, ConsumeCommand, SrtpParameters } from 'mediakitchen-common'; 2 | import { KitchenTransportPlain } from './model/KitchenTransportPlain'; 3 | 4 | export class PlainTransport { 5 | #transport: KitchenTransportPlain 6 | 7 | constructor(transport: KitchenTransportPlain) { 8 | this.#transport = transport; 9 | Object.freeze(this); 10 | } 11 | 12 | get id() { 13 | return this.#transport.id; 14 | } 15 | 16 | get appData() { 17 | return this.#transport.appData; 18 | } 19 | 20 | get closed() { 21 | return this.#transport.closed; 22 | } 23 | 24 | get tuple() { 25 | return this.#transport.tuple; 26 | } 27 | 28 | get rtcpTuple() { 29 | return this.#transport.rtcpTuple; 30 | } 31 | 32 | get sctpState() { 33 | return this.#transport.sctpState; 34 | } 35 | 36 | get sctpParameters() { 37 | return this.#transport.sctpParameters; 38 | } 39 | 40 | get srtpParameters() { 41 | return this.#transport.srtpParameters; 42 | } 43 | 44 | async connect(args: { ip?: string, port?: number, rtcpPort?: number, srtpParameters?: SrtpParameters }) { 45 | await this.#transport.connect(args); 46 | } 47 | 48 | async produce(args: ProduceCommand['args'], retryKey: string) { 49 | return (await this.#transport.produce(args, retryKey)).facade; 50 | } 51 | 52 | async consume(producerId: string, args: ConsumeCommand['args'], retryKey: string) { 53 | return (await this.#transport.consume(producerId, args, retryKey)).facade; 54 | } 55 | 56 | async getStats() { 57 | return this.#transport.getStats(); 58 | } 59 | 60 | async close() { 61 | await this.#transport.close(); 62 | } 63 | 64 | toString() { 65 | return `PlainTransport{id:${this.id},closed:${this.closed},appData:${JSON.stringify(this.appData)}}`; 66 | } 67 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/WebRtcTransport.ts: -------------------------------------------------------------------------------- 1 | import { ProduceCommand, ConsumeCommand } from 'mediakitchen-common'; 2 | import { DtlsParameters } from 'mediakitchen-common'; 3 | import { KitchenTransportWebRTC } from './model/KitchenTransportWebRTC'; 4 | 5 | export class WebRtcTransport { 6 | #transport: KitchenTransportWebRTC 7 | 8 | constructor(transport: KitchenTransportWebRTC) { 9 | this.#transport = transport; 10 | Object.freeze(this); 11 | } 12 | 13 | get id() { 14 | return this.#transport.id; 15 | } 16 | 17 | get closed() { 18 | return this.#transport.closed; 19 | } 20 | 21 | get dtlsParameters() { 22 | return this.#transport.dtlsParameters; 23 | } 24 | 25 | get dtlsState() { 26 | return this.#transport.dtlsState; 27 | } 28 | 29 | get iceParameters() { 30 | return this.#transport.iceParameters; 31 | } 32 | 33 | get iceCandidates() { 34 | return this.#transport.iceCandidates; 35 | } 36 | 37 | get iceState() { 38 | return this.#transport.iceState; 39 | } 40 | 41 | get appData() { 42 | return this.#transport.appData; 43 | } 44 | 45 | async connect(args: { dtlsParameters: DtlsParameters }) { 46 | await this.#transport.connect(args); 47 | } 48 | 49 | async restartIce() { 50 | await this.#transport.restartIce(); 51 | } 52 | 53 | async produce(args: ProduceCommand['args'], retryKey: string) { 54 | return (await this.#transport.produce(args, retryKey)).facade; 55 | } 56 | 57 | async consume(producerId: string, args: ConsumeCommand['args'], retryKey: string) { 58 | return (await this.#transport.consume(producerId, args, retryKey)).facade; 59 | } 60 | 61 | async getStats() { 62 | return this.#transport.getStats(); 63 | } 64 | 65 | async close() { 66 | await this.#transport.close(); 67 | } 68 | 69 | toString() { 70 | return `WebRTCTransport{id:${this.id},closed:${this.closed},appData:${JSON.stringify(this.appData)}}`; 71 | } 72 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/PipeTransport.ts: -------------------------------------------------------------------------------- 1 | import { ProduceCommand, ConsumeCommand, SrtpParameters } from 'mediakitchen-common'; 2 | import { KitchenTransportPipe } from './model/KitchenTransportPipe'; 3 | 4 | export class PipeTransport { 5 | #transport: KitchenTransportPipe 6 | 7 | constructor(transport: KitchenTransportPipe) { 8 | this.#transport = transport; 9 | Object.freeze(this); 10 | } 11 | 12 | get id() { 13 | return this.#transport.id; 14 | } 15 | 16 | get appData() { 17 | return this.#transport.appData; 18 | } 19 | 20 | get closed() { 21 | return this.#transport.closed; 22 | } 23 | 24 | get tuple() { 25 | return this.#transport.tuple; 26 | } 27 | 28 | get sctpState() { 29 | return this.#transport.sctpState; 30 | } 31 | 32 | get sctpParameters() { 33 | return this.#transport.sctpParameters; 34 | } 35 | 36 | get srtpParameters() { 37 | return this.#transport.srtpParameters; 38 | } 39 | 40 | async connect(args: { ip: string, port: number, srtpParameters?: SrtpParameters }) { 41 | await this.#transport.connect(args); 42 | } 43 | 44 | async produce(args: ProduceCommand['args'], retryKey: string) { 45 | return (await this.#transport.produce(args, retryKey)).facade; 46 | } 47 | 48 | async consume(producerId: string, args: ConsumeCommand['args'], retryKey: string) { 49 | return (await this.#transport.consume(producerId, args, retryKey)).facade; 50 | } 51 | 52 | async close() { 53 | await this.#transport.close(); 54 | } 55 | 56 | async getStats() { 57 | return this.#transport.getStats(); 58 | } 59 | 60 | toString() { 61 | return `PipeTransport{` + 62 | `id:${this.id},` + 63 | `tuple:${JSON.stringify(this.tuple)},` + 64 | `sctpState:${this.sctpState},` + 65 | `sctpParameters:${JSON.stringify(this.sctpParameters)},` + 66 | `srtpParameters:${JSON.stringify(this.srtpParameters)},` + 67 | `closed:${this.closed},` + 68 | `appData:${JSON.stringify(this.appData)}` + 69 | `}`; 70 | } 71 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/model/KitchenTransportPipe.ts: -------------------------------------------------------------------------------- 1 | import { PipeTransport } from '../PipeTransport'; 2 | import { TransportTuple, SctpParameters, SrtpParameters, PipeTransportState, PlainTransportState, SctpState } from 'mediakitchen-common'; 3 | import { KitchenApi } from './KitchenApi'; 4 | import { KitchenTransport } from './KitchenTransport'; 5 | 6 | export class KitchenTransportPipe extends KitchenTransport { 7 | 8 | tuple: TransportTuple; 9 | sctpParameters: SctpParameters | null; 10 | srtpParameters: SrtpParameters | null; 11 | sctpState: SctpState | null; 12 | 13 | facade: PipeTransport; 14 | 15 | constructor( 16 | id: string, 17 | state: PipeTransportState, 18 | api: KitchenApi 19 | ) { 20 | super(id, state, api); 21 | 22 | this.tuple = state.tuple; 23 | this.sctpParameters = state.sctpParameters; 24 | this.srtpParameters = state.srtpParameters; 25 | this.sctpState = state.sctpState; 26 | 27 | this.facade = new PipeTransport(this); 28 | } 29 | 30 | async connect(args: { 31 | ip: string, 32 | port: number, 33 | srtpParameters?: SrtpParameters 34 | }) { 35 | if (this.closed) { 36 | throw Error('Transport already closed'); 37 | } 38 | let r = await this.api.connectPipeTransport({ id: this.id, ...args }); 39 | if (this.closed) { 40 | throw Error('Transport already closed'); 41 | } 42 | this.applyState(r); 43 | } 44 | 45 | async getStats() { 46 | if (!this.closed) { 47 | return await this.api.getPipeTransportStats(this.id); 48 | } else { 49 | return null; 50 | } 51 | } 52 | 53 | applyClosed() { 54 | if (this.sctpState) { 55 | this.sctpState = 'closed'; 56 | } 57 | } 58 | 59 | applyStateInternal(state: PlainTransportState) { 60 | this.tuple = state.tuple; 61 | this.sctpParameters = state.sctpParameters; 62 | this.srtpParameters = state.srtpParameters; 63 | this.sctpState = state.sctpState; 64 | } 65 | 66 | async invokeClose() { 67 | await this.api.closePlainTransport(this.id); 68 | } 69 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/model/KitchenTransportPlain.ts: -------------------------------------------------------------------------------- 1 | import { PlainTransport } from '../PlainTransport'; 2 | import { TransportTuple, SctpParameters, SrtpParameters, PlainTransportState, SctpState } from 'mediakitchen-common'; 3 | import { KitchenApi } from './KitchenApi'; 4 | import { KitchenTransport } from './KitchenTransport'; 5 | 6 | export class KitchenTransportPlain extends KitchenTransport { 7 | 8 | tuple: TransportTuple; 9 | rtcpTuple: TransportTuple | null; 10 | sctpParameters: SctpParameters | null; 11 | srtpParameters: SrtpParameters | null; 12 | sctpState: SctpState | null; 13 | 14 | facade: PlainTransport; 15 | 16 | constructor( 17 | id: string, 18 | state: PlainTransportState, 19 | api: KitchenApi 20 | ) { 21 | super(id, state, api); 22 | 23 | this.tuple = state.tuple; 24 | this.rtcpTuple = state.rtcpTuple; 25 | this.sctpParameters = state.sctpParameters; 26 | this.srtpParameters = state.srtpParameters; 27 | this.sctpState = state.sctpState; 28 | 29 | this.facade = new PlainTransport(this); 30 | } 31 | 32 | async connect(args: { 33 | ip?: string, 34 | port?: number, 35 | rtcpPort?: number, 36 | srtpParameters?: SrtpParameters 37 | }) { 38 | if (this.closed) { 39 | throw Error('Transport already closed'); 40 | } 41 | let r = await this.api.connectPlainTransport({ id: this.id, ...args }); 42 | if (this.closed) { 43 | throw Error('Transport already closed'); 44 | } 45 | this.applyState(r); 46 | } 47 | 48 | async getStats() { 49 | if (!this.closed) { 50 | return await this.api.getPlainTransportStats(this.id); 51 | } else { 52 | return null; 53 | } 54 | } 55 | 56 | applyClosed() { 57 | if (this.sctpState) { 58 | this.sctpState = 'closed'; 59 | } 60 | } 61 | 62 | applyStateInternal(state: PlainTransportState) { 63 | this.tuple = state.tuple; 64 | this.rtcpTuple = state.rtcpTuple; 65 | this.sctpParameters = state.sctpParameters; 66 | this.srtpParameters = state.srtpParameters; 67 | this.sctpState = state.sctpState; 68 | } 69 | 70 | async invokeClose() { 71 | await this.api.closePlainTransport(this.id); 72 | } 73 | } -------------------------------------------------------------------------------- /packages/mediakitchen-common/src/wire/events.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { simpleMapCodec } from './common'; 3 | import { routerStateCodec, webRtcTransportStateCodec, producerStateCodec, consumerStateCodec, plainTransportStateCodec, pipeTransportStateCodec } from './states'; 4 | 5 | // 6 | // State Updates 7 | // 8 | 9 | const routerStateEvent = t.type({ 10 | type: t.literal('state-router'), 11 | state: routerStateCodec, 12 | routerId: t.string, 13 | workerId: t.string, 14 | time: t.number 15 | }); 16 | 17 | const webRtcTransportStateEvent = t.type({ 18 | type: t.literal('state-webrtc-transport'), 19 | state: webRtcTransportStateCodec, 20 | transportId: t.string, 21 | routerId: t.string, 22 | workerId: t.string, 23 | time: t.number 24 | }); 25 | 26 | const plainTransportStateEvent = t.type({ 27 | type: t.literal('state-plain-transport'), 28 | state: plainTransportStateCodec, 29 | transportId: t.string, 30 | routerId: t.string, 31 | workerId: t.string, 32 | time: t.number 33 | }); 34 | 35 | const pipeTransportStateEvent = t.type({ 36 | type: t.literal('state-pipe-transport'), 37 | state: pipeTransportStateCodec, 38 | transportId: t.string, 39 | routerId: t.string, 40 | workerId: t.string, 41 | time: t.number 42 | }); 43 | 44 | const producerStateEvent = t.type({ 45 | type: t.literal('state-producer'), 46 | state: producerStateCodec, 47 | producerId: t.string, 48 | transportId: t.string, 49 | routerId: t.string, 50 | workerId: t.string, 51 | time: t.number 52 | }); 53 | 54 | const consumerStateEvent = t.type({ 55 | type: t.literal('state-consumer'), 56 | state: consumerStateCodec, 57 | consumerId: t.string, 58 | producerId: t.string, 59 | transportId: t.string, 60 | routerId: t.string, 61 | workerId: t.string, 62 | time: t.number 63 | }); 64 | 65 | export const eventsCodec = t.union([ 66 | routerStateEvent, 67 | webRtcTransportStateEvent, 68 | plainTransportStateEvent, 69 | pipeTransportStateEvent, 70 | producerStateEvent, 71 | consumerStateEvent 72 | ]); 73 | export type Event = t.TypeOf 74 | 75 | export const eventBoxCodec = t.type({ 76 | event: eventsCodec, 77 | seq: t.number 78 | }); 79 | export type EventBox = t.TypeOf; 80 | 81 | // 82 | // Global Report 83 | // 84 | 85 | export const reportCodec = t.type({ 86 | type: t.literal('report'), 87 | workerId: t.string, 88 | state: t.union([t.literal('alive'), t.literal('dead')]), 89 | appData: simpleMapCodec, 90 | time: t.number 91 | }); 92 | export type Report = t.TypeOf; -------------------------------------------------------------------------------- /packages/mediakitchen/src/model/KitchenTransportWebRTC.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IceCandidate, 3 | DtlsState, 4 | IceState, 5 | WebRtcTransportState, 6 | DtlsParameters, 7 | IceParameters, 8 | } from 'mediakitchen-common'; 9 | import { WebRtcTransport } from '../WebRtcTransport'; 10 | import { KitchenApi } from './KitchenApi'; 11 | import { KitchenTransport } from './KitchenTransport'; 12 | 13 | export class KitchenTransportWebRTC extends KitchenTransport { 14 | 15 | dtlsParameters: DtlsParameters; 16 | dtlsState: DtlsState; 17 | 18 | iceParameters: IceParameters; 19 | iceCandidates: IceCandidate[]; 20 | iceState: IceState; 21 | 22 | facade: WebRtcTransport; 23 | 24 | constructor( 25 | id: string, 26 | state: WebRtcTransportState, 27 | api: KitchenApi 28 | ) { 29 | super(id, state, api); 30 | 31 | this.dtlsParameters = state.dtlsParameters; 32 | this.dtlsState = state.dtlsState; 33 | 34 | this.iceParameters = state.iceParameters; 35 | this.iceCandidates = state.iceCandidates; 36 | this.iceState = state.iceState; 37 | 38 | this.facade = new WebRtcTransport(this); 39 | } 40 | 41 | async connect(args: { dtlsParameters: DtlsParameters }) { 42 | if (this.closed) { 43 | throw Error('Transport already closed'); 44 | } 45 | let r = await this.api.connectWebRtcTransport({ id: this.id, dtlsParameters: args.dtlsParameters }); 46 | if (this.closed) { 47 | throw Error('Transport already closed'); 48 | } 49 | this.applyState(r); 50 | } 51 | 52 | async restartIce() { 53 | if (this.closed) { 54 | throw Error('Transport already closed'); 55 | } 56 | let r = await this.api.restartWebRtcTransport(this.id); 57 | if (this.closed) { 58 | throw Error('Transport already closed'); 59 | } 60 | this.applyState(r); 61 | } 62 | 63 | async getStats() { 64 | if (!this.closed) { 65 | return await this.api.getWebRtcTransportStats(this.id); 66 | } else { 67 | return null; 68 | } 69 | } 70 | 71 | applyStateInternal(state: WebRtcTransportState) { 72 | this.dtlsState = state.dtlsState; 73 | this.iceState = state.iceState; 74 | this.iceParameters = state.iceParameters; 75 | this.iceCandidates = state.iceCandidates; 76 | } 77 | 78 | applyClosed() { 79 | this.dtlsState = 'closed'; 80 | this.iceState = 'closed'; 81 | } 82 | 83 | async invokeClose() { 84 | await this.api.closeWebRtcTransport(this.id); 85 | } 86 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/model/KitchenConsumer.ts: -------------------------------------------------------------------------------- 1 | import { RtpParameters, SimpleMap, ConsumerState, backoff } from 'mediakitchen-common'; 2 | import { Consumer } from '../Consumer'; 3 | import { KitchenApi } from './KitchenApi'; 4 | 5 | export class KitchenConsumer { 6 | readonly id: string; 7 | readonly appData: SimpleMap; 8 | readonly kind: 'video' | 'audio'; 9 | readonly type: 'simple' | 'simulcast' | 'svc' | 'pipe'; 10 | readonly rtpParameters: RtpParameters; 11 | readonly facade: Consumer; 12 | closed: boolean; 13 | paused: boolean; 14 | 15 | #closedExternally: boolean = false; 16 | #lastSeen: number; 17 | #api: KitchenApi; 18 | 19 | constructor( 20 | id: string, 21 | state: ConsumerState, 22 | api: KitchenApi 23 | ) { 24 | this.id = id; 25 | this.appData = state.appData; 26 | this.closed = state.closed; 27 | this.paused = state.paused; 28 | this.kind = state.kind; 29 | this.type = state.type; 30 | this.rtpParameters = state.rtpParameters; 31 | this.#lastSeen = state.time; 32 | this.#api = api; 33 | this.facade = new Consumer(this); 34 | } 35 | 36 | async close() { 37 | if (!this.closed) { 38 | this.closed = true; 39 | this.paused = true; 40 | await backoff(async () => { 41 | if (this.#closedExternally) { 42 | return; 43 | } 44 | await this.#api.closeConsumer(this.id); 45 | }) 46 | } 47 | } 48 | 49 | async pause() { 50 | if (!this.closed) { 51 | let r = await this.#api.pauseConsumer(this.id); 52 | this.applyState(r); 53 | } 54 | } 55 | 56 | async resume() { 57 | if (!this.closed) { 58 | let r = await this.#api.resumeConsumer(this.id); 59 | this.applyState(r); 60 | } 61 | } 62 | 63 | async getStats() { 64 | if (!this.closed) { 65 | return await this.#api.getConsumerStats(this.id); 66 | } else { 67 | return null; 68 | } 69 | } 70 | 71 | onClosed = () => { 72 | this.#closedExternally = true; 73 | if (!this.closed) { 74 | this.closed = true; 75 | this.paused = true; 76 | } 77 | } 78 | 79 | applyState(state: ConsumerState) { 80 | if (this.closed) { 81 | return; 82 | } 83 | if (this.#lastSeen >= state.time) { 84 | return; 85 | } 86 | this.#lastSeen = state.time; 87 | this.paused = state.paused; 88 | this.closed = state.closed; 89 | if (this.closed) { 90 | this.onClosed(); 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/model/KitchenProducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RtpParameters, ProducerState, SimpleMap, backoff 3 | } from 'mediakitchen-common'; 4 | import { Producer } from '../Producer'; 5 | import { KitchenApi } from './KitchenApi'; 6 | 7 | export class KitchenProducer { 8 | readonly id: string; 9 | readonly appData: SimpleMap; 10 | readonly kind: 'video' | 'audio'; 11 | readonly type: 'simple' | 'simulcast' | 'svc'; 12 | readonly rtpParameters: RtpParameters; 13 | readonly facade: Producer; 14 | closed: boolean; 15 | paused: boolean; 16 | 17 | #api: KitchenApi; 18 | #closedExternally: boolean = false; 19 | #lastSeen: number; 20 | 21 | 22 | constructor( 23 | id: string, 24 | state: ProducerState, 25 | api: KitchenApi 26 | ) { 27 | this.id = id; 28 | this.closed = state.closed; 29 | this.paused = state.paused; 30 | this.appData = state.appData; 31 | this.kind = state.kind 32 | this.type = state.type; 33 | this.#lastSeen = state.time; 34 | this.rtpParameters = state.rtpParameters; 35 | this.#api = api; 36 | this.facade = new Producer(this); 37 | } 38 | 39 | async pause() { 40 | if (!this.closed) { 41 | let r = await this.#api.pauseProducer(this.id); 42 | this.applyState(r); 43 | } 44 | } 45 | 46 | async resume() { 47 | if (!this.closed) { 48 | let r = await this.#api.resumeProducer(this.id); 49 | this.applyState(r); 50 | } 51 | } 52 | 53 | async getStats() { 54 | if (!this.closed) { 55 | return await this.#api.getProducerStats(this.id); 56 | } else { 57 | return null; 58 | } 59 | } 60 | 61 | async close() { 62 | if (!this.closed) { 63 | this.closed = true; 64 | this.paused = true; 65 | await backoff(async () => { 66 | if (this.#closedExternally) { 67 | return; 68 | } 69 | await this.#api.closeProducer(this.id); 70 | }) 71 | } 72 | } 73 | 74 | onClosed = () => { 75 | this.#closedExternally = true; 76 | if (!this.closed) { 77 | this.closed = true; 78 | this.paused = true; 79 | } 80 | } 81 | 82 | applyState(state: ProducerState) { 83 | if (this.closed) { 84 | return; 85 | } 86 | if (this.#lastSeen >= state.time) { 87 | return; 88 | } 89 | this.#lastSeen = state.time; 90 | this.paused = state.paused; 91 | this.closed = state.closed; 92 | if (this.closed) { 93 | this.onClosed(); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /packages/mediakitchen-common/src/wire/states.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { 3 | simpleMapCodec, 4 | dtlsParametersCodec, 5 | iceParametersCodec, 6 | iceCandidateCodec, 7 | iceStateCodec, 8 | dtlsStateCodec, 9 | rtpParametersCodec, 10 | transportTupleCodec, 11 | srtpParametersCodec, 12 | sctpParametersCodec, 13 | sctpStateCodec 14 | } from './common'; 15 | 16 | export const routerStateCodec = t.type({ 17 | id: t.string, 18 | closed: t.boolean, 19 | appData: simpleMapCodec, 20 | time: t.number 21 | }); 22 | 23 | export type RouterState = t.TypeOf; 24 | 25 | export const webRtcTransportStateCodec = t.type({ 26 | id: t.string, 27 | closed: t.boolean, 28 | appData: simpleMapCodec, 29 | dtlsParameters: dtlsParametersCodec, 30 | dtlsState: dtlsStateCodec, 31 | iceParameters: iceParametersCodec, 32 | iceCandidates: t.array(iceCandidateCodec), 33 | iceState: iceStateCodec, 34 | time: t.number 35 | }); 36 | export type WebRtcTransportState = t.TypeOf; 37 | 38 | export const plainTransportStateCodec = t.type({ 39 | id: t.string, 40 | closed: t.boolean, 41 | appData: simpleMapCodec, 42 | tuple: transportTupleCodec, 43 | rtcpTuple: t.union([transportTupleCodec, t.null]), 44 | sctpParameters: t.union([sctpParametersCodec, t.null]), 45 | sctpState: t.union([sctpStateCodec, t.null]), 46 | srtpParameters: t.union([srtpParametersCodec, t.null]), 47 | time: t.number 48 | }); 49 | export type PlainTransportState = t.TypeOf; 50 | 51 | export const pipeTransportStateCodec = t.type({ 52 | id: t.string, 53 | closed: t.boolean, 54 | appData: simpleMapCodec, 55 | tuple: transportTupleCodec, 56 | sctpParameters: t.union([sctpParametersCodec, t.null]), 57 | sctpState: t.union([sctpStateCodec, t.null]), 58 | srtpParameters: t.union([srtpParametersCodec, t.null]), 59 | time: t.number 60 | }); 61 | export type PipeTransportState = t.TypeOf; 62 | 63 | export const producerStateCodec = t.type({ 64 | id: t.string, 65 | closed: t.boolean, 66 | appData: simpleMapCodec, 67 | paused: t.boolean, 68 | rtpParameters: rtpParametersCodec, 69 | type: t.union([t.literal('simple'), t.literal('simulcast'), t.literal('svc')]), 70 | kind: t.union([t.literal('audio'), t.literal('video')]), 71 | time: t.number 72 | }); 73 | export type ProducerState = t.TypeOf; 74 | 75 | export const consumerStateCodec = t.type({ 76 | id: t.string, 77 | closed: t.boolean, 78 | appData: simpleMapCodec, 79 | paused: t.boolean, 80 | rtpParameters: rtpParametersCodec, 81 | type: t.union([t.literal('simple'), t.literal('simulcast'), t.literal('svc'), t.literal('pipe')]), 82 | kind: t.union([t.literal('audio'), t.literal('video')]), 83 | time: t.number 84 | }); 85 | export type ConsumerState = t.TypeOf; -------------------------------------------------------------------------------- /packages/mediakitchen/src/model/KitchenTransport.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ProduceCommand, 3 | ConsumeCommand, 4 | SimpleMap, 5 | backoff 6 | } from 'mediakitchen-common'; 7 | import { KitchenConsumer } from './KitchenConsumer'; 8 | import { KitchenProducer } from './KitchenProducer'; 9 | import { KitchenApi } from './KitchenApi'; 10 | 11 | export abstract class KitchenTransport { 12 | id: string; 13 | appData: SimpleMap; 14 | 15 | closed: boolean; 16 | closedExternally: boolean = false; 17 | lastSeen: number; 18 | 19 | api: KitchenApi; 20 | 21 | producers = new Map(); 22 | consumers = new Map(); 23 | 24 | constructor( 25 | id: string, 26 | state: T, 27 | api: KitchenApi 28 | ) { 29 | this.id = id; 30 | this.appData = state.appData; 31 | this.api = api; 32 | 33 | this.closed = state.closed; 34 | this.lastSeen = state.time; 35 | } 36 | 37 | async produce(args: ProduceCommand['args'], retryKey: string) { 38 | let res = await this.api.createProducer(this.id, args as ProduceCommand['args'], retryKey); 39 | if (this.producers.has(res.id)) { 40 | let r = this.producers.get(res.id)!; 41 | r.applyState(res); 42 | return r; 43 | } else { 44 | let r = new KitchenProducer(res.id, res, this.api); 45 | this.producers.set(res.id, r); 46 | return r; 47 | } 48 | } 49 | 50 | async consume(producerId: string, args: ConsumeCommand['args'], retryKey: string) { 51 | let res = await this.api.createConsumer(this.id, producerId, args, retryKey); 52 | if (this.consumers.has(res.id)) { 53 | let r = this.consumers.get(res.id)!; 54 | r.applyState(res); 55 | return r; 56 | } else { 57 | let r = new KitchenConsumer(res.id, res, this.api); 58 | this.consumers.set(res.id, r); 59 | return r; 60 | } 61 | } 62 | 63 | async close() { 64 | if (!this.closed) { 65 | this.closed = true; 66 | this.applyClosed(); 67 | for (let p of this.producers.values()) { 68 | p.onClosed(); 69 | } 70 | for (let c of this.consumers.values()) { 71 | c.onClosed(); 72 | } 73 | await backoff(async () => { 74 | if (this.closedExternally) { 75 | return; 76 | } 77 | await this.invokeClose(); 78 | }); 79 | } 80 | } 81 | 82 | applyState(state: T) { 83 | if (this.closed) { 84 | return; 85 | } 86 | if (this.lastSeen >= state.time) { 87 | return; 88 | } 89 | 90 | this.closed = state.closed; 91 | this.applyStateInternal(state); 92 | if (this.closed) { 93 | this.applyClosed(); 94 | this.onClosed(); 95 | } 96 | } 97 | 98 | onClosed = () => { 99 | this.closedExternally = true; 100 | if (!this.closed) { 101 | this.closed = true; 102 | this.applyClosed(); 103 | for (let p of this.producers.values()) { 104 | p.onClosed(); 105 | } 106 | for (let c of this.consumers.values()) { 107 | c.onClosed(); 108 | } 109 | } 110 | } 111 | 112 | protected abstract applyStateInternal(state: T): void; 113 | protected abstract applyClosed(): void; 114 | protected abstract invokeClose(): Promise; 115 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/model/KitchenCluster.ts: -------------------------------------------------------------------------------- 1 | import * as nats from 'ts-nats'; 2 | import { reportCodec, SimpleMap } from 'mediakitchen-common'; 3 | import { ConnectionInfo } from './../ConnectionInfo'; 4 | import { KitchenWorker } from './KitchenWorker'; 5 | 6 | export class KitchenCluster { 7 | readonly connectionInfo: ConnectionInfo; 8 | 9 | #client: nats.Client 10 | #rootTopic: string; 11 | #subscription!: nats.Subscription; 12 | #alive: boolean = true; 13 | #workers = new Map(); 14 | #healthCheckTimeout: number; 15 | 16 | onWorkerStatusChanged?: (worker: KitchenWorker) => void; 17 | 18 | constructor(connectionInfo: ConnectionInfo) { 19 | this.#client = connectionInfo.nc; 20 | this.#rootTopic = connectionInfo.rootTopic || 'mediakitchen'; 21 | this.connectionInfo = connectionInfo; 22 | this.#healthCheckTimeout = connectionInfo.healthCheckTimeout || 10000; 23 | } 24 | 25 | get alive() { 26 | return this.#alive; 27 | } 28 | 29 | getWorkers() { 30 | let res: KitchenWorker[] = []; 31 | for (let e of this.#workers.values()) { 32 | res.push(e.worker); 33 | } 34 | return res; 35 | } 36 | 37 | close() { 38 | if (this.#alive) { 39 | this.#alive = false; 40 | if (this.#subscription) { 41 | this.#subscription.unsubscribe(); 42 | } 43 | return; 44 | } 45 | } 46 | 47 | // 48 | // Worker Lifecycle 49 | // 50 | 51 | connect = async () => { 52 | this.#subscription = await this.#client.subscribe(this.#rootTopic + '.report', (err, msg) => { 53 | if (err) { 54 | console.warn(err); 55 | return; 56 | } 57 | let event = msg.data; 58 | if (reportCodec.is(event)) { 59 | if (event.state === 'alive') { 60 | this.#onWorkerAlive(event.workerId, event.time, event.appData); 61 | } else if (event.state === 'dead') { 62 | this.#onWorkerDead(event.workerId); 63 | } 64 | } else { 65 | console.warn('Unknown message: ' + JSON.stringify(msg.data || {})); 66 | } 67 | }); 68 | } 69 | 70 | #onWorkerAlive = (id: string, time: number, appData: SimpleMap) => { 71 | if (!this.#workers.has(id)) { 72 | let timer = setTimeout(() => { 73 | this.#onWorkerTimeout(id); 74 | }, this.#healthCheckTimeout); 75 | let worker = new KitchenWorker(id, appData, this); 76 | this.#workers.set(id, { worker, lastSeen: time, timer }); 77 | if (this.onWorkerStatusChanged) { 78 | this.onWorkerStatusChanged(worker); 79 | } 80 | } else { 81 | let ex = this.#workers.get(id)!; 82 | if (ex.lastSeen > time) { 83 | return; 84 | } 85 | ex.worker.onReport(); 86 | 87 | if (ex.worker.status === 'dead') { 88 | return; 89 | } 90 | ex.lastSeen = time; 91 | clearTimeout(ex.timer); 92 | ex.timer = setTimeout(() => { 93 | this.#onWorkerTimeout(id); 94 | }, this.#healthCheckTimeout); 95 | } 96 | } 97 | 98 | #onWorkerDead = (id: string) => { 99 | let ex = this.#workers.get(id)!; 100 | if (!ex) { 101 | return; 102 | } 103 | ex.worker.onDead(); 104 | clearTimeout(ex.timer); 105 | } 106 | 107 | #onWorkerTimeout = (id: string) => { 108 | let ex = this.#workers.get(id)!; 109 | if (!ex) { 110 | return; 111 | } 112 | ex.worker.onReportTimeout(); 113 | } 114 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/WorkerApi.ts: -------------------------------------------------------------------------------- 1 | import { KitchenApi } from './model/KitchenApi'; 2 | import { 3 | RouterCreateCommand, 4 | WebRTCTransportCreateCommand, 5 | WebRTCTransportConnectCommand, 6 | ProduceCommand, 7 | ConsumeCommand, 8 | PlainTransportCreateCommand, 9 | PipeTransportCreateCommand, 10 | PlainTransportConnectCommand, 11 | PipeTransportConnectCommand, 12 | } from 'mediakitchen-common'; 13 | 14 | export class WorkerApi { 15 | readonly #api: KitchenApi; 16 | constructor(api: KitchenApi) { 17 | this.#api = api; 18 | } 19 | 20 | createRouter = (command: RouterCreateCommand['args'], retryKey: string) => { 21 | return this.#api.createRouter(command, retryKey); 22 | } 23 | 24 | closeRouter = (id: string) => { 25 | return this.#api.closeRouter(id); 26 | } 27 | 28 | createWebRtcTransport = (routerId: string, command: WebRTCTransportCreateCommand['args'], retryKey: string) => { 29 | return this.#api.createWebRtcTransport(routerId, command, retryKey); 30 | } 31 | 32 | connectWebRtcTransport = (command: WebRTCTransportConnectCommand['args']) => { 33 | return this.#api.connectWebRtcTransport(command); 34 | } 35 | 36 | restartWebRtcTransport = (id: string) => { 37 | return this.#api.restartWebRtcTransport(id); 38 | } 39 | 40 | closeWebRtcTransport = (id: string) => { 41 | return this.#api.closeWebRtcTransport(id); 42 | } 43 | 44 | getWebRtcTransportStats = (id: string) => { 45 | return this.#api.getWebRtcTransportStats(id); 46 | } 47 | 48 | createPlainTransport = (routerId: string, command: PlainTransportCreateCommand['args'], retryKey: string) => { 49 | return this.#api.createPlainTransport(routerId, command, retryKey); 50 | } 51 | 52 | connectPlainTransport = (command: PlainTransportConnectCommand['args']) => { 53 | return this.#api.connectPlainTransport(command); 54 | } 55 | 56 | closePlainTransport = (id: string) => { 57 | return this.#api.closePlainTransport(id); 58 | } 59 | 60 | getPlainTransportStats = (id: string) => { 61 | return this.#api.getPlainTransportStats(id); 62 | } 63 | 64 | createPipeTransport = (routerId: string, command: PipeTransportCreateCommand['args'], retryKey: string) => { 65 | return this.#api.createPipeTransport(routerId, command, retryKey); 66 | } 67 | 68 | connectPipeTransport = (command: PipeTransportConnectCommand['args']) => { 69 | return this.#api.connectPipeTransport(command); 70 | } 71 | 72 | closePipeTransport = (id: string) => { 73 | return this.#api.closePipeTransport(id); 74 | } 75 | 76 | getPipeTransportStats = (id: string) => { 77 | return this.#api.getPipeTransportStats(id); 78 | } 79 | 80 | createProducer = (transportId: string, command: ProduceCommand['args'], retryKey: string) => { 81 | return this.#api.createProducer(transportId, command, retryKey); 82 | } 83 | 84 | pauseProducer = (producerId: string) => { 85 | return this.#api.pauseProducer(producerId); 86 | } 87 | 88 | resumeProducer = (producerId: string) => { 89 | return this.#api.resumeProducer(producerId); 90 | } 91 | 92 | closeProducer = (producerId: string) => { 93 | return this.#api.closeProducer(producerId); 94 | } 95 | 96 | getProducerStats = (id: string) => { 97 | return this.#api.getProducerStats(id); 98 | } 99 | 100 | createConsumer = (transportId: string, producerId: string, command: ConsumeCommand['args'], retryKey: string) => { 101 | return this.#api.createConsumer(transportId, producerId, command, retryKey); 102 | } 103 | 104 | pauseConsumer = (consumerId: string) => { 105 | return this.#api.pauseConsumer(consumerId); 106 | } 107 | 108 | resumeConsumer = (consumerId: string) => { 109 | return this.#api.resumeConsumer(consumerId); 110 | } 111 | 112 | closeConsumer = (consumerId: string) => { 113 | return this.#api.closeConsumer(consumerId); 114 | } 115 | 116 | getConsumerStats = (id: string) => { 117 | return this.#api.getConsumerStats(id); 118 | } 119 | } -------------------------------------------------------------------------------- /packages/mediakitchen/src/model/KitchenRouter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RouterState, 3 | SimpleMap, 4 | WebRTCTransportCreateCommand, 5 | backoff, 6 | PlainTransportCreateCommand, 7 | PipeTransportCreateCommand 8 | } from 'mediakitchen-common'; 9 | import { Router } from '../Router'; 10 | import { KitchenApi } from './KitchenApi'; 11 | import { KitchenTransport } from './KitchenTransport'; 12 | import { KitchenTransportPipe } from './KitchenTransportPipe'; 13 | import { KitchenTransportPlain } from './KitchenTransportPlain'; 14 | import { KitchenTransportWebRTC } from './KitchenTransportWebRTC'; 15 | 16 | export class KitchenRouter { 17 | id: string; 18 | appData: SimpleMap; 19 | closed: boolean; 20 | closedExternally: boolean = false; 21 | lastSeen: number; 22 | 23 | api: KitchenApi; 24 | facade: Router; 25 | 26 | transports = new Map>(); 27 | 28 | constructor(id: string, state: RouterState, api: KitchenApi) { 29 | this.id = id; 30 | this.appData = Object.freeze({ ...state.appData }); 31 | this.lastSeen = state.time; 32 | this.closed = state.closed; 33 | 34 | this.api = api; 35 | this.facade = new Router(this); 36 | } 37 | 38 | async createWebRTCTransport(args: WebRTCTransportCreateCommand['args'], retryKey: string) { 39 | let res = await this.api.createWebRtcTransport(this.id, args, retryKey); 40 | if (this.transports.has(res.id)) { 41 | let r = this.transports.get(res.id)!; 42 | r.applyState(res); 43 | return r as KitchenTransportWebRTC; 44 | } else { 45 | let ts = new KitchenTransportWebRTC( 46 | res.id, 47 | res, 48 | this.api 49 | ); 50 | this.transports.set(res.id, ts); 51 | return ts; 52 | } 53 | } 54 | 55 | async createPlainTransport(args: PlainTransportCreateCommand['args'], retryKey: string) { 56 | let res = await this.api.createPlainTransport(this.id, args, retryKey); 57 | if (this.transports.has(res.id)) { 58 | let r = this.transports.get(res.id)!; 59 | r.applyState(res); 60 | return r as KitchenTransportPlain; 61 | } else { 62 | let ts = new KitchenTransportPlain( 63 | res.id, 64 | res, 65 | this.api 66 | ); 67 | this.transports.set(res.id, ts); 68 | return ts; 69 | } 70 | } 71 | 72 | async createPipeTransport(args: PipeTransportCreateCommand['args'], retryKey: string) { 73 | let res = await this.api.createPipeTransport(this.id, args, retryKey); 74 | if (this.transports.has(res.id)) { 75 | let r = this.transports.get(res.id)!; 76 | r.applyState(res); 77 | return r as KitchenTransportPipe; 78 | } else { 79 | let ts = new KitchenTransportPipe( 80 | res.id, 81 | res, 82 | this.api 83 | ); 84 | this.transports.set(res.id, ts); 85 | return ts; 86 | } 87 | } 88 | 89 | async close() { 90 | if (!this.closed) { 91 | this.closed = true; 92 | for (let t of this.transports.values()) { 93 | t.onClosed(); 94 | } 95 | await backoff(async () => { 96 | if (this.closedExternally) { 97 | return; 98 | } 99 | await this.api.closeRouter(this.id); 100 | }); 101 | } 102 | } 103 | 104 | applyState = (state: RouterState) => { 105 | if (this.closed) { 106 | return; 107 | } 108 | if (this.lastSeen > state.time) { 109 | return; 110 | } 111 | this.lastSeen = state.time; 112 | this.closed = state.closed; 113 | if (this.closed) { 114 | this.onClosed(); 115 | } 116 | } 117 | 118 | onClosed() { 119 | this.closedExternally = true; 120 | if (!this.closed) { 121 | this.closed = true; 122 | for (let t of this.transports.values()) { 123 | t.onClosed(); 124 | } 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /packages/tests/model.spec.ts: -------------------------------------------------------------------------------- 1 | import { RtpCodecCapability } from 'mediakitchen-common'; 2 | import { randomKey } from 'mediakitchen-common'; 3 | import { connect, Payload } from "ts-nats"; 4 | import { createWorker } from 'mediakitchen-server'; 5 | import { ConnectionInfo, connectToCluster } from 'mediakitchen'; 6 | 7 | export const ROUTER_CODECS: RtpCodecCapability[] = [{ 8 | kind: 'audio', 9 | mimeType: 'audio/opus', 10 | clockRate: 48000, 11 | channels: 2, 12 | rtcpFeedback: [ 13 | { type: 'transport-cc' } 14 | ] 15 | }, { 16 | kind: 'video', 17 | mimeType: 'video/H264', 18 | clockRate: 90000, 19 | parameters: { 20 | 'packetization-mode': 1, 21 | 'profile-level-id': '42e01f', 22 | 'level-asymmetry-allowed': 1, 23 | }, 24 | rtcpFeedback: [{ 25 | type: 'goog-remb' 26 | }, { 27 | type: 'transport-cc' 28 | }, { 29 | type: 'ccm', 30 | parameter: 'fir' 31 | }, { 32 | type: 'nack' 33 | }, { 34 | type: 'nack', 35 | parameter: 'pli' 36 | }] 37 | }, { 38 | kind: 'video', 39 | mimeType: 'video/VP8', 40 | clockRate: 90000, 41 | parameters: {}, 42 | rtcpFeedback: [{ 43 | type: 'goog-remb' 44 | }, { 45 | type: 'transport-cc' 46 | }, { 47 | type: 'ccm', 48 | parameter: 'fir' 49 | }, { 50 | type: 'nack' 51 | }, { 52 | type: 'nack', 53 | parameter: 'pli' 54 | }] 55 | }]; 56 | 57 | describe('Model', () => { 58 | 59 | it('should create and delete routers', async () => { 60 | jest.setTimeout(10000); 61 | const nc = await connect({ payload: Payload.JSON }); 62 | const connectionInfo: ConnectionInfo = { nc, rootTopic: randomKey() }; 63 | const workerInstance = await createWorker({ connectionInfo }); 64 | const cluster = await connectToCluster(connectionInfo); 65 | const worker = cluster.workers[0]; 66 | 67 | const router1 = await worker.createRouter({ mediaCodecs: ROUTER_CODECS }, 'router1'); 68 | const router2 = await worker.createRouter({ mediaCodecs: ROUTER_CODECS }, 'router2'); 69 | const router3 = await worker.createRouter({ mediaCodecs: ROUTER_CODECS }, 'router1'); 70 | const router4 = await worker.createRouter({ mediaCodecs: ROUTER_CODECS }, 'router2'); 71 | expect(router1).toBe(router3); 72 | expect(router2).toBe(router4); 73 | 74 | // Pipe transport 75 | let pipe1 = await router1.createPipeTransport({ enableSrtp: true, enableSctp: true }, 'transport1'); 76 | let pipe2 = await router1.createPipeTransport({ enableSrtp: true, enableSctp: true }, 'transport2'); 77 | expect(pipe1.sctpParameters).not.toBeNull(); 78 | expect(pipe1.sctpState).not.toBeNull(); 79 | expect(pipe1.srtpParameters).not.toBeNull(); 80 | expect(pipe2.sctpParameters).not.toBeNull(); 81 | expect(pipe2.sctpState).not.toBeNull(); 82 | expect(pipe2.srtpParameters).not.toBeNull(); 83 | await pipe1.connect({ ip: pipe2.tuple.localIp, port: pipe2.tuple.localPort, srtpParameters: pipe2.srtpParameters! }); 84 | await pipe2.connect({ ip: pipe1.tuple.localIp, port: pipe1.tuple.localPort, srtpParameters: pipe1.srtpParameters! }); 85 | expect(pipe1.tuple.remoteIp).toBe(pipe2.tuple.localIp); 86 | expect(pipe1.tuple.remotePort).toBe(pipe2.tuple.localPort); 87 | expect(pipe2.tuple.remoteIp).toBe(pipe1.tuple.localIp); 88 | expect(pipe2.tuple.remotePort).toBe(pipe1.tuple.localPort); 89 | 90 | // Produce 91 | await pipe1.produce({ 92 | kind: 'audio', 93 | rtpParameters: { 94 | codecs: [{ 95 | ssrc: 1000, 96 | mimeType: 'audio/opus', 97 | payloadType: 10, 98 | clockRate: 48000, 99 | channels: 2 100 | }], 101 | encodings: [{ ssrc: 1000 }] 102 | } 103 | }, 'produce-1'); 104 | 105 | await router1.close(); 106 | await router2.close(); 107 | expect(router1.closed).toBe(true); 108 | expect(router2.closed).toBe(true); 109 | expect(pipe1.closed).toBe(true); 110 | expect(pipe2.closed).toBe(true); 111 | 112 | cluster.close(); 113 | workerInstance.close(); 114 | nc.close(); 115 | }); 116 | }); -------------------------------------------------------------------------------- /packages/mediakitchen-common/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | DtlsParameters, 3 | DtlsState, 4 | DtlsRole, 5 | IceCandidate, 6 | IceParameters, 7 | IceState, 8 | RtpCodecCapability, 9 | RtpParameters, 10 | RtpCodecParameters, 11 | RtpHeaderExtensionParameters, 12 | RtpHeaderExtensionCodec, 13 | RtpCapabilities, 14 | RtpEncoding, 15 | RtcpParameters, 16 | RtcpFeedback, 17 | SimpleMap, 18 | SctpParameters, 19 | SrtpParameters, 20 | SctpState, 21 | TransportTuple, 22 | NumSctpStreams, 23 | simpleMapCodec, 24 | dtlsRoleCodec, 25 | dtlsParametersCodec, 26 | dtlsStateCodec, 27 | iceParametersCodec, 28 | iceCandidateCodec, 29 | iceStateCodec, 30 | rtcpFeedbackCodec, 31 | rtpCodecCapabilityCodec, 32 | rtpCodecParametersCodec, 33 | rtpHeaderExtensionParametersCodec, 34 | rtpHeaderExtensionCodec, 35 | rtpEncodingCodec, 36 | rtcpParameters, 37 | rtpParametersCodec, 38 | rtpCapabilitiesCodec, 39 | transportTupleCodec, 40 | sctpParametersCodec, 41 | srtpParametersCodec, 42 | sctpStateCodec, 43 | numSctpStreamsCodec 44 | } from './wire/common'; 45 | export { 46 | eventsCodec, 47 | Event, 48 | eventBoxCodec, 49 | EventBox, 50 | reportCodec, 51 | Report 52 | } from './wire/events'; 53 | export { 54 | routerStateCodec, 55 | RouterState, 56 | webRtcTransportStateCodec, 57 | WebRtcTransportState, 58 | pipeTransportStateCodec, 59 | PipeTransportState, 60 | plainTransportStateCodec, 61 | PlainTransportState, 62 | producerStateCodec, 63 | ProducerState, 64 | consumerStateCodec, 65 | ConsumerState 66 | } from './wire/states'; 67 | export { 68 | GetEventsCommand, 69 | GetEventsResponse, 70 | getEventsResponseCodec, 71 | 72 | GetStateCommand, 73 | GetStateResponse, 74 | getStateResponseCodec, 75 | 76 | RouterCreateCommand, 77 | RouterCreateResponse, 78 | routerCreateResponseCodec, 79 | 80 | RouterCloseCommand, 81 | RouterCloseResponse, 82 | routerCloseResponseCodec, 83 | 84 | PlainTransportCreateCommand, 85 | PlainTransportCreateResponse, 86 | plainTransportCreateResponseCodec, 87 | PlainTransportCloseCommand, 88 | PlainTransportCloseResponse, 89 | plainTransportCloseResponseCodec, 90 | PlainTransportConnectCommand, 91 | PlainTransportConnectResponse, 92 | plainTransportConnectResponseCodec, 93 | 94 | WebRTCTransportCreateCommand, 95 | WebRTCTransportCreateResponse, 96 | webRTCTransportCreateResponseCodec, 97 | WebRTCTransportCloseCommand, 98 | WebRTCTransportCloseResponse, 99 | webRtcTransportCloseResponseCodec, 100 | WebRTCTransportConnectCommand, 101 | WebRTCTransportConnectResponse, 102 | webRtcTransportConnectResponseCodec, 103 | WebRTCTransportRestartCommand, 104 | WebRTCTransportRestartResponse, 105 | webRtcTransportRestartResponseCodec, 106 | 107 | PipeTransportCreateCommand, 108 | PipeTransportCreateResponse, 109 | pipeTransportCreateResponseCodec, 110 | PipeTransportCloseCommand, 111 | PipeTransportCloseResponse, 112 | pipeTransportCloseResponseCodec, 113 | PipeTransportConnectCommand, 114 | PipeTransportConnectResponse, 115 | pipeTransportConnectResponseCodec, 116 | 117 | ProduceCommand, 118 | ProduceResponse, 119 | produceResponseCodec, 120 | 121 | ProducePauseCommand, 122 | ProducePauseResponse, 123 | producePauseResponseCodec, 124 | 125 | ProduceResumeCommand, 126 | ProduceResumeResponse, 127 | produceResumeResponseCodec, 128 | 129 | ProduceCloseCommand, 130 | ProduceCloseResponse, 131 | produceCloseResponseCodec, 132 | 133 | ConsumeCommand, 134 | ConsumeResponse, 135 | consumeResponseCodec, 136 | 137 | ConsumePauseCommand, 138 | ConsumePauseResponse, 139 | consumePauseResponseCodec, 140 | 141 | ConsumeResumeCommand, 142 | ConsumeResumeResponse, 143 | consumeResumeResponseCodec, 144 | 145 | ConsumeCloseCommand, 146 | ConsumeCloseResponse, 147 | consumeCloseResponseCodec, 148 | 149 | StatsCommand, 150 | StatsResponse, 151 | statsResponseCodec, 152 | 153 | commandsCodec, 154 | Commands, 155 | commandBoxCodec, 156 | CommandBox, 157 | } from './wire/commands'; 158 | export { 159 | backoff 160 | } from './utils/backoff'; 161 | export { 162 | delay 163 | } from './utils/delay'; 164 | export { 165 | randomKey 166 | } from './utils/randomKey'; 167 | export { 168 | now 169 | } from './utils/time'; 170 | export { 171 | AsyncLock, 172 | AsyncLockMap 173 | } from './utils/AsyncLock'; -------------------------------------------------------------------------------- /packages/mediakitchen/src/Stats.ts: -------------------------------------------------------------------------------- 1 | export type ConsumerStats = { 2 | type: string; 3 | timestamp: number; 4 | ssrc: number; 5 | rtxSsrc?: number; 6 | kind: string; 7 | mimeType: string; 8 | packetsLost: number; 9 | fractionLost: number; 10 | packetsDiscarded: number; 11 | packetsRetransmitted: number; 12 | packetsRepaired: number; 13 | nackCount: number; 14 | nackPacketCount: number; 15 | pliCount: number; 16 | firCount: number; 17 | score: number; 18 | packetCount: number; 19 | byteCount: number; 20 | bitrate: number; 21 | roundTripTime?: number; 22 | rtxPacketsDiscarded?: number; 23 | jitter: number; 24 | bitrateByLayer?: any; 25 | }; 26 | 27 | export type ProducerStats = { 28 | type: string; 29 | timestamp: number; 30 | ssrc: number; 31 | rtxSsrc?: number; 32 | rid?: string; 33 | kind: string; 34 | mimeType: string; 35 | packetsLost: number; 36 | fractionLost: number; 37 | packetsDiscarded: number; 38 | packetsRetransmitted: number; 39 | packetsRepaired: number; 40 | nackCount: number; 41 | nackPacketCount: number; 42 | pliCount: number; 43 | firCount: number; 44 | score: number; 45 | packetCount: number; 46 | byteCount: number; 47 | bitrate: number; 48 | roundTripTime?: number; 49 | rtxPacketsDiscarded?: number; 50 | // RtpStreamRecv specific. 51 | jitter: number; 52 | bitrateByLayer?: any; 53 | }[]; 54 | 55 | type SctpState = 'new' | 'connecting' | 'connected' | 'failed' | 'closed'; 56 | type IceState = 'new' | 'connected' | 'completed' | 'disconnected' | 'closed'; 57 | type DtlsState = 'new' | 'connecting' | 'connected' | 'failed' | 'closed'; 58 | type TransportProtocol = 'udp' | 'tcp' 59 | interface TransportTuple { 60 | localIp: string; 61 | localPort: number; 62 | remoteIp?: string; 63 | remotePort?: number; 64 | protocol: TransportProtocol; 65 | } 66 | 67 | export type WebRtcTransportStats = { 68 | // Common to all Transports. 69 | type: string; 70 | transportId: string; 71 | timestamp: number; 72 | sctpState?: SctpState; 73 | bytesReceived: number; 74 | recvBitrate: number; 75 | bytesSent: number; 76 | sendBitrate: number; 77 | rtpBytesReceived: number; 78 | rtpRecvBitrate: number; 79 | rtpBytesSent: number; 80 | rtpSendBitrate: number; 81 | rtxBytesReceived: number; 82 | rtxRecvBitrate: number; 83 | rtxBytesSent: number; 84 | rtxSendBitrate: number; 85 | probationBytesSent: number; 86 | probationSendBitrate: number; 87 | availableOutgoingBitrate?: number; 88 | availableIncomingBitrate?: number; 89 | maxIncomingBitrate?: number; 90 | // WebRtcTransport specific. 91 | iceRole: string; 92 | iceState: IceState; 93 | iceSelectedTuple?: TransportTuple; 94 | dtlsState: DtlsState; 95 | }[]; 96 | 97 | export type PipeTransportStats = { 98 | // Common to all Transports. 99 | type: string; 100 | transportId: string; 101 | timestamp: number; 102 | sctpState?: SctpState; 103 | bytesReceived: number; 104 | recvBitrate: number; 105 | bytesSent: number; 106 | sendBitrate: number; 107 | rtpBytesReceived: number; 108 | rtpRecvBitrate: number; 109 | rtpBytesSent: number; 110 | rtpSendBitrate: number; 111 | rtxBytesReceived: number; 112 | rtxRecvBitrate: number; 113 | rtxBytesSent: number; 114 | rtxSendBitrate: number; 115 | probationBytesSent: number; 116 | probationSendBitrate: number; 117 | availableOutgoingBitrate?: number; 118 | availableIncomingBitrate?: number; 119 | maxIncomingBitrate?: number; 120 | // PipeTransport specific. 121 | tuple: TransportTuple; 122 | }[]; 123 | 124 | export type PlainTransportStats = { 125 | // Common to all Transports. 126 | type: string; 127 | transportId: string; 128 | timestamp: number; 129 | sctpState?: SctpState; 130 | bytesReceived: number; 131 | recvBitrate: number; 132 | bytesSent: number; 133 | sendBitrate: number; 134 | rtpBytesReceived: number; 135 | rtpRecvBitrate: number; 136 | rtpBytesSent: number; 137 | rtpSendBitrate: number; 138 | rtxBytesReceived: number; 139 | rtxRecvBitrate: number; 140 | rtxBytesSent: number; 141 | rtxSendBitrate: number; 142 | probationBytesSent: number; 143 | probationSendBitrate: number; 144 | availableOutgoingBitrate?: number; 145 | availableIncomingBitrate?: number; 146 | maxIncomingBitrate?: number; 147 | // PlainTransport specific. 148 | rtcpMux: boolean; 149 | comedia: boolean; 150 | tuple: TransportTuple; 151 | rtcpTuple?: TransportTuple; 152 | }[]; -------------------------------------------------------------------------------- /packages/mediakitchen-server/src/server.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import * as changeCase from 'change-case'; 3 | import publicIp from 'public-ip'; 4 | import { connect, Payload } from 'ts-nats'; 5 | import { 6 | randomKey, backoff, SimpleMap 7 | } from 'mediakitchen-common'; 8 | import { ServerWorker } from './ServerWorker'; 9 | import { createWorker } from "./createWorker"; 10 | import { WorkerLogTag } from 'mediasoup/lib/Worker'; 11 | import debug from 'debug'; 12 | 13 | const loggerInfo = debug('mediakitchen:'); 14 | loggerInfo.log = console.info.bind(console); 15 | 16 | (async () => { 17 | try { 18 | loggerInfo('Starting...'); 19 | 20 | // Resolve Workers Count 21 | const workersCount = 22 | process.env.MEDIAKITCHEN_WORKERS 23 | ? parseInt(process.env.MEDIAKITCHEN_WORKERS, 10) 24 | : os.cpus().length; 25 | 26 | // Resolve IPs 27 | let listenIp = process.env.MEDIAKITCHEN_LISTEN || '0.0.0.0'; 28 | let announce = process.env.MEDIAKITCHEN_ANNOUNCE || '127.0.0.1'; 29 | if (process.env.MEDIAKITCHEN_DETECT_IP === 'true') { 30 | loggerInfo('Detecting public ip...'); 31 | let ip = await publicIp.v4(); 32 | loggerInfo('IP detected: ' + ip); 33 | announce = ip; 34 | } 35 | 36 | // Resolve Ports 37 | let minPort = process.env.MEDIAKITCHEN_MIN_PORT ? parseInt(process.env.MEDIAKITCHEN_MIN_PORT, 10) : 10000; 38 | let maxPort = process.env.MEDIAKITCHEN_MAX_PORT ? parseInt(process.env.MEDIAKITCHEN_MAX_PORT, 10) : 59999; 39 | 40 | // Resolve log tags 41 | let logTags: WorkerLogTag[] = []; 42 | if (process.env.MEDIAKITCHEN_LOG_TAGS) { 43 | logTags = process.env.MEDIAKITCHEN_LOG_TAGS.split(',') as WorkerLogTag[]; 44 | } 45 | 46 | // Resolve log level 47 | let logLevel: 'debug' | 'warn' | 'error' | 'none' = 'error'; 48 | if (process.env.MEDIAKITCHEN_LOG_LEVEL === 'debug') { 49 | logLevel = 'debug'; 50 | } else if (process.env.MEDIAKITCHEN_LOG_LEVEL === 'warn') { 51 | logLevel = 'warn'; 52 | } else if (process.env.MEDIAKITCHEN_LOG_LEVEL === 'error') { 53 | logLevel = 'error'; 54 | } else if (process.env.MEDIAKITCHEN_LOG_LEVEL === 'none') { 55 | logLevel = 'none'; 56 | } 57 | 58 | // Resolve Root Topic 59 | let rootTopic = process.env.MEDIAKITCHEN_TOPIC || 'mediakitchen'; 60 | 61 | // NATS 62 | let natsHost = process.env.MEDIAKITCHEN_NATS ? process.env.MEDIAKITCHEN_NATS.split(',').map((v) => v.trim()) : []; 63 | 64 | // Resolve App Data 65 | const appData: SimpleMap = {}; 66 | const processId = randomKey(); 67 | appData['process'] = processId; 68 | appData['topic'] = rootTopic; 69 | appData['ip'] = announce; 70 | appData['maxPort'] = maxPort; 71 | appData['minPort'] = minPort; 72 | 73 | for (let k of Object.keys(process.env)) { 74 | if (k.startsWith('MEDIAKITCHEN_')) { 75 | let subs = k.substring('MEDIAKITCHEN_'.length); 76 | let converted = changeCase.camelCase(subs); 77 | appData[converted] = process.env[k]! 78 | } 79 | } 80 | 81 | loggerInfo('App Data:'); 82 | loggerInfo(appData); 83 | 84 | // Connect to NATS 85 | loggerInfo('Connecting to NATS...'); 86 | const nc = await connect({ payload: Payload.JSON, servers: natsHost.length > 0 ? natsHost : undefined }); 87 | 88 | // Spawn Workers 89 | loggerInfo('Spawing workers....'); 90 | let closing = false; 91 | let workers: ServerWorker[] = []; 92 | async function spawnWorker(index: number) { 93 | let w = await createWorker({ 94 | listenIp: { ip: listenIp, announcedIp: announce }, 95 | connectionInfo: { nc, rootTopic }, 96 | settings: { 97 | rtcMaxPort: maxPort, 98 | rtcMinPort: minPort, 99 | logTags, 100 | logLevel, 101 | appData: appData 102 | } 103 | }); 104 | w.onClosed = () => { 105 | if (closing) { 106 | return; 107 | } 108 | loggerInfo('Worker ' + w.id + ' closed'); 109 | backoff(async () => { 110 | if (closing) { 111 | return; 112 | } 113 | await spawnWorker(index); 114 | }); 115 | }; 116 | if (closing) { 117 | w.close(); 118 | return; 119 | } 120 | loggerInfo('Worker ' + w.id + ' started'); 121 | workers[index] = w; 122 | } 123 | for (let i = 0; i < workersCount; i++) { 124 | await spawnWorker(i); 125 | } 126 | 127 | // Started 128 | loggerInfo('Started'); 129 | 130 | // Graceful shutdown 131 | async function onExit() { 132 | if (closing) { 133 | return; 134 | } 135 | closing = true; 136 | loggerInfo('Stopping....'); 137 | for (let w of workers) { 138 | w.close(); 139 | } 140 | await nc.flush(); 141 | console.log('Bye!'); 142 | } 143 | process.on('exit', onExit); 144 | process.on('SIGTERM', onExit); 145 | process.on('SIGINT', onExit); 146 | } catch (e) { 147 | console.warn(e); 148 | process.exit(-1); 149 | } 150 | })() -------------------------------------------------------------------------------- /packages/mediakitchen-common/src/wire/common.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | 3 | // App Data 4 | 5 | export const simpleMapCodec = t.record(t.string, t.union([t.string, t.number, t.boolean])); 6 | export type SimpleMap = t.TypeOf; 7 | 8 | // DTLS 9 | 10 | export const dtlsRoleCodec = t.union([t.literal('auto'), t.literal('client'), t.literal('server')]); 11 | export type DtlsRole = t.TypeOf; 12 | 13 | export const dtlsParametersCodec = t.intersection([t.type({ 14 | fingerprints: t.array(t.type({ algorithm: t.string, value: t.string })) 15 | }), t.partial({ 16 | role: dtlsRoleCodec 17 | })]); 18 | export type DtlsParameters = t.TypeOf; 19 | 20 | export const dtlsStateCodec = t.union([t.literal('new'), t.literal('connecting'), t.literal('connected'), t.literal('failed'), t.literal('closed')]); 21 | export type DtlsState = t.TypeOf; 22 | 23 | // ICE 24 | 25 | export const iceParametersCodec = t.intersection([t.type({ 26 | usernameFragment: t.string, 27 | password: t.string 28 | }), t.partial({ 29 | iceLite: t.boolean 30 | })]); 31 | 32 | export type IceParameters = t.TypeOf; 33 | 34 | export const iceCandidateCodec = t.intersection([t.type({ 35 | foundation: t.string, 36 | priority: t.number, 37 | ip: t.string, 38 | protocol: t.union([t.literal('udp'), t.literal('tcp')]), 39 | port: t.number, 40 | type: t.literal('host') 41 | }), t.partial({ 42 | tcpType: t.literal('passive') 43 | })]); 44 | 45 | export type IceCandidate = t.TypeOf; 46 | 47 | export const iceStateCodec = t.union([t.literal('new'), t.literal('connected'), t.literal('completed'), t.literal('disconnected'), t.literal('closed')]) 48 | export type IceState = t.TypeOf; 49 | 50 | // RTP Feedback 51 | 52 | export const rtcpFeedbackCodec = t.intersection([t.type({ 53 | type: t.string, 54 | }), t.partial({ 55 | parameter: t.string 56 | })]); 57 | export type RtcpFeedback = t.TypeOf; 58 | 59 | // RTP Codec Capability 60 | 61 | export const rtpCodecCapabilityCodec = t.intersection([t.type({ 62 | kind: t.union([t.literal('audio'), t.literal('video')]), 63 | mimeType: t.string, 64 | clockRate: t.number, 65 | }), t.partial({ 66 | channels: t.number, 67 | parameters: simpleMapCodec, 68 | rtcpFeedback: t.array(rtcpFeedbackCodec), 69 | preferredPayloadType: t.number 70 | })]); 71 | export type RtpCodecCapability = t.TypeOf; 72 | 73 | // RTP Codec Parameters 74 | 75 | export const rtpCodecParametersCodec = t.intersection([t.type({ 76 | mimeType: t.string, 77 | payloadType: t.number, 78 | clockRate: t.number, 79 | }), t.partial({ 80 | channels: t.number, 81 | parameters: simpleMapCodec, 82 | rtcpFeedback: t.array(rtcpFeedbackCodec) 83 | })]); 84 | 85 | export type RtpCodecParameters = t.TypeOf; 86 | 87 | // RTP Header Extension Parameters 88 | 89 | export const rtpHeaderExtensionParametersCodec = t.intersection([t.type({ 90 | uri: t.string, 91 | id: t.number, 92 | }), t.partial({ 93 | encrypt: t.boolean, 94 | parameters: simpleMapCodec 95 | })]); 96 | export type RtpHeaderExtensionParameters = t.TypeOf; 97 | 98 | export const rtpHeaderExtensionCodec = t.intersection([t.type({ 99 | uri: t.string, 100 | preferredId: t.number, 101 | }), t.partial({ 102 | kind: t.union([t.literal(''), t.literal('audio'), t.literal('video')]), 103 | preferredEncrypt: t.boolean, 104 | direction: t.union([t.literal('sendrecv'), t.literal('sendonly'), t.literal('recvonly'), t.literal('inactive')]) 105 | })]); 106 | export type RtpHeaderExtensionCodec = t.TypeOf; 107 | 108 | // RTP Encoding 109 | 110 | export const rtpEncodingCodec = t.partial({ 111 | ssrc: t.number, 112 | rid: t.string, 113 | codecPayloadType: t.number, 114 | rtx: t.type({ ssrc: t.number }), 115 | dtx: t.boolean, 116 | scalabilityMode: t.string 117 | }); 118 | export type RtpEncoding = t.TypeOf; 119 | 120 | // RTCP Parameters 121 | 122 | export const rtcpParameters = t.partial({ 123 | cname: t.string, 124 | reducedSize: t.boolean, 125 | mux: t.boolean 126 | }); 127 | export type RtcpParameters = t.TypeOf; 128 | 129 | // RTP Parameters 130 | 131 | export const rtpParametersCodec = t.intersection([t.type({ 132 | codecs: t.array(rtpCodecParametersCodec), 133 | }), t.partial({ 134 | mid: t.string, 135 | headerExtensions: t.array(rtpHeaderExtensionParametersCodec), 136 | encodings: t.array(rtpEncodingCodec), 137 | rtcp: rtcpParameters 138 | })]); 139 | 140 | export type RtpParameters = t.TypeOf; 141 | 142 | // RTP Capabilities 143 | 144 | export const rtpCapabilitiesCodec = t.partial({ 145 | codecs: t.array(rtpCodecCapabilityCodec), 146 | headerExtensions: t.array(rtpHeaderExtensionCodec), 147 | fecMechanisms: t.array(t.string) 148 | }) 149 | export type RtpCapabilities = t.TypeOf; 150 | 151 | // Plain Transport 152 | 153 | export const transportTupleCodec = t.intersection([t.type({ 154 | localIp: t.string, 155 | localPort: t.number, 156 | protocol: t.union([t.literal('tcp'), t.literal('udp')]) 157 | }), t.partial({ 158 | remoteIp: t.string, 159 | remotePort: t.number 160 | })]); 161 | export type TransportTuple = t.TypeOf; 162 | 163 | // SCTP Parameters 164 | 165 | export const sctpParametersCodec = t.type({ 166 | port: t.number, 167 | OS: t.number, 168 | MIS: t.number, 169 | maxMessageSize: t.number 170 | }); 171 | export type SctpParameters = t.TypeOf; 172 | 173 | // SRTP Parameters 174 | 175 | export const srtpParametersCodec = t.type({ 176 | cryptoSuite: t.union([t.literal('AES_CM_128_HMAC_SHA1_80'), t.literal('AES_CM_128_HMAC_SHA1_32')]), 177 | keyBase64: t.string 178 | }); 179 | export type SrtpParameters = t.TypeOf; 180 | 181 | // SCTP State 182 | 183 | export const sctpStateCodec = t.union([t.literal('new'), t.literal('connecting'), t.literal('connected'), t.literal('failed'), t.literal('failed'), t.literal('closed')]); 184 | export type SctpState = t.TypeOf; 185 | 186 | // NumSctpStreams 187 | export const numSctpStreamsCodec = t.type({ 188 | OS: t.number, 189 | MIS: t.number 190 | }); 191 | export type NumSctpStreams = t.TypeOf; -------------------------------------------------------------------------------- /packages/mediakitchen/src/model/KitchenWorker.ts: -------------------------------------------------------------------------------- 1 | import { KitchenTransportPipe } from './KitchenTransportPipe'; 2 | import { 3 | RouterState, WebRtcTransportState, ProducerState, ConsumerState, 4 | RouterCreateCommand, SimpleMap, backoff, PlainTransportState, PipeTransportState 5 | } from 'mediakitchen-common'; 6 | import { Worker } from '../Worker'; 7 | import { KitchenApi } from './KitchenApi'; 8 | import { KitchenRouter } from './KitchenRouter'; 9 | import { KitchenCluster } from './KitchenCluster'; 10 | import { KitchenTransportWebRTC } from './KitchenTransportWebRTC'; 11 | import { KitchenTransportPlain } from './KitchenTransportPlain'; 12 | 13 | export class KitchenWorker { 14 | 15 | #id: string; 16 | #appData: SimpleMap; 17 | 18 | #status: 'healthy' | 'unhealthy' | 'dead' = 'healthy'; 19 | #closedExternally: boolean = false; 20 | 21 | #routers = new Map(); 22 | 23 | #api: KitchenApi; 24 | #cluster: KitchenCluster; 25 | #facade: Worker; 26 | 27 | constructor(id: string, appData: SimpleMap, cluster: KitchenCluster) { 28 | this.#id = id; 29 | this.#appData = Object.freeze(appData); 30 | this.#cluster = cluster; 31 | this.#api = new KitchenApi(id, cluster.connectionInfo); 32 | this.#api.onEvent = (e) => { 33 | if (e.type === 'state-router') { 34 | this.#onRouterState(e.state); 35 | } else if (e.type === 'state-webrtc-transport') { 36 | this.#onWebRtcTransportState(e.routerId, e.state); 37 | } else if (e.type === 'state-plain-transport') { 38 | this.#onPlainTransportState(e.routerId, e.state); 39 | } else if (e.type === 'state-pipe-transport') { 40 | this.#onPipeTransportState(e.routerId, e.state); 41 | } else if (e.type === 'state-consumer') { 42 | this.#onConsumerState(e.routerId, e.transportId, e.state); 43 | } else if (e.type === 'state-producer') { 44 | this.#onProducerState(e.routerId, e.transportId, e.state); 45 | } 46 | } 47 | this.#facade = new Worker(this); 48 | } 49 | 50 | get id() { 51 | return this.#id; 52 | } 53 | 54 | get appData() { 55 | return this.#appData; 56 | } 57 | 58 | get status() { 59 | return this.#status; 60 | } 61 | 62 | get facade() { 63 | return this.#facade; 64 | } 65 | 66 | get api() { 67 | return this.#api; 68 | } 69 | 70 | // 71 | // Actions 72 | // 73 | 74 | async createRouter(args: RouterCreateCommand['args'], retryKey: string) { 75 | let res = await this.#api.createRouter(args, retryKey); 76 | let id = res.id; 77 | if (this.#routers.has(id)) { 78 | let r = this.#routers.get(id)!; 79 | r.applyState(res); 80 | return r; 81 | } else { 82 | let r = new KitchenRouter(res.id, res, this.#api); 83 | this.#routers.set(id, r); 84 | return r; 85 | } 86 | } 87 | 88 | // 89 | // Kill 90 | // 91 | 92 | kill() { 93 | if (this.#status === 'dead') { 94 | return; 95 | } 96 | this.#status = 'dead'; 97 | for (let r of this.#routers.values()) { 98 | if (!r.closed) { 99 | r.onClosed(); 100 | } 101 | } 102 | if (this.#cluster.onWorkerStatusChanged) { 103 | this.#cluster.onWorkerStatusChanged(this); 104 | } 105 | 106 | // Kill worker in background 107 | backoff(async () => { 108 | if (this.#closedExternally) { 109 | return; 110 | } 111 | await this.#api.killWorker(); 112 | }) 113 | } 114 | 115 | // 116 | // Livecycle 117 | // 118 | 119 | onReport() { 120 | if (this.#status === 'healthy' || this.#status === 'dead') { 121 | return; 122 | } 123 | this.#status = 'healthy'; 124 | if (this.#cluster.onWorkerStatusChanged) { 125 | this.#cluster.onWorkerStatusChanged(this); 126 | } 127 | } 128 | 129 | onReportTimeout() { 130 | if (this.#status === 'unhealthy' || this.#status === 'dead') { 131 | return; 132 | } 133 | this.#status = 'unhealthy'; 134 | if (this.#cluster.onWorkerStatusChanged) { 135 | this.#cluster.onWorkerStatusChanged(this); 136 | } 137 | } 138 | 139 | onDead() { 140 | this.#closedExternally = true; 141 | if (this.#status === 'dead') { 142 | return; 143 | } 144 | 145 | this.#status = 'dead'; 146 | for (let r of this.#routers.values()) { 147 | if (!r.closed) { 148 | r.onClosed(); 149 | } 150 | } 151 | if (this.#cluster.onWorkerStatusChanged) { 152 | this.#cluster.onWorkerStatusChanged(this); 153 | } 154 | } 155 | 156 | // 157 | // Events 158 | // 159 | 160 | #onRouterState = (state: RouterState) => { 161 | if (this.#status === 'dead') { 162 | return; 163 | } 164 | 165 | let r = this.#routers.get(state.id); 166 | if (r) { 167 | r.applyState(state); 168 | } 169 | } 170 | 171 | #onWebRtcTransportState = (routerId: string, state: WebRtcTransportState) => { 172 | if (this.#status === 'dead') { 173 | return; 174 | } 175 | 176 | let r = this.#routers.get(routerId); 177 | if (r) { 178 | let tr = r.transports.get(state.id); 179 | if (tr && tr instanceof KitchenTransportWebRTC) { 180 | tr.applyState(state); 181 | } 182 | } 183 | } 184 | 185 | #onPlainTransportState = (routerId: string, state: PlainTransportState) => { 186 | if (this.#status === 'dead') { 187 | return; 188 | } 189 | 190 | let r = this.#routers.get(routerId); 191 | if (r) { 192 | let tr = r.transports.get(state.id); 193 | if (tr && tr instanceof KitchenTransportPlain) { 194 | tr.applyState(state); 195 | } 196 | } 197 | } 198 | 199 | #onPipeTransportState = (routerId: string, state: PipeTransportState) => { 200 | if (this.#status === 'dead') { 201 | return; 202 | } 203 | 204 | let r = this.#routers.get(routerId); 205 | if (r) { 206 | let tr = r.transports.get(state.id); 207 | if (tr && tr instanceof KitchenTransportPipe) { 208 | tr.applyState(state); 209 | } 210 | } 211 | } 212 | 213 | #onProducerState = (routerId: string, transportId: string, state: ProducerState) => { 214 | if (this.#status === 'dead') { 215 | return; 216 | } 217 | 218 | let r = this.#routers.get(routerId); 219 | if (r) { 220 | let tr = r.transports.get(transportId); 221 | if (tr) { 222 | let p = tr.producers.get(state.id); 223 | if (p) { 224 | p.applyState(state); 225 | } 226 | } 227 | } 228 | } 229 | 230 | #onConsumerState = (routerId: string, transportId: string, state: ConsumerState) => { 231 | if (this.#status === 'dead') { 232 | return; 233 | } 234 | 235 | let r = this.#routers.get(routerId); 236 | if (r) { 237 | let tr = r.transports.get(transportId); 238 | if (tr) { 239 | let c = tr.consumers.get(state.id); 240 | if (c) { 241 | c.applyState(state); 242 | } 243 | } 244 | } 245 | } 246 | } -------------------------------------------------------------------------------- /packages/tests/api.spec.ts: -------------------------------------------------------------------------------- 1 | import { connect, Payload } from 'ts-nats'; 2 | import { 3 | ConnectionInfo 4 | } from 'mediakitchen'; 5 | import { randomKey, delay, Event } from 'mediakitchen-common'; 6 | import { createWorker } from 'mediakitchen-server'; 7 | import { KitchenApi } from '../mediakitchen/src/model/KitchenApi'; 8 | 9 | describe('api', () => { 10 | 11 | it('should return state and events', async () => { 12 | const nc = await connect({ payload: Payload.JSON }); 13 | const connectionInfo: ConnectionInfo = { nc, rootTopic: randomKey() }; 14 | const worker = await createWorker({ connectionInfo }); 15 | const api = new KitchenApi(worker.id, connectionInfo); 16 | let state = await api.getState(); 17 | expect(state.seq).toBe(0); 18 | let events = await api.getEvents(0); 19 | expect(events.seq).toBe(0); 20 | expect(events.hasMore).toBe(false); 21 | expect(events.events.length).toBe(0); 22 | worker.close(); 23 | nc.close(); 24 | api.close(); 25 | }); 26 | 27 | it('should create router', async () => { 28 | const nc = await connect({ payload: Payload.JSON }); 29 | const connectionInfo: ConnectionInfo = { nc, rootTopic: randomKey() }; 30 | const worker = await createWorker({ connectionInfo }); 31 | const api = new KitchenApi(worker.id, connectionInfo); 32 | let fn = jest.fn() 33 | api.onEvent = fn; 34 | 35 | // Create Router 36 | let routerState1 = await api.createRouter({ mediaCodecs: [] }, 'router1'); 37 | let routerState2 = await api.createRouter({ mediaCodecs: [], appData: { data: 'string' } }, 'router1'); 38 | expect(routerState1.id).toBe(routerState2.id); 39 | expect(routerState1.closed).toBe(false); 40 | expect(Object.keys(routerState1.appData).length).toBe(0); 41 | 42 | // Close Router 43 | let routerState3 = await api.closeRouter(routerState1.id); 44 | let routerState4 = await api.createRouter({ mediaCodecs: [] }, 'router1'); 45 | let routerState5 = await api.closeRouter(routerState1.id); 46 | expect(routerState4.id).toBe(routerState3.id); 47 | expect(routerState4.id).toBe(routerState5.id); 48 | expect(routerState3.closed).toBe(true); 49 | expect(routerState4.closed).toBe(true); 50 | expect(routerState5.closed).toBe(true); 51 | 52 | // Check Events 53 | await delay(50); 54 | expect(fn.mock.calls.length).toBe(2); 55 | expect(fn.mock.calls[0][0].type).toBe('state-router'); 56 | expect(fn.mock.calls[0][0].state.closed).toBe(false); 57 | expect(fn.mock.calls[0][0].state.id).toBe(routerState1.id); 58 | expect(Object.keys(fn.mock.calls[0][0].state.appData).length).toBe(0); 59 | expect(fn.mock.calls[1][0].type).toBe('state-router'); 60 | expect(fn.mock.calls[1][0].state.closed).toBe(true); 61 | expect(fn.mock.calls[1][0].state.id).toBe(routerState1.id); 62 | expect(Object.keys(fn.mock.calls[1][0].state.appData).length).toBe(0); 63 | 64 | worker.close(); 65 | nc.close(); 66 | api.close(); 67 | }); 68 | 69 | it('should create transport', async () => { 70 | const nc = await connect({ payload: Payload.JSON }); 71 | const connectionInfo: ConnectionInfo = { nc, rootTopic: randomKey() }; 72 | const worker1 = await createWorker({ connectionInfo }); 73 | const worker2 = await createWorker({ connectionInfo }); 74 | 75 | const api1 = new KitchenApi(worker1.id, connectionInfo); 76 | const api2 = new KitchenApi(worker2.id, connectionInfo); 77 | 78 | let router1 = (await api1.createRouter({ mediaCodecs: [] }, 'router1')).id; 79 | let router2 = (await api2.createRouter({ mediaCodecs: [] }, 'router2')).id; 80 | 81 | let transport1 = (await api1.createWebRtcTransport(router1, {}, 'transport1')); 82 | let transport2 = (await api2.createWebRtcTransport(router2, {}, 'transport2')); 83 | await api1.connectWebRtcTransport({ id: transport1.id, dtlsParameters: { role: 'client', fingerprints: transport2.dtlsParameters.fingerprints } }); 84 | await api2.connectWebRtcTransport({ id: transport2.id, dtlsParameters: { role: 'server', fingerprints: transport2.dtlsParameters.fingerprints } }); 85 | 86 | // Double invoke 87 | let transport3 = await api1.connectWebRtcTransport({ id: transport1.id, dtlsParameters: { role: 'client', fingerprints: transport2.dtlsParameters.fingerprints } }); 88 | let transport4 = await api2.connectWebRtcTransport({ id: transport2.id, dtlsParameters: { role: 'server', fingerprints: transport2.dtlsParameters.fingerprints } }); 89 | expect(transport3.id).toBe(transport1.id); 90 | expect(transport4.id).toBe(transport2.id); 91 | 92 | // Close 93 | await api1.closeWebRtcTransport(transport1.id); 94 | await api2.closeWebRtcTransport(transport2.id); 95 | 96 | // Double invoke 97 | await api1.closeWebRtcTransport(transport1.id); 98 | await api2.closeWebRtcTransport(transport2.id); 99 | 100 | worker1.close(); 101 | worker2.close(); 102 | api1.close(); 103 | api2.close(); 104 | nc.close(); 105 | }); 106 | 107 | it('should create plain transport', async () => { 108 | jest.setTimeout(10000); 109 | const nc = await connect({ payload: Payload.JSON }); 110 | const connectionInfo: ConnectionInfo = { nc, rootTopic: randomKey() }; 111 | const worker1 = await createWorker({ connectionInfo }); 112 | const worker2 = await createWorker({ connectionInfo }); 113 | const api1 = new KitchenApi(worker1.id, connectionInfo); 114 | const api2 = new KitchenApi(worker2.id, connectionInfo); 115 | let router1 = (await api1.createRouter({ mediaCodecs: [] }, 'router1')).id; 116 | let router2 = (await api2.createRouter({ mediaCodecs: [] }, 'router2')).id; 117 | 118 | // Create transport 119 | let transport1 = await api1.createPlainTransport(router1, { enableSrtp: true }, 'transport1'); 120 | let transport2 = await api2.createPlainTransport(router2, { enableSrtp: true }, 'transport2'); 121 | 122 | // Double invoke 123 | let transport3 = await api1.createPlainTransport(router1, { enableSrtp: true }, 'transport1'); 124 | let transport4 = await api2.createPlainTransport(router2, { enableSrtp: true }, 'transport2'); 125 | expect(transport3.id).toBe(transport1.id); 126 | expect(transport4.id).toBe(transport2.id); 127 | 128 | // Connect 129 | await api1.connectPlainTransport({ id: transport1.id, ip: transport2.tuple.localIp, port: transport2.tuple.localPort, srtpParameters: transport2.srtpParameters! }); 130 | await api2.connectPlainTransport({ id: transport2.id, ip: transport1.tuple.localIp, port: transport1.tuple.localPort, srtpParameters: transport1.srtpParameters! }); 131 | 132 | // Double invoke 133 | await api1.connectPlainTransport({ id: transport1.id, ip: transport2.tuple.localIp, port: transport2.tuple.localPort }); 134 | await api2.connectPlainTransport({ id: transport2.id, ip: transport1.tuple.localIp, port: transport1.tuple.localPort }); 135 | 136 | // Close 137 | await api1.closePlainTransport(transport1.id); 138 | await api2.closePlainTransport(transport2.id); 139 | 140 | // Double invoke 141 | await api1.closePlainTransport(transport1.id); 142 | await api2.closePlainTransport(transport2.id); 143 | 144 | worker1.close(); 145 | worker2.close(); 146 | api1.close(); 147 | api2.close(); 148 | nc.close(); 149 | }); 150 | 151 | it('should create pipe transport', async () => { 152 | jest.setTimeout(10000); 153 | const nc = await connect({ payload: Payload.JSON }); 154 | const connectionInfo: ConnectionInfo = { nc, rootTopic: randomKey() }; 155 | const worker1 = await createWorker({ connectionInfo }); 156 | const worker2 = await createWorker({ connectionInfo }); 157 | const api1 = new KitchenApi(worker1.id, connectionInfo); 158 | const api2 = new KitchenApi(worker2.id, connectionInfo); 159 | let router1 = (await api1.createRouter({ mediaCodecs: [] }, 'router1')).id; 160 | let router2 = (await api2.createRouter({ mediaCodecs: [] }, 'router2')).id; 161 | 162 | // Create transport 163 | let transport1 = await api1.createPipeTransport(router1, { enableSrtp: true }, 'transport1'); 164 | let transport2 = await api2.createPipeTransport(router2, { enableSrtp: true }, 'transport2'); 165 | 166 | // Double invoke 167 | let transport3 = await api1.createPipeTransport(router1, { enableSrtp: true }, 'transport1'); 168 | let transport4 = await api2.createPipeTransport(router2, { enableSrtp: true }, 'transport2'); 169 | expect(transport3.id).toBe(transport1.id); 170 | expect(transport4.id).toBe(transport2.id); 171 | 172 | console.warn(transport1); 173 | console.warn(transport2); 174 | 175 | // Connect 176 | await api1.connectPipeTransport({ id: transport1.id, ip: transport2.tuple.localIp, port: transport2.tuple.localPort, srtpParameters: transport2.srtpParameters! }); 177 | await api2.connectPipeTransport({ id: transport2.id, ip: transport1.tuple.localIp, port: transport1.tuple.localPort, srtpParameters: transport1.srtpParameters! }); 178 | 179 | // Double invoke 180 | await api1.connectPipeTransport({ id: transport1.id, ip: transport2.tuple.localIp, port: transport2.tuple.localPort }); 181 | await api2.connectPipeTransport({ id: transport2.id, ip: transport1.tuple.localIp, port: transport1.tuple.localPort }); 182 | 183 | // Close 184 | await api1.closePipeTransport(transport1.id); 185 | await api2.closePipeTransport(transport2.id); 186 | 187 | // Double invoke 188 | await api1.closePipeTransport(transport1.id); 189 | await api2.closePipeTransport(transport2.id); 190 | 191 | worker1.close(); 192 | worker2.close(); 193 | api1.close(); 194 | api2.close(); 195 | nc.close(); 196 | }); 197 | }); -------------------------------------------------------------------------------- /packages/mediakitchen/src/model/KitchenApi.ts: -------------------------------------------------------------------------------- 1 | import { WebRtcTransportStats, PipeTransportStats, PlainTransportStats } from './../Stats'; 2 | import { ConnectionInfo } from '../ConnectionInfo'; 3 | import { 4 | Commands, 5 | CommandBox, 6 | routerCloseResponseCodec, 7 | RouterCreateCommand, 8 | routerCreateResponseCodec, 9 | WebRTCTransportCreateCommand, 10 | webRTCTransportCreateResponseCodec, 11 | webRtcTransportCloseResponseCodec, 12 | WebRTCTransportConnectCommand, 13 | webRtcTransportConnectResponseCodec, 14 | ProduceCommand, 15 | produceResponseCodec, 16 | produceCloseResponseCodec, 17 | ConsumeCommand, 18 | consumeResponseCodec, 19 | producePauseResponseCodec, 20 | produceResumeResponseCodec, 21 | consumeCloseResponseCodec, 22 | consumePauseResponseCodec, 23 | consumeResumeResponseCodec, 24 | getStateResponseCodec, 25 | getEventsResponseCodec, 26 | eventBoxCodec, 27 | Event, 28 | backoff, 29 | PlainTransportCreateCommand, 30 | plainTransportCreateResponseCodec, 31 | plainTransportCloseResponseCodec, 32 | PipeTransportCreateCommand, 33 | PlainTransportConnectCommand, 34 | webRtcTransportRestartResponseCodec, 35 | pipeTransportCreateResponseCodec, 36 | PipeTransportConnectCommand, 37 | pipeTransportConnectResponseCodec, 38 | pipeTransportCloseResponseCodec, 39 | statsResponseCodec 40 | } from 'mediakitchen-common'; 41 | import * as nats from 'ts-nats'; 42 | import * as t from 'io-ts'; 43 | import { ConsumerStats, ProducerStats } from '../Stats'; 44 | 45 | export class KitchenApi { 46 | #id: string; 47 | #seq!: number; 48 | #invalidating: boolean = false; 49 | #pending = new Map(); 50 | #closed = false; 51 | #rootTopic: string 52 | 53 | #subscription: nats.Subscription | null = null; 54 | #client: nats.Client 55 | 56 | onEvent?: (event: Event) => void; 57 | 58 | constructor(id: string, connectionInfo: ConnectionInfo) { 59 | this.#id = id; 60 | this.#client = connectionInfo.nc; 61 | this.#rootTopic = connectionInfo.rootTopic || 'mediakitchen'; 62 | this.#init(); 63 | } 64 | 65 | // Router 66 | 67 | createRouter = (command: RouterCreateCommand['args'], retryKey: string) => { 68 | return this.#doCommand({ type: 'router-create', args: command }, retryKey, routerCreateResponseCodec); 69 | } 70 | 71 | closeRouter = (id: string) => { 72 | return this.#doCommand({ type: 'router-close', args: { id } }, '', routerCloseResponseCodec); 73 | } 74 | 75 | // WebRTC Transport 76 | 77 | createWebRtcTransport = (routerId: string, command: WebRTCTransportCreateCommand['args'], retryKey: string) => { 78 | return this.#doCommand({ type: 'transport-webrtc-create', routerId, args: command }, retryKey, webRTCTransportCreateResponseCodec); 79 | } 80 | 81 | connectWebRtcTransport = (command: WebRTCTransportConnectCommand['args']) => { 82 | return this.#doCommand({ type: 'transport-webrtc-connect', args: command }, '', webRtcTransportConnectResponseCodec); 83 | } 84 | 85 | restartWebRtcTransport = (id: string) => { 86 | return this.#doCommand({ type: 'transport-webrtc-restart', args: { id } }, '', webRtcTransportRestartResponseCodec); 87 | } 88 | 89 | closeWebRtcTransport = (id: string) => { 90 | return this.#doCommand({ type: 'transport-webrtc-close', args: { id } }, '', webRtcTransportCloseResponseCodec); 91 | } 92 | 93 | getWebRtcTransportStats = (id: string) => { 94 | return this.#getStats(id); 95 | } 96 | 97 | // Plain Transport 98 | 99 | createPlainTransport = (routerId: string, command: PlainTransportCreateCommand['args'], retryKey: string) => { 100 | return this.#doCommand({ type: 'transport-plain-create', routerId, args: command }, retryKey, plainTransportCreateResponseCodec); 101 | } 102 | 103 | connectPlainTransport = (command: PlainTransportConnectCommand['args']) => { 104 | return this.#doCommand({ type: 'transport-plain-connect', args: command }, '', plainTransportCreateResponseCodec); 105 | } 106 | 107 | closePlainTransport = (id: string) => { 108 | return this.#doCommand({ type: 'transport-plain-close', args: { id } }, '', plainTransportCloseResponseCodec); 109 | } 110 | 111 | getPlainTransportStats = (id: string) => { 112 | return this.#getStats(id); 113 | } 114 | 115 | // Pipe Transport 116 | 117 | createPipeTransport = (routerId: string, command: PipeTransportCreateCommand['args'], retryKey: string) => { 118 | return this.#doCommand({ type: 'transport-pipe-create', routerId, args: command }, retryKey, pipeTransportCreateResponseCodec); 119 | } 120 | 121 | connectPipeTransport = (command: PipeTransportConnectCommand['args']) => { 122 | return this.#doCommand({ type: 'transport-pipe-connect', args: command }, '', pipeTransportConnectResponseCodec); 123 | } 124 | 125 | closePipeTransport = (id: string) => { 126 | return this.#doCommand({ type: 'transport-pipe-close', args: { id } }, '', pipeTransportCloseResponseCodec); 127 | } 128 | 129 | getPipeTransportStats = (id: string) => { 130 | return this.#getStats(id); 131 | } 132 | 133 | // Producer 134 | 135 | createProducer = (transportId: string, command: ProduceCommand['args'], retryKey: string) => { 136 | return this.#doCommand({ type: 'produce-create', transportId, args: command }, retryKey, produceResponseCodec) 137 | } 138 | 139 | pauseProducer = (producerId: string) => { 140 | return this.#doCommand({ type: 'produce-pause', args: { id: producerId } }, '', producePauseResponseCodec); 141 | } 142 | 143 | resumeProducer = (producerId: string) => { 144 | return this.#doCommand({ type: 'produce-resume', args: { id: producerId } }, '', produceResumeResponseCodec); 145 | } 146 | 147 | closeProducer = (producerId: string) => { 148 | return this.#doCommand({ type: 'produce-close', args: { id: producerId } }, '', produceCloseResponseCodec); 149 | } 150 | 151 | getProducerStats = (id: string) => { 152 | return this.#getStats(id); 153 | } 154 | 155 | // Consumer 156 | 157 | createConsumer = (transportId: string, producerId: string, command: ConsumeCommand['args'], retryKey: string) => { 158 | return this.#doCommand({ type: 'consume-create', transportId, producerId, args: command }, retryKey, consumeResponseCodec); 159 | } 160 | 161 | pauseConsumer = (consumerId: string) => { 162 | return this.#doCommand({ type: 'consume-pause', args: { id: consumerId } }, '', consumePauseResponseCodec); 163 | } 164 | 165 | resumeConsumer = (consumerId: string) => { 166 | return this.#doCommand({ type: 'consume-resume', args: { id: consumerId } }, '', consumeResumeResponseCodec); 167 | } 168 | 169 | closeConsumer = (consumerId: string) => { 170 | return this.#doCommand({ type: 'consume-close', args: { id: consumerId } }, '', consumeCloseResponseCodec); 171 | } 172 | 173 | getConsumerStats = (id: string) => { 174 | return this.#getStats(id); 175 | } 176 | 177 | // Worker 178 | 179 | killWorker = async () => { 180 | await this.#doCommand({ type: 'worker-kill' }, '', t.type({})); 181 | } 182 | 183 | getState = async () => { 184 | return await this.#doCommand({ type: 'worker-state' }, '', getStateResponseCodec); 185 | } 186 | 187 | getEvents = async (seq: number) => { 188 | return await this.#doCommand({ type: 'worker-events', seq, batchSize: 500 }, '', getEventsResponseCodec); 189 | } 190 | 191 | // 192 | // Close 193 | // 194 | 195 | close() { 196 | if (!this.#closed) { 197 | this.#closed = true; 198 | if (this.#subscription) { 199 | this.#subscription.unsubscribe(); 200 | this.#subscription = null; 201 | } 202 | } 203 | } 204 | 205 | // 206 | // Implementation 207 | // 208 | 209 | #init = async () => { 210 | let subscription = await this.#client.subscribe(this.#rootTopic + '.' + this.#id + '.events', (e, msg) => { 211 | if (this.#closed) { 212 | return; 213 | } 214 | 215 | let box = msg.data; 216 | if (!box) { 217 | return; 218 | } 219 | if (!eventBoxCodec.is(box)) { 220 | return; 221 | } 222 | this.#onEvent(box.seq, box.event); 223 | }); 224 | if (this.#closed) { 225 | subscription.unsubscribe(); 226 | return; 227 | } 228 | this.#subscription = subscription; 229 | 230 | let state = await backoff(async () => { 231 | if (this.#closed) { 232 | return null; 233 | } 234 | let r = await this.getState(); 235 | if (this.#closed) { 236 | return null; 237 | } 238 | return r; 239 | }); 240 | if (!state) { 241 | return; 242 | } 243 | if (this.#closed) { 244 | return; 245 | } 246 | this.#seq = state.seq; 247 | this.#flushEvents(); 248 | } 249 | 250 | #flushEvents = () => { 251 | // Remove Old 252 | let toRemove: number[] = []; 253 | for (let k of this.#pending.keys()) { 254 | if (k <= this.#seq) { 255 | toRemove.push(k); 256 | } 257 | } 258 | for (let k of toRemove) { 259 | this.#pending.delete(k); 260 | } 261 | 262 | // Flush next 263 | while (this.#pending.size > 0) { 264 | let ev = this.#pending.get(this.#seq + 1); 265 | if (ev) { 266 | this.#seq++; 267 | this.#pending.delete(this.#seq); 268 | if (this.onEvent) { 269 | this.onEvent(ev); 270 | } 271 | } else { 272 | return; 273 | } 274 | } 275 | } 276 | 277 | #onEvent = (seq: number, event: Event) => { 278 | if (this.#seq === undefined) { 279 | this.#pending.set(seq, event); 280 | } else { 281 | if (seq === this.#seq + 1) { 282 | this.#seq = seq; 283 | if (this.onEvent) { 284 | this.onEvent(event); 285 | } 286 | this.#flushEvents(); 287 | } else if (seq <= this.#seq) { 288 | return; // Ignore 289 | } else { 290 | this.#pending.set(seq, event); 291 | this.#doInvalidateIfNeeded(); 292 | } 293 | } 294 | } 295 | 296 | #doInvalidateIfNeeded = () => { 297 | if (!this.#invalidating) { 298 | this.#invalidating = true; 299 | backoff(async () => { 300 | while (true) { 301 | if (this.#closed) { 302 | return; 303 | } 304 | let s = this.#seq; 305 | let response = (await this.getEvents(s)); 306 | if (this.#closed) { 307 | return; 308 | } 309 | s++; 310 | for (let e of response.events) { 311 | this.#pending.set(s, e); 312 | s++; 313 | } 314 | this.#flushEvents(); 315 | if (!response.hasMore) { 316 | return; 317 | } 318 | } 319 | }); 320 | } 321 | }; 322 | 323 | #doCommand = async(command: Commands, repeatKey: string, responseCodec: t.Type): Promise => { 324 | let box: CommandBox = { 325 | command, 326 | repeatKey, 327 | time: Date.now() 328 | }; 329 | let res = await this.#client.request(this.#rootTopic + '.' + this.#id + '.commands', 5000, box); 330 | if (!res.data) { 331 | throw Error('Unknown error'); 332 | } 333 | if (res.data.response === 'success') { 334 | let response = res.data.data; 335 | if (responseCodec.is(response)) { 336 | return response; 337 | } 338 | } else if (res.data.response === 'error') { 339 | let message = res.data.message; 340 | if (typeof message === 'string') { 341 | throw Error(message); 342 | } 343 | } 344 | throw Error('Unknown error'); 345 | } 346 | 347 | #getStats = async (id: string) => { 348 | let res = await this.#doCommand({ type: 'get-stats', args: { id } }, '', statsResponseCodec); 349 | if (res.data) { 350 | return JSON.parse(res.data) as T; 351 | } else { 352 | return null; 353 | } 354 | } 355 | } -------------------------------------------------------------------------------- /packages/mediakitchen-common/src/wire/commands.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { eventsCodec } from './events'; 3 | import { simpleMapCodec, dtlsParametersCodec, rtpParametersCodec, rtpCodecCapabilityCodec, rtpCapabilitiesCodec, numSctpStreamsCodec, srtpParametersCodec } from './common'; 4 | import { routerStateCodec, RouterState, webRtcTransportStateCodec, producerStateCodec, consumerStateCodec, plainTransportStateCodec, pipeTransportStateCodec } from './states'; 5 | 6 | // 7 | // Kill Worker 8 | // 9 | 10 | const killCommandCodec = t.type({ 11 | type: t.literal('worker-kill') 12 | }); 13 | 14 | // 15 | // Get Events 16 | // 17 | 18 | const getEventsCommandCodec = t.type({ 19 | type: t.literal('worker-events'), 20 | batchSize: t.number, 21 | seq: t.number 22 | }); 23 | export type GetEventsCommand = t.TypeOf; 24 | 25 | export const getEventsResponseCodec = t.type({ 26 | hasMore: t.boolean, 27 | seq: t.number, 28 | events: t.array(eventsCodec) 29 | }); 30 | export type GetEventsResponse = t.TypeOf; 31 | 32 | // 33 | // Get State 34 | // 35 | 36 | const getStateCommandCodec = t.type({ 37 | type: t.literal('worker-state') 38 | }); 39 | export type GetStateCommand = t.TypeOf; 40 | 41 | export const getStateResponseCodec = t.type({ 42 | seq: t.number 43 | }); 44 | export type GetStateResponse = t.TypeOf; 45 | 46 | // 47 | // Create Router Command 48 | // 49 | 50 | const routerCreateCommandCodec = t.type({ 51 | type: t.literal('router-create'), 52 | args: t.intersection([t.type({ 53 | mediaCodecs: t.array(rtpCodecCapabilityCodec), 54 | }), t.partial({ 55 | appData: simpleMapCodec 56 | })]) 57 | }); 58 | export type RouterCreateCommand = t.TypeOf; 59 | 60 | export const routerCreateResponseCodec = routerStateCodec; 61 | export type RouterCreateResponse = RouterState; 62 | 63 | // 64 | // Close Router Command 65 | // 66 | 67 | const routerCloseCommandCodec = t.type({ 68 | type: t.literal('router-close'), 69 | args: t.type({ 70 | id: t.string 71 | }) 72 | }); 73 | export type RouterCloseCommand = t.TypeOf; 74 | 75 | export const routerCloseResponseCodec = routerStateCodec; 76 | export type RouterCloseResponse = RouterState; 77 | 78 | // 79 | // Create WebRTC Transport Command 80 | // 81 | 82 | const webRtcTransportCreateCommandCodec = t.type({ 83 | type: t.literal('transport-webrtc-create'), 84 | routerId: t.string, 85 | args: t.partial({ 86 | enableUdp: t.boolean, 87 | enableTcp: t.boolean, 88 | preferUdp: t.boolean, 89 | preferTcp: t.boolean, 90 | initialAvailableOutgoingBitrate: t.number, 91 | enableSctp: t.boolean, 92 | numSctpStreams: numSctpStreamsCodec, 93 | maxSctpMessageSize: t.number, 94 | sctpSendBufferSize: t.number, 95 | appData: simpleMapCodec 96 | }) 97 | }); 98 | export type WebRTCTransportCreateCommand = t.TypeOf; 99 | 100 | export const webRTCTransportCreateResponseCodec = webRtcTransportStateCodec; 101 | export type WebRTCTransportCreateResponse = t.TypeOf; 102 | 103 | // 104 | // Close WebRTC Transport Command 105 | // 106 | 107 | const webRtcTransportCloseCommandCodec = t.type({ 108 | type: t.literal('transport-webrtc-close'), 109 | args: t.type({ 110 | id: t.string 111 | }) 112 | }); 113 | export type WebRTCTransportCloseCommand = t.TypeOf; 114 | 115 | export const webRtcTransportCloseResponseCodec = webRtcTransportStateCodec; 116 | export type WebRTCTransportCloseResponse = t.TypeOf; 117 | 118 | // 119 | // Restart WebRTC Transport Command 120 | // 121 | 122 | const webRtcTransportRestartCommandCodec = t.type({ 123 | type: t.literal('transport-webrtc-restart'), 124 | args: t.type({ 125 | id: t.string 126 | }) 127 | }); 128 | export type WebRTCTransportRestartCommand = t.TypeOf; 129 | 130 | export const webRtcTransportRestartResponseCodec = webRtcTransportStateCodec; 131 | export type WebRTCTransportRestartResponse = t.TypeOf; 132 | 133 | // 134 | // Connect WebRTC Transport Command 135 | // 136 | 137 | const webRtcTransportConnectCommandCodec = t.type({ 138 | type: t.literal('transport-webrtc-connect'), 139 | args: t.type({ 140 | id: t.string, 141 | dtlsParameters: dtlsParametersCodec, 142 | }) 143 | }); 144 | export type WebRTCTransportConnectCommand = t.TypeOf; 145 | 146 | export const webRtcTransportConnectResponseCodec = webRtcTransportStateCodec; 147 | export type WebRTCTransportConnectResponse = t.TypeOf; 148 | 149 | // 150 | // Create Plain Transport Command 151 | // 152 | 153 | const plainTransportCreateCommandCodec = t.type({ 154 | type: t.literal('transport-plain-create'), 155 | routerId: t.string, 156 | args: t.partial({ 157 | rtcpMux: t.boolean, 158 | comedia: t.boolean, 159 | enableSctp: t.boolean, 160 | numSctpStreams: numSctpStreamsCodec, 161 | maxSctpMessageSize: t.number, 162 | sctpSendBufferSize: t.number, 163 | enableSrtp: t.boolean, 164 | srtpCryptoSuite: t.union([t.literal('AES_CM_128_HMAC_SHA1_80'), t.literal('AES_CM_128_HMAC_SHA1_32')]), 165 | appData: simpleMapCodec 166 | }) 167 | }); 168 | export type PlainTransportCreateCommand = t.TypeOf; 169 | 170 | export const plainTransportCreateResponseCodec = plainTransportStateCodec; 171 | export type PlainTransportCreateResponse = t.TypeOf; 172 | 173 | // 174 | // Connect Plain Transport Command 175 | // 176 | 177 | const plainTransportConnectCommandCodec = t.type({ 178 | type: t.literal('transport-plain-connect'), 179 | args: t.intersection([t.type({ 180 | id: t.string, 181 | }), t.partial({ 182 | ip: t.string, 183 | port: t.number, 184 | rtcpPort: t.number, 185 | srtpParameters: srtpParametersCodec 186 | })]) 187 | }); 188 | export type PlainTransportConnectCommand = t.TypeOf; 189 | 190 | export const plainTransportConnectResponseCodec = plainTransportStateCodec; 191 | export type PlainTransportConnectResponse = t.TypeOf; 192 | 193 | // 194 | // Close Plain Transport Command 195 | // 196 | 197 | const plainTransportCloseCommandCodec = t.type({ 198 | type: t.literal('transport-plain-close'), 199 | args: t.type({ 200 | id: t.string, 201 | }) 202 | }); 203 | export type PlainTransportCloseCommand = t.TypeOf; 204 | 205 | export const plainTransportCloseResponseCodec = plainTransportStateCodec; 206 | export type PlainTransportCloseResponse = t.TypeOf; 207 | 208 | // 209 | // Create Pipe Transport Command 210 | // 211 | 212 | const pipeTransportCreateCommandCodec = t.type({ 213 | type: t.literal('transport-pipe-create'), 214 | routerId: t.string, 215 | args: t.partial({ 216 | enableSctp: t.boolean, 217 | numSctpStreams: numSctpStreamsCodec, 218 | maxSctpMessageSize: t.number, 219 | sctpSendBufferSize: t.number, 220 | enableRtx: t.boolean, 221 | enableSrtp: t.boolean, 222 | appData: simpleMapCodec 223 | }) 224 | }); 225 | export type PipeTransportCreateCommand = t.TypeOf; 226 | 227 | export const pipeTransportCreateResponseCodec = pipeTransportStateCodec; 228 | export type PipeTransportCreateResponse = t.TypeOf; 229 | 230 | // 231 | // Close Pipe Transport Command 232 | // 233 | 234 | const pipeTransportCloseCommandCodec = t.type({ 235 | type: t.literal('transport-pipe-close'), 236 | args: t.type({ 237 | id: t.string, 238 | }) 239 | }); 240 | export type PipeTransportCloseCommand = t.TypeOf; 241 | 242 | export const pipeTransportCloseResponseCodec = pipeTransportStateCodec; 243 | export type PipeTransportCloseResponse = t.TypeOf; 244 | 245 | // 246 | // Connect Pipe Transport Command 247 | // 248 | 249 | const pipeTransportConnectCommandCodec = t.type({ 250 | type: t.literal('transport-pipe-connect'), 251 | args: t.intersection([t.type({ 252 | id: t.string, 253 | ip: t.string, 254 | port: t.number, 255 | }), t.partial({ 256 | srtpParameters: srtpParametersCodec 257 | })]) 258 | }); 259 | export type PipeTransportConnectCommand = t.TypeOf; 260 | 261 | export const pipeTransportConnectResponseCodec = pipeTransportStateCodec; 262 | export type PipeTransportConnectResponse = t.TypeOf; 263 | 264 | // 265 | // Produce Create Command 266 | // 267 | 268 | const produceCommandCodec = t.type({ 269 | type: t.literal('produce-create'), 270 | transportId: t.string, 271 | args: t.intersection([t.type({ 272 | kind: t.union([t.literal('audio'), t.literal('video')]), 273 | rtpParameters: rtpParametersCodec, 274 | }), t.partial({ 275 | id: t.string, 276 | paused: t.boolean, 277 | keyFrameRequestDelay: t.number, 278 | appData: simpleMapCodec 279 | })]) 280 | }); 281 | export type ProduceCommand = t.TypeOf; 282 | 283 | export const produceResponseCodec = producerStateCodec; 284 | export type ProduceResponse = t.TypeOf; 285 | 286 | // 287 | // Produce Pause Command 288 | // 289 | 290 | const producePauseCommandCodec = t.type({ 291 | type: t.literal('produce-pause'), 292 | args: t.type({ 293 | id: t.string 294 | }) 295 | }); 296 | export type ProducePauseCommand = t.TypeOf; 297 | export const producePauseResponseCodec = producerStateCodec; 298 | export type ProducePauseResponse = t.TypeOf; 299 | 300 | // 301 | // Produce Resume Command 302 | // 303 | 304 | const produceResumeCommandCodec = t.type({ 305 | type: t.literal('produce-resume'), 306 | args: t.type({ 307 | id: t.string 308 | }) 309 | }); 310 | export type ProduceResumeCommand = t.TypeOf; 311 | export const produceResumeResponseCodec = producerStateCodec; 312 | export type ProduceResumeResponse = t.TypeOf; 313 | 314 | // 315 | // Produce Close Commnand 316 | // 317 | 318 | const produceCloseCommandCodec = t.type({ 319 | type: t.literal('produce-close'), 320 | args: t.type({ 321 | id: t.string 322 | }) 323 | }); 324 | export type ProduceCloseCommand = t.TypeOf; 325 | 326 | export const produceCloseResponseCodec = producerStateCodec; 327 | export type ProduceCloseResponse = t.TypeOf; 328 | 329 | // 330 | // Consume Command 331 | // 332 | 333 | const consumeCommandCodec = t.type({ 334 | type: t.literal('consume-create'), 335 | transportId: t.string, 336 | producerId: t.string, 337 | args: t.partial({ 338 | rtpCapabilities: rtpCapabilitiesCodec, 339 | paused: t.boolean, 340 | appData: simpleMapCodec, 341 | preferredLayers: t.intersection([ 342 | t.type({ spatialLayer: t.number }), 343 | t.partial({ temporalLayer: t.number }) 344 | ]) 345 | }) 346 | }); 347 | export type ConsumeCommand = t.TypeOf; 348 | 349 | export const consumeResponseCodec = consumerStateCodec; 350 | export type ConsumeResponse = t.TypeOf; 351 | 352 | // 353 | // Consume Pause 354 | // 355 | 356 | const consumePauseCommandCodec = t.type({ 357 | type: t.literal('consume-pause'), 358 | args: t.type({ 359 | id: t.string 360 | }) 361 | }); 362 | export type ConsumePauseCommand = t.TypeOf; 363 | 364 | export const consumePauseResponseCodec = consumerStateCodec; 365 | export type ConsumePauseResponse = t.TypeOf; 366 | 367 | // 368 | // Consume Resume 369 | // 370 | 371 | const consumeResumeCommandCodec = t.type({ 372 | type: t.literal('consume-resume'), 373 | args: t.type({ 374 | id: t.string 375 | }) 376 | }); 377 | export type ConsumeResumeCommand = t.TypeOf; 378 | 379 | export const consumeResumeResponseCodec = consumerStateCodec; 380 | export type ConsumeResumeResponse = t.TypeOf; 381 | 382 | // 383 | // Consume Close 384 | // 385 | 386 | const consumeCloseCommandCodec = t.type({ 387 | type: t.literal('consume-close'), 388 | args: t.type({ 389 | id: t.string 390 | }) 391 | }); 392 | export type ConsumeCloseCommand = t.TypeOf; 393 | 394 | export const consumeCloseResponseCodec = consumerStateCodec; 395 | export type ConsumeCloseResponse = t.TypeOf; 396 | 397 | // 398 | // Stats 399 | // 400 | 401 | // 402 | // Produce Create Command 403 | // 404 | 405 | const statsCommandCodec = t.type({ 406 | type: t.literal('get-stats'), 407 | args: t.type({ 408 | id: t.string, 409 | }) 410 | }); 411 | export type StatsCommand = t.TypeOf; 412 | 413 | export const statsResponseCodec = t.type({ 414 | data: t.union([t.string, t.null]) 415 | });; 416 | export type StatsResponse = t.TypeOf; 417 | 418 | // 419 | // All Commands 420 | // 421 | 422 | export const commandsCodec = t.union([ 423 | killCommandCodec, 424 | getEventsCommandCodec, 425 | getStateCommandCodec, 426 | 427 | routerCreateCommandCodec, 428 | routerCloseCommandCodec, 429 | 430 | webRtcTransportCreateCommandCodec, 431 | webRtcTransportConnectCommandCodec, 432 | webRtcTransportCloseCommandCodec, 433 | webRtcTransportRestartCommandCodec, 434 | 435 | plainTransportCreateCommandCodec, 436 | plainTransportCloseCommandCodec, 437 | plainTransportConnectCommandCodec, 438 | 439 | pipeTransportCreateCommandCodec, 440 | pipeTransportCloseCommandCodec, 441 | pipeTransportConnectCommandCodec, 442 | 443 | produceCommandCodec, 444 | producePauseCommandCodec, 445 | produceResumeCommandCodec, 446 | produceCloseCommandCodec, 447 | 448 | consumeCommandCodec, 449 | consumePauseCommandCodec, 450 | consumeResumeCommandCodec, 451 | consumeCloseCommandCodec, 452 | 453 | statsCommandCodec 454 | ]); 455 | 456 | export type Commands = t.TypeOf; 457 | 458 | // 459 | // Command Box 460 | // 461 | 462 | export const commandBoxCodec = t.type({ 463 | command: commandsCodec, 464 | repeatKey: t.string, 465 | time: t.number 466 | }); 467 | 468 | export type CommandBox = t.TypeOf; -------------------------------------------------------------------------------- /yarn-error.log: -------------------------------------------------------------------------------- 1 | Arguments: 2 | /usr/local/Cellar/node/13.10.1/bin/node /usr/local/Cellar/yarn/1.19.1/libexec/bin/yarn.js add ts-io 3 | 4 | PATH: 5 | /Users/steve/Library/Android/sdk/platform-tools/:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin:/Library/Apple/usr/bin:/Users/steve/Library/Android/sdk/platform-tools/ 6 | 7 | Yarn version: 8 | 1.19.1 9 | 10 | Node version: 11 | 13.10.1 12 | 13 | Platform: 14 | darwin x64 15 | 16 | Trace: 17 | Error: https://registry.yarnpkg.com/ts-io: Not found 18 | at Request.params.callback [as _callback] (/usr/local/Cellar/yarn/1.19.1/libexec/lib/cli.js:66926:18) 19 | at Request.self.callback (/usr/local/Cellar/yarn/1.19.1/libexec/lib/cli.js:140564:22) 20 | at Request.emit (events.js:316:20) 21 | at Request. (/usr/local/Cellar/yarn/1.19.1/libexec/lib/cli.js:141536:10) 22 | at Request.emit (events.js:316:20) 23 | at IncomingMessage. (/usr/local/Cellar/yarn/1.19.1/libexec/lib/cli.js:141458:12) 24 | at Object.onceWrapper (events.js:422:28) 25 | at IncomingMessage.emit (events.js:328:22) 26 | at endReadableNT (_stream_readable.js:1201:12) 27 | at processTicksAndRejections (internal/process/task_queues.js:84:21) 28 | 29 | npm manifest: 30 | { 31 | "name": "@openland/video", 32 | "version": "1.0.0", 33 | "description": "Openland Video Server", 34 | "main": "dist/index.js", 35 | "repository": "https://github.com/openland/openland-video.git", 36 | "author": "Steve Korshakov ", 37 | "license": "MIT", 38 | "scripts": { 39 | "build": "tsc", 40 | "start": "tsc && DEBUG=* node ./dist/test.js" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "^13.9.8", 44 | "@types/pino": "^5.17.0", 45 | "typescript": "^3.8.3", 46 | "mediasoup": "^3.5.5" 47 | }, 48 | "peerDependencies": { 49 | "mediasoup": "*" 50 | }, 51 | "dependencies": { 52 | "ts-nats": "^1.2.12", 53 | "debug": "^4.1.1" 54 | } 55 | } 56 | 57 | yarn manifest: 58 | No manifest 59 | 60 | Lockfile: 61 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 62 | # yarn lockfile v1 63 | 64 | 65 | "@types/debug@^4.1.5": 66 | version "4.1.5" 67 | resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" 68 | integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== 69 | 70 | "@types/node@*", "@types/node@^13.9.8": 71 | version "13.11.0" 72 | resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b" 73 | integrity sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ== 74 | 75 | "@types/pino-std-serializers@*": 76 | version "2.4.0" 77 | resolved "https://registry.yarnpkg.com/@types/pino-std-serializers/-/pino-std-serializers-2.4.0.tgz#8cad99175cb79c2448f7455a2d32fb3fde29579c" 78 | integrity sha512-eAdu+NW1IkCdmp85SnhyKha+OOREQMT9lXaoICQxa7bhSauRiLzu3WSNt9Mf2piuJvWeXF/G0hGWHr63xNpIRA== 79 | 80 | "@types/pino@^5.17.0": 81 | version "5.17.0" 82 | resolved "https://registry.yarnpkg.com/@types/pino/-/pino-5.17.0.tgz#850cd1d7c5a0e123f022badc2e2bb42d8d0efd9d" 83 | integrity sha512-L5DBGFBRY4DKc7ufjZqV4J61ji9FSn4zKvQ5CUBbWi0gE0uOSNwDBpj1t7VwRwdmrpG3QlFxAeXgpViGUOS5Bg== 84 | dependencies: 85 | "@types/node" "*" 86 | "@types/pino-std-serializers" "*" 87 | "@types/sonic-boom" "*" 88 | 89 | "@types/sonic-boom@*": 90 | version "0.7.0" 91 | resolved "https://registry.yarnpkg.com/@types/sonic-boom/-/sonic-boom-0.7.0.tgz#38337036293992a1df65dd3161abddf8fb9b7176" 92 | integrity sha512-AfqR0fZMoUXUNwusgXKxcE9DPlHNDHQp6nKYUd4PSRpLobF5CCevSpyTEBcVZreqaWKCnGBr9KI1fHMTttoB7A== 93 | dependencies: 94 | "@types/node" "*" 95 | 96 | ajv@^6.5.5: 97 | version "6.12.0" 98 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" 99 | integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw== 100 | dependencies: 101 | fast-deep-equal "^3.1.1" 102 | fast-json-stable-stringify "^2.0.0" 103 | json-schema-traverse "^0.4.1" 104 | uri-js "^4.2.2" 105 | 106 | array-find-index@^1.0.1: 107 | version "1.0.2" 108 | resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" 109 | integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= 110 | 111 | asn1@~0.2.3: 112 | version "0.2.4" 113 | resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" 114 | integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== 115 | dependencies: 116 | safer-buffer "~2.1.0" 117 | 118 | assert-plus@1.0.0, assert-plus@^1.0.0: 119 | version "1.0.0" 120 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 121 | integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= 122 | 123 | asynckit@^0.4.0: 124 | version "0.4.0" 125 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 126 | integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= 127 | 128 | awaitqueue@^2.1.1: 129 | version "2.1.1" 130 | resolved "https://registry.yarnpkg.com/awaitqueue/-/awaitqueue-2.1.1.tgz#2d5a801cc8952702ad9374e5c193ed096c8c1f53" 131 | integrity sha512-pzmngNP5W9S0dg24oim4wEQRCBadowAxVv5aLIJmgI5a8m8Cxxm1jXtScgHzHFY1aDR5ep1rktPtRbUEuIPl0A== 132 | 133 | aws-sign2@~0.7.0: 134 | version "0.7.0" 135 | resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" 136 | integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= 137 | 138 | aws4@^1.8.0: 139 | version "1.9.1" 140 | resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" 141 | integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== 142 | 143 | bcrypt-pbkdf@^1.0.0: 144 | version "1.0.2" 145 | resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" 146 | integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= 147 | dependencies: 148 | tweetnacl "^0.14.3" 149 | 150 | camelcase-keys@^2.0.0: 151 | version "2.1.0" 152 | resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" 153 | integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= 154 | dependencies: 155 | camelcase "^2.0.0" 156 | map-obj "^1.0.0" 157 | 158 | camelcase@^2.0.0: 159 | version "2.1.1" 160 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" 161 | integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= 162 | 163 | caseless@~0.12.0: 164 | version "0.12.0" 165 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 166 | integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= 167 | 168 | clang-tools-prebuilt@^0.1.4: 169 | version "0.1.4" 170 | resolved "https://registry.yarnpkg.com/clang-tools-prebuilt/-/clang-tools-prebuilt-0.1.4.tgz#f2020d36537608c0cfad07aebe094d99730590b3" 171 | integrity sha1-8gINNlN2CMDPrQeuvglNmXMFkLM= 172 | dependencies: 173 | home-path "^0.1.1" 174 | mkdirp "^0.5.0" 175 | nugget "^1.5.1" 176 | path-exists "^1.0.0" 177 | 178 | combined-stream@^1.0.6, combined-stream@~1.0.6: 179 | version "1.0.8" 180 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 181 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 182 | dependencies: 183 | delayed-stream "~1.0.0" 184 | 185 | core-util-is@1.0.2, core-util-is@~1.0.0: 186 | version "1.0.2" 187 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 188 | integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= 189 | 190 | currently-unhandled@^0.4.1: 191 | version "0.4.1" 192 | resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" 193 | integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= 194 | dependencies: 195 | array-find-index "^1.0.1" 196 | 197 | dashdash@^1.12.0: 198 | version "1.14.1" 199 | resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" 200 | integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= 201 | dependencies: 202 | assert-plus "^1.0.0" 203 | 204 | debug@^2.1.3: 205 | version "2.6.9" 206 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 207 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 208 | dependencies: 209 | ms "2.0.0" 210 | 211 | debug@^4.1.1: 212 | version "4.1.1" 213 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" 214 | integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== 215 | dependencies: 216 | ms "^2.1.1" 217 | 218 | decamelize@^1.1.2: 219 | version "1.2.0" 220 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 221 | integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= 222 | 223 | delayed-stream@~1.0.0: 224 | version "1.0.0" 225 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 226 | integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= 227 | 228 | ecc-jsbn@~0.1.1: 229 | version "0.1.2" 230 | resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" 231 | integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= 232 | dependencies: 233 | jsbn "~0.1.0" 234 | safer-buffer "^2.1.0" 235 | 236 | error-ex@^1.2.0: 237 | version "1.3.2" 238 | resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" 239 | integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== 240 | dependencies: 241 | is-arrayish "^0.2.1" 242 | 243 | extend@~3.0.2: 244 | version "3.0.2" 245 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 246 | integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== 247 | 248 | extsprintf@1.3.0: 249 | version "1.3.0" 250 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" 251 | integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= 252 | 253 | extsprintf@^1.2.0: 254 | version "1.4.0" 255 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" 256 | integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= 257 | 258 | fast-deep-equal@^3.1.1: 259 | version "3.1.1" 260 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" 261 | integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== 262 | 263 | fast-json-stable-stringify@^2.0.0: 264 | version "2.1.0" 265 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" 266 | integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== 267 | 268 | find-up@^1.0.0: 269 | version "1.1.2" 270 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" 271 | integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= 272 | dependencies: 273 | path-exists "^2.0.0" 274 | pinkie-promise "^2.0.0" 275 | 276 | forever-agent@~0.6.1: 277 | version "0.6.1" 278 | resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" 279 | integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= 280 | 281 | form-data@~2.3.2: 282 | version "2.3.3" 283 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" 284 | integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== 285 | dependencies: 286 | asynckit "^0.4.0" 287 | combined-stream "^1.0.6" 288 | mime-types "^2.1.12" 289 | 290 | get-stdin@^4.0.1: 291 | version "4.0.1" 292 | resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" 293 | integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= 294 | 295 | getpass@^0.1.1: 296 | version "0.1.7" 297 | resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" 298 | integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= 299 | dependencies: 300 | assert-plus "^1.0.0" 301 | 302 | graceful-fs@^4.1.2: 303 | version "4.2.3" 304 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" 305 | integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== 306 | 307 | h264-profile-level-id@^1.0.1: 308 | version "1.0.1" 309 | resolved "https://registry.yarnpkg.com/h264-profile-level-id/-/h264-profile-level-id-1.0.1.tgz#92033c190766c846e57c6a97e4c1d922943a9cce" 310 | integrity sha512-D3Rln/jKNjKDW5ZTJTK3niSoOGE+pFqPvRHHVgQN3G7umcn/zWGPUo8Q8VpDj16x3hKz++zVviRNRmXu5cpN+Q== 311 | dependencies: 312 | debug "^4.1.1" 313 | 314 | har-schema@^2.0.0: 315 | version "2.0.0" 316 | resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" 317 | integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= 318 | 319 | har-validator@~5.1.3: 320 | version "5.1.3" 321 | resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" 322 | integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== 323 | dependencies: 324 | ajv "^6.5.5" 325 | har-schema "^2.0.0" 326 | 327 | has-flag@^4.0.0: 328 | version "4.0.0" 329 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 330 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 331 | 332 | home-path@^0.1.1: 333 | version "0.1.2" 334 | resolved "https://registry.yarnpkg.com/home-path/-/home-path-0.1.2.tgz#3db26ca23adc144feea8f1e2d7c8f6c890cbfe2a" 335 | integrity sha1-PbJsojrcFE/uqPHi18j2yJDL/io= 336 | 337 | hosted-git-info@^2.1.4: 338 | version "2.8.8" 339 | resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" 340 | integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== 341 | 342 | http-signature@~1.2.0: 343 | version "1.2.0" 344 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" 345 | integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= 346 | dependencies: 347 | assert-plus "^1.0.0" 348 | jsprim "^1.2.2" 349 | sshpk "^1.7.0" 350 | 351 | indent-string@^2.1.0: 352 | version "2.1.0" 353 | resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" 354 | integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= 355 | dependencies: 356 | repeating "^2.0.0" 357 | 358 | inherits@~2.0.1: 359 | version "2.0.4" 360 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 361 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 362 | 363 | is-arrayish@^0.2.1: 364 | version "0.2.1" 365 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" 366 | integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= 367 | 368 | is-finite@^1.0.0: 369 | version "1.1.0" 370 | resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" 371 | integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== 372 | 373 | is-typedarray@~1.0.0: 374 | version "1.0.0" 375 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 376 | integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= 377 | 378 | is-utf8@^0.2.0: 379 | version "0.2.1" 380 | resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" 381 | integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= 382 | 383 | isarray@0.0.1: 384 | version "0.0.1" 385 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" 386 | integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= 387 | 388 | isstream@~0.1.2: 389 | version "0.1.2" 390 | resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 391 | integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= 392 | 393 | jsbn@~0.1.0: 394 | version "0.1.1" 395 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" 396 | integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= 397 | 398 | json-schema-traverse@^0.4.1: 399 | version "0.4.1" 400 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 401 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 402 | 403 | json-schema@0.2.3: 404 | version "0.2.3" 405 | resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" 406 | integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= 407 | 408 | json-stringify-safe@~5.0.1: 409 | version "5.0.1" 410 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 411 | integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= 412 | 413 | jsprim@^1.2.2: 414 | version "1.4.1" 415 | resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" 416 | integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= 417 | dependencies: 418 | assert-plus "1.0.0" 419 | extsprintf "1.3.0" 420 | json-schema "0.2.3" 421 | verror "1.10.0" 422 | 423 | load-json-file@^1.0.0: 424 | version "1.1.0" 425 | resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" 426 | integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= 427 | dependencies: 428 | graceful-fs "^4.1.2" 429 | parse-json "^2.2.0" 430 | pify "^2.0.0" 431 | pinkie-promise "^2.0.0" 432 | strip-bom "^2.0.0" 433 | 434 | loud-rejection@^1.0.0: 435 | version "1.6.0" 436 | resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" 437 | integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= 438 | dependencies: 439 | currently-unhandled "^0.4.1" 440 | signal-exit "^3.0.0" 441 | 442 | map-obj@^1.0.0, map-obj@^1.0.1: 443 | version "1.0.1" 444 | resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" 445 | integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= 446 | 447 | mediasoup@^3.5.5: 448 | version "3.5.6" 449 | resolved "https://registry.yarnpkg.com/mediasoup/-/mediasoup-3.5.6.tgz#2de2b6a0d30f67507327c860a603b893634179f2" 450 | integrity sha512-gcm/t0OkodKoC8WQ9PRUGnn5Bv/lf1jC98t9IzbjOtFzaGMMI2X9ukEuY/HrgfZK6maO0S8WWR7f6Fgeu8dAbA== 451 | dependencies: 452 | "@types/debug" "^4.1.5" 453 | awaitqueue "^2.1.1" 454 | debug "^4.1.1" 455 | h264-profile-level-id "^1.0.1" 456 | netstring "^0.3.0" 457 | random-number "^0.0.9" 458 | supports-color "^7.1.0" 459 | uuid "^7.0.2" 460 | optionalDependencies: 461 | clang-tools-prebuilt "^0.1.4" 462 | 463 | meow@^3.1.0: 464 | version "3.7.0" 465 | resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" 466 | integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= 467 | dependencies: 468 | camelcase-keys "^2.0.0" 469 | decamelize "^1.1.2" 470 | loud-rejection "^1.0.0" 471 | map-obj "^1.0.1" 472 | minimist "^1.1.3" 473 | normalize-package-data "^2.3.4" 474 | object-assign "^4.0.1" 475 | read-pkg-up "^1.0.1" 476 | redent "^1.0.0" 477 | trim-newlines "^1.0.0" 478 | 479 | mime-db@1.43.0: 480 | version "1.43.0" 481 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" 482 | integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== 483 | 484 | mime-types@^2.1.12, mime-types@~2.1.19: 485 | version "2.1.26" 486 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" 487 | integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== 488 | dependencies: 489 | mime-db "1.43.0" 490 | 491 | minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.5: 492 | version "1.2.5" 493 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" 494 | integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== 495 | 496 | mkdirp@^0.5.0: 497 | version "0.5.5" 498 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" 499 | integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== 500 | dependencies: 501 | minimist "^1.2.5" 502 | 503 | ms@2.0.0: 504 | version "2.0.0" 505 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 506 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 507 | 508 | ms@^2.1.1: 509 | version "2.1.2" 510 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 511 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 512 | 513 | netstring@^0.3.0: 514 | version "0.3.0" 515 | resolved "https://registry.yarnpkg.com/netstring/-/netstring-0.3.0.tgz#868dc5b20c58d3f7305531d49368eaaabd19b712" 516 | integrity sha1-ho3FsgxY0/cwVTHUk2jqqr0ZtxI= 517 | 518 | normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: 519 | version "2.5.0" 520 | resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" 521 | integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== 522 | dependencies: 523 | hosted-git-info "^2.1.4" 524 | resolve "^1.10.0" 525 | semver "2 || 3 || 4 || 5" 526 | validate-npm-package-license "^3.0.1" 527 | 528 | nugget@^1.5.1: 529 | version "1.6.2" 530 | resolved "https://registry.yarnpkg.com/nugget/-/nugget-1.6.2.tgz#88ca6e03ba5706a99173f5da0902593d6bcae107" 531 | integrity sha1-iMpuA7pXBqmRc/XaCQJZPWvK4Qc= 532 | dependencies: 533 | debug "^2.1.3" 534 | minimist "^1.1.0" 535 | pretty-bytes "^1.0.2" 536 | progress-stream "^1.1.0" 537 | request "^2.45.0" 538 | single-line-log "^0.4.1" 539 | throttleit "0.0.2" 540 | 541 | nuid@^1.1.2: 542 | version "1.1.4" 543 | resolved "https://registry.yarnpkg.com/nuid/-/nuid-1.1.4.tgz#6145710b6cc9ef0df7b94af09c6d750925939097" 544 | integrity sha512-PXiYyHhGfrq8H4g5HyC8enO1lz6SBe5z6x1yx/JG4tmADzDGJVQy3l1sRf3VtEvPsN8dGn9hRFRwDKWL62x0BA== 545 | 546 | oauth-sign@~0.9.0: 547 | version "0.9.0" 548 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" 549 | integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== 550 | 551 | object-assign@^4.0.1: 552 | version "4.1.1" 553 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 554 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 555 | 556 | object-keys@~0.4.0: 557 | version "0.4.0" 558 | resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" 559 | integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY= 560 | 561 | parse-json@^2.2.0: 562 | version "2.2.0" 563 | resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" 564 | integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= 565 | dependencies: 566 | error-ex "^1.2.0" 567 | 568 | path-exists@^1.0.0: 569 | version "1.0.0" 570 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-1.0.0.tgz#d5a8998eb71ef37a74c34eb0d9eba6e878eea081" 571 | integrity sha1-1aiZjrce83p0w06w2eum6HjuoIE= 572 | 573 | path-exists@^2.0.0: 574 | version "2.1.0" 575 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" 576 | integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= 577 | dependencies: 578 | pinkie-promise "^2.0.0" 579 | 580 | path-parse@^1.0.6: 581 | version "1.0.6" 582 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" 583 | integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== 584 | 585 | path-type@^1.0.0: 586 | version "1.1.0" 587 | resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" 588 | integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= 589 | dependencies: 590 | graceful-fs "^4.1.2" 591 | pify "^2.0.0" 592 | pinkie-promise "^2.0.0" 593 | 594 | performance-now@^2.1.0: 595 | version "2.1.0" 596 | resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" 597 | integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= 598 | 599 | pify@^2.0.0: 600 | version "2.3.0" 601 | resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" 602 | integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= 603 | 604 | pinkie-promise@^2.0.0: 605 | version "2.0.1" 606 | resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" 607 | integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= 608 | dependencies: 609 | pinkie "^2.0.0" 610 | 611 | pinkie@^2.0.0: 612 | version "2.0.4" 613 | resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" 614 | integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= 615 | 616 | pretty-bytes@^1.0.2: 617 | version "1.0.4" 618 | resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84" 619 | integrity sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ= 620 | dependencies: 621 | get-stdin "^4.0.1" 622 | meow "^3.1.0" 623 | 624 | progress-stream@^1.1.0: 625 | version "1.2.0" 626 | resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77" 627 | integrity sha1-LNPP6jO6OonJwSHsM0er6asSX3c= 628 | dependencies: 629 | speedometer "~0.1.2" 630 | through2 "~0.2.3" 631 | 632 | psl@^1.1.28: 633 | version "1.8.0" 634 | resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" 635 | integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== 636 | 637 | punycode@^2.1.0, punycode@^2.1.1: 638 | version "2.1.1" 639 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 640 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== 641 | 642 | qs@~6.5.2: 643 | version "6.5.2" 644 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" 645 | integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== 646 | 647 | random-number@^0.0.9: 648 | version "0.0.9" 649 | resolved "https://registry.yarnpkg.com/random-number/-/random-number-0.0.9.tgz#5907b96f05041807c52aed601c869524d86fbbd5" 650 | integrity sha512-ipG3kRCREi/YQpi2A5QGcvDz1KemohovWmH6qGfboVyyGdR2t/7zQz0vFxrfxpbHQgPPdtVlUDaks3aikD1Ljw== 651 | 652 | read-pkg-up@^1.0.1: 653 | version "1.0.1" 654 | resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" 655 | integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= 656 | dependencies: 657 | find-up "^1.0.0" 658 | read-pkg "^1.0.0" 659 | 660 | read-pkg@^1.0.0: 661 | version "1.1.0" 662 | resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" 663 | integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= 664 | dependencies: 665 | load-json-file "^1.0.0" 666 | normalize-package-data "^2.3.2" 667 | path-type "^1.0.0" 668 | 669 | readable-stream@~1.1.9: 670 | version "1.1.14" 671 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" 672 | integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= 673 | dependencies: 674 | core-util-is "~1.0.0" 675 | inherits "~2.0.1" 676 | isarray "0.0.1" 677 | string_decoder "~0.10.x" 678 | 679 | redent@^1.0.0: 680 | version "1.0.0" 681 | resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" 682 | integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= 683 | dependencies: 684 | indent-string "^2.1.0" 685 | strip-indent "^1.0.1" 686 | 687 | repeating@^2.0.0: 688 | version "2.0.1" 689 | resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" 690 | integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= 691 | dependencies: 692 | is-finite "^1.0.0" 693 | 694 | request@^2.45.0: 695 | version "2.88.2" 696 | resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" 697 | integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== 698 | dependencies: 699 | aws-sign2 "~0.7.0" 700 | aws4 "^1.8.0" 701 | caseless "~0.12.0" 702 | combined-stream "~1.0.6" 703 | extend "~3.0.2" 704 | forever-agent "~0.6.1" 705 | form-data "~2.3.2" 706 | har-validator "~5.1.3" 707 | http-signature "~1.2.0" 708 | is-typedarray "~1.0.0" 709 | isstream "~0.1.2" 710 | json-stringify-safe "~5.0.1" 711 | mime-types "~2.1.19" 712 | oauth-sign "~0.9.0" 713 | performance-now "^2.1.0" 714 | qs "~6.5.2" 715 | safe-buffer "^5.1.2" 716 | tough-cookie "~2.5.0" 717 | tunnel-agent "^0.6.0" 718 | uuid "^3.3.2" 719 | 720 | resolve@^1.10.0: 721 | version "1.15.1" 722 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" 723 | integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== 724 | dependencies: 725 | path-parse "^1.0.6" 726 | 727 | safe-buffer@^5.0.1, safe-buffer@^5.1.2: 728 | version "5.2.0" 729 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" 730 | integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== 731 | 732 | safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: 733 | version "2.1.2" 734 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 735 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 736 | 737 | "semver@2 || 3 || 4 || 5": 738 | version "5.7.1" 739 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" 740 | integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== 741 | 742 | signal-exit@^3.0.0: 743 | version "3.0.3" 744 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" 745 | integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== 746 | 747 | single-line-log@^0.4.1: 748 | version "0.4.1" 749 | resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-0.4.1.tgz#87a55649f749d783ec0dcd804e8140d9873c7cee" 750 | integrity sha1-h6VWSfdJ14PsDc2AToFA2Yc8fO4= 751 | 752 | spdx-correct@^3.0.0: 753 | version "3.1.0" 754 | resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" 755 | integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== 756 | dependencies: 757 | spdx-expression-parse "^3.0.0" 758 | spdx-license-ids "^3.0.0" 759 | 760 | spdx-exceptions@^2.1.0: 761 | version "2.2.0" 762 | resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" 763 | integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== 764 | 765 | spdx-expression-parse@^3.0.0: 766 | version "3.0.0" 767 | resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" 768 | integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== 769 | dependencies: 770 | spdx-exceptions "^2.1.0" 771 | spdx-license-ids "^3.0.0" 772 | 773 | spdx-license-ids@^3.0.0: 774 | version "3.0.5" 775 | resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" 776 | integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== 777 | 778 | speedometer@~0.1.2: 779 | version "0.1.4" 780 | resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d" 781 | integrity sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0= 782 | 783 | sshpk@^1.7.0: 784 | version "1.16.1" 785 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" 786 | integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== 787 | dependencies: 788 | asn1 "~0.2.3" 789 | assert-plus "^1.0.0" 790 | bcrypt-pbkdf "^1.0.0" 791 | dashdash "^1.12.0" 792 | ecc-jsbn "~0.1.1" 793 | getpass "^0.1.1" 794 | jsbn "~0.1.0" 795 | safer-buffer "^2.0.2" 796 | tweetnacl "~0.14.0" 797 | 798 | string_decoder@~0.10.x: 799 | version "0.10.31" 800 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" 801 | integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= 802 | 803 | strip-bom@^2.0.0: 804 | version "2.0.0" 805 | resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" 806 | integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= 807 | dependencies: 808 | is-utf8 "^0.2.0" 809 | 810 | strip-indent@^1.0.1: 811 | version "1.0.1" 812 | resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" 813 | integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= 814 | dependencies: 815 | get-stdin "^4.0.1" 816 | 817 | supports-color@^7.1.0: 818 | version "7.1.0" 819 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" 820 | integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== 821 | dependencies: 822 | has-flag "^4.0.0" 823 | 824 | throttleit@0.0.2: 825 | version "0.0.2" 826 | resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf" 827 | integrity sha1-z+34jmDADdlpe2H90qg0OptoDq8= 828 | 829 | through2@~0.2.3: 830 | version "0.2.3" 831 | resolved "https://registry.yarnpkg.com/through2/-/through2-0.2.3.tgz#eb3284da4ea311b6cc8ace3653748a52abf25a3f" 832 | integrity sha1-6zKE2k6jEbbMis42U3SKUqvyWj8= 833 | dependencies: 834 | readable-stream "~1.1.9" 835 | xtend "~2.1.1" 836 | 837 | tough-cookie@~2.5.0: 838 | version "2.5.0" 839 | resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" 840 | integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== 841 | dependencies: 842 | psl "^1.1.28" 843 | punycode "^2.1.1" 844 | 845 | trim-newlines@^1.0.0: 846 | version "1.0.0" 847 | resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" 848 | integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= 849 | 850 | ts-nats@^1.2.12: 851 | version "1.2.12" 852 | resolved "https://registry.yarnpkg.com/ts-nats/-/ts-nats-1.2.12.tgz#7de7a191177a0bf49939e3114d60fed4ad0cafa3" 853 | integrity sha512-pYVUoXjQYtiYA3ShOFqt+9SEo2fpGNyX7TNMEDugU3E7H1HrGsgzc8J4I2fJDMkjPOMDd5K/OXdQg1Yefe1J4Q== 854 | dependencies: 855 | nuid "^1.1.2" 856 | ts-nkeys "^1.0.16" 857 | 858 | ts-nkeys@^1.0.16: 859 | version "1.0.16" 860 | resolved "https://registry.yarnpkg.com/ts-nkeys/-/ts-nkeys-1.0.16.tgz#b0c6e7c4f16f976c7e7ddb6982fc789a2f971248" 861 | integrity sha512-1qrhAlavbm36wtW+7NtKOgxpzl+70NTF8xlz9mEhiA5zHMlMxjj3sEVKWm3pGZhHXE0Q3ykjrj+OSRVaYw+Dqg== 862 | dependencies: 863 | tweetnacl "^1.0.3" 864 | 865 | tunnel-agent@^0.6.0: 866 | version "0.6.0" 867 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 868 | integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= 869 | dependencies: 870 | safe-buffer "^5.0.1" 871 | 872 | tweetnacl@^0.14.3, tweetnacl@~0.14.0: 873 | version "0.14.5" 874 | resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 875 | integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= 876 | 877 | tweetnacl@^1.0.3: 878 | version "1.0.3" 879 | resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" 880 | integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== 881 | 882 | typescript@^3.8.3: 883 | version "3.8.3" 884 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" 885 | integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== 886 | 887 | uri-js@^4.2.2: 888 | version "4.2.2" 889 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" 890 | integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== 891 | dependencies: 892 | punycode "^2.1.0" 893 | 894 | uuid@^3.3.2: 895 | version "3.4.0" 896 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" 897 | integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== 898 | 899 | uuid@^7.0.2: 900 | version "7.0.3" 901 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" 902 | integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== 903 | 904 | validate-npm-package-license@^3.0.1: 905 | version "3.0.4" 906 | resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" 907 | integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== 908 | dependencies: 909 | spdx-correct "^3.0.0" 910 | spdx-expression-parse "^3.0.0" 911 | 912 | verror@1.10.0: 913 | version "1.10.0" 914 | resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" 915 | integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= 916 | dependencies: 917 | assert-plus "^1.0.0" 918 | core-util-is "1.0.2" 919 | extsprintf "^1.2.0" 920 | 921 | xtend@~2.1.1: 922 | version "2.1.2" 923 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" 924 | integrity sha1-bv7MKk2tjmlixJAbM3znuoe10os= 925 | dependencies: 926 | object-keys "~0.4.0" 927 | --------------------------------------------------------------------------------