diff --git a/.gitignore b/.gitignore index 6704566..48da8b2 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,9 @@ dist # TernJS port file .tern-port + +# Media folder +media/ + +# Startup commands +startupAMCPCommands diff --git a/package.json b/package.json index ce9a962..d5a11c7 100644 --- a/package.json +++ b/package.json @@ -8,24 +8,36 @@ "license": "GPL-3.0", "private": false, "scripts": { + "buildstart": "yarn build && yarn start", "build": "trash dist && yarn build:main", "build:main": "tsc -p tsconfig.json", "lint": "eslint . --ext .ts", "lint:fix": "yarn lint --fix", - "start": "node dist/index.js" + "start": "node dist/index.js", + "build-start": "yarn build && yarn start" }, "dependencies": { "@koa/cors": "3", + "@types/koa-bodyparser": "^4.3.0", + "@types/koa-route": "^3.2.4", + "@types/uuid": "^8.3.0", + "@types/webrtc": "^0.0.26", "beamcoder": "^0.6.11", "koa": "^2.13.0", + "koa-bodyparser": "^4.3.0", + "koa-route": "^3.2.0", + "koa-static": "^5.0.0", "macadam": "^2.0.14", "naudiodon": "^2.3.4", "nodencl": "^1.3.1", "redioactive": "0.0.16", - "ts-osc": "^0.3.1" + "ts-osc": "^0.3.1", + "uuid": "^8.3.2", + "wrtc": "^0.4.7" }, "devDependencies": { "@types/koa": "2.11.6", + "@types/koa-static": "^4.0.1", "@types/koa__cors": "3.0.2", "@types/node": "^14.14.28", "@typescript-eslint/eslint-plugin": "^4.15.1", diff --git a/src/AMCP/basicCmds.ts b/src/AMCP/basicCmds.ts index d7285f1..f77b8d9 100644 --- a/src/AMCP/basicCmds.ts +++ b/src/AMCP/basicCmds.ts @@ -24,6 +24,7 @@ import { Channel } from '../channel' import { ConsumerRegistry } from '../consumer/consumer' import { ClJobs } from '../clJobQueue' import { ConfigParams } from '../config' +import { assetManager } from '../assets/assetManager' export class BasicCmds implements CmdList { private readonly consumerRegistry: ConsumerRegistry @@ -104,8 +105,10 @@ export class BasicCmds implements CmdList { if (params.find((param, i) => { curParam = i; return param === 'SEEK' }) !== undefined) seek = +params[curParam + 1] + const url = await assetManager.getAsset(clip) + const loadParams: LoadParams = { - url: clip, + url: url, layer: chanLay.layer, channel: chanNum, loop: loop, @@ -224,6 +227,7 @@ export class BasicCmds implements CmdList { try { const consumer = this.consumerRegistry.createConsumer( + channel, chanLay.channel, consumerIndex, this.parseParams(params), diff --git a/src/assets/assetManager.ts b/src/assets/assetManager.ts new file mode 100644 index 0000000..e11f1cf --- /dev/null +++ b/src/assets/assetManager.ts @@ -0,0 +1,28 @@ +import fs from 'fs' +import path from 'path' + +const MEDIA_DIR = '../media/dpp/' + +class AssetManager { + public getAsset(clip: string): Promise { + const files = fs.existsSync(MEDIA_DIR) ? fs.readdirSync(MEDIA_DIR) : [] + console.log("FILES:", files) + if (clip.match(/^(?!file).*:\/\//i)) { + return Promise.resolve(clip) + } + + const exact_match = files.find((f) => f === clip) + if (exact_match) { + return Promise.resolve(path.join(MEDIA_DIR, exact_match)) + } + + const inexact_match = files.find((f) => f.toUpperCase() === clip.toUpperCase()) + if (inexact_match) { + return Promise.resolve(path.join(MEDIA_DIR, inexact_match)) + } + + return Promise.reject(`Could not find clip ${clip}`) + } +} + +export const assetManager = new AssetManager() diff --git a/src/channel.ts b/src/channel.ts index 8eeae89..4b39b00 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -53,6 +53,7 @@ export class Channel { this.clJobs = clJobs this.combiner = new Combiner(this.clContext, chanID, this.consumerConfig.format, this.clJobs) this.consumers = this.consumerRegistry.createConsumers( + this, chanNum, chanID, this.consumerConfig, diff --git a/src/consumer/consumer.ts b/src/consumer/consumer.ts index 747d855..6d15be8 100644 --- a/src/consumer/consumer.ts +++ b/src/consumer/consumer.ts @@ -27,6 +27,7 @@ import { Frame } from 'beamcoder' import { Channel } from '../channel' import { ConfigParams, VideoFormat, DeviceConfig, ConsumerConfig } from '../config' import { ClJobs } from '../clJobQueue' +import { WebRTCConsumerFactory } from './webrtcConsumer' export interface Consumer { initialise(): Promise @@ -35,6 +36,7 @@ export interface Consumer { export interface ConsumerFactory { createConsumer( + channel: Channel, chanID: string, params: ConfigParams, format: VideoFormat, @@ -55,12 +57,14 @@ export class ConsumerRegistry { this.consumerFactories.set('decklink', new MacadamConsumerFactory(clContext)) this.consumerFactories.set('screen', new ScreenConsumerFactory(clContext)) this.consumerFactories.set('ffmpeg', new FFmpegConsumerFactory(clContext)) + this.consumerFactories.set('webrtc', new WebRTCConsumerFactory(clContext)) this.consumers = new Map() this.chanIDs = new Map() this.formats = new Map() } createConsumer( + channel: Channel, chanNum: number, consumerIndex: number, params: ConfigParams, @@ -83,6 +87,7 @@ export class ConsumerRegistry { if (!format) throw new Error(`channel format not registered`) const consumer = factory.createConsumer( + channel, chanID, params, format, @@ -115,6 +120,7 @@ export class ConsumerRegistry { } createConsumers( + channel: Channel, chanNum: number, chanID: string, config: ConsumerConfig, @@ -124,7 +130,7 @@ export class ConsumerRegistry { this.formats.set(chanNum, config.format) return config.devices.map((device) => - this.createConsumer(chanNum, this.consumerIndex++, {}, device, clJobs) + this.createConsumer(channel, chanNum, this.consumerIndex++, {}, device, clJobs) ) } } diff --git a/src/consumer/ffmpegConsumer.ts b/src/consumer/ffmpegConsumer.ts index 506615a..3f55388 100644 --- a/src/consumer/ffmpegConsumer.ts +++ b/src/consumer/ffmpegConsumer.ts @@ -26,6 +26,7 @@ import { FromRGBA } from '../process/io' import { Writer } from '../process/yuv422p8' import { ConfigParams, VideoFormat, DeviceConfig } from '../config' import { ClJobs } from '../clJobQueue' +import { Channel } from '../channel' interface AudioBuffer { buffer: Buffer @@ -261,6 +262,7 @@ export class FFmpegConsumerFactory implements ConsumerFactory { } createConsumer( + _channel: Channel, chanID: string, params: ConfigParams, format: VideoFormat, diff --git a/src/consumer/macadamConsumer.ts b/src/consumer/macadamConsumer.ts index a1ded5f..9467b6e 100644 --- a/src/consumer/macadamConsumer.ts +++ b/src/consumer/macadamConsumer.ts @@ -27,6 +27,7 @@ import { Writer } from '../process/v210' import { Frame, Filterer, filterer } from 'beamcoder' import { ConfigParams, VideoFormat, DeviceConfig } from '../config' import { ClJobs } from '../clJobQueue' +import { Channel } from '../channel' interface DecklinkConfig extends DeviceConfig { keyDeviceIndex: number @@ -296,6 +297,7 @@ export class MacadamConsumerFactory implements ConsumerFactory } createConsumer( + _channel: Channel, chanID: string, params: ConfigParams, format: VideoFormat, diff --git a/src/consumer/screenConsumer.ts b/src/consumer/screenConsumer.ts index e8cffa5..8a9010b 100644 --- a/src/consumer/screenConsumer.ts +++ b/src/consumer/screenConsumer.ts @@ -29,6 +29,7 @@ import { FromRGBA } from '../process/io' import { Writer } from '../process/rgba8' import { ConfigParams, VideoFormat, DeviceConfig } from '../config' import { ClJobs } from '../clJobQueue' +import { Channel } from '../channel' interface AudioBuffer { buffer: Buffer @@ -237,6 +238,7 @@ export class ScreenConsumerFactory implements ConsumerFactory { } createConsumer( + _channel: Channel, chanID: string, params: ConfigParams, format: VideoFormat, diff --git a/src/consumer/webrtcConsumer/api.ts b/src/consumer/webrtcConsumer/api.ts new file mode 100644 index 0000000..1279855 --- /dev/null +++ b/src/consumer/webrtcConsumer/api.ts @@ -0,0 +1,126 @@ +import Koa from 'koa' +import * as _ from 'koa-route' +import { ConsumerInfoExt } from './peerManager' +// import { RTCSessionDescriptionInit /* RTCPeerConnection, nonstandard as WebRTCNonstandard, MediaStream, MediaStreamTrack */ } from 'wrtc' +// const { RTCVideoSource /*, rgbaToI420, RTCAudioSource */ } = WebRTCNonstandard + +export default function mountConnectionsApi( + kapp: Koa, + consumers: Map, + prefix: string = '' +) { + kapp.use(_.get(`${prefix}/streams`, (ctx) => { + ctx.body = JSON.stringify(Array.from(consumers.values()).map(c => ({ streamId: c.id, description: c.description}))) + })) + kapp.use(_.get(`${prefix}/streams/:consumerId/connections`, (ctx, consumerId: string) => { + const consumer = consumers.get(consumerId) + if (!consumer) { + ctx.status = 404 + return + } + ctx.body = consumer.connectionManager.getConnections() + })) + kapp.use( + _.post(`${prefix}/streams/:consumerId/connections`, async (ctx, consumerId: string) => { + try { + const consumer = consumers.get(consumerId) + if (!consumer) { + ctx.status = 404 + return + } + let offer = ctx.request.body as RTCSessionDescription + // console.log('Received offer:', offer) + // const connection = new RTCPeerConnection({}) + // await connection.setRemoteDescription(offer) + const connection = await consumer.connectionManager.createConnection(offer) + // console.log(connection.localDescription) + // ctx.body = connection + ctx.body = connection.localDescription + } catch (error) { + console.error(error) + ctx.status = 500 + } + }) + ) + kapp.use( + _.delete(`${prefix}/streams/:consumerId/connections/:id`, (ctx, consumerId: string, id: string) => { + const consumer = consumers.get(consumerId) + if (!consumer) { + ctx.status = 404 + return + } + const connection = consumer.connectionManager.getConnection(id) + if (!connection) { + ctx.status = 404 + return + } + connection.close() + ctx.body = connection + }) + ) + kapp.use( + _.get(`${prefix}/streams/:consumerId/connections/:id`, (ctx, consumerId: string, id: string) => { + const consumer = consumers.get(consumerId) + if (!consumer) { + ctx.status = 404 + return + } + const connection = consumer.connectionManager.getConnection(id) + if (!connection) { + ctx.status = 404 + return + } + ctx.body = connection + }) + ) + kapp.use( + _.get(`${prefix}/streams/:consumerId/connections/:id/local-description`, (ctx, consumerId: string, id: string) => { + const consumer = consumers.get(consumerId) + if (!consumer) { + ctx.status = 404 + return + } + const connection = consumer.connectionManager.getConnection(id) + if (!connection) { + ctx.status = 404 + return + } + ctx.body = connection.toJSON().localDescription + }) + ) + kapp.use( + _.get(`${prefix}/streams/:consumerId/connections/:id/remote-description`, (ctx, consumerId: string, id: string) => { + const consumer = consumers.get(consumerId) + if (!consumer) { + ctx.status = 404 + return + } + const connection = consumer.connectionManager.getConnection(id) + if (!connection) { + ctx.status = 404 + return + } + ctx.body = connection.toJSON().remoteDescription + }) + ) + // kapp.use( + // _.post(`${prefix}/streams/:consumerId/connections/:id/remote-description`, async (ctx, consumerId: string, id: string) => { + // const consumer = consumers.get(consumerId) + // if (!consumer) { + // ctx.status = 404 + // return + // } + // const connection = consumer.connectionManager.getConnection(id) + // if (!connection) { + // ctx.status = 404 + // return + // } + // try { + // await connection.applyAnswer(ctx.request.body as RTCSessionDescriptionInit) + // ctx.body = connection.toJSON().remoteDescription + // } catch (error) { + // ctx.status = 400 + // } + // }) + //) +} diff --git a/src/consumer/webrtcConsumer/connections/connection.ts b/src/consumer/webrtcConsumer/connections/connection.ts new file mode 100644 index 0000000..3b3ac51 --- /dev/null +++ b/src/consumer/webrtcConsumer/connections/connection.ts @@ -0,0 +1,28 @@ +import { EventEmitter } from 'events' + +/** + * Lifted from: https://github.com/node-webrtc/node-webrtc-examples/ + */ + +export default class Connection extends EventEmitter { + id: string + state: 'open' | 'closed' + + constructor(id: string, _options: {}) { + super() + this.id = id + this.state = 'open' + } + + close() { + this.state = 'closed' + this.emit('closed') + } + + toJSON() { + return { + id: this.id, + state: this.state + } + } +} diff --git a/src/consumer/webrtcConsumer/connections/connectionManager.ts b/src/consumer/webrtcConsumer/connections/connectionManager.ts new file mode 100644 index 0000000..54b5e9f --- /dev/null +++ b/src/consumer/webrtcConsumer/connections/connectionManager.ts @@ -0,0 +1,71 @@ +import DefaultConnection from './connection' + +export type ConnectionID = string +type IDGenerationFunction = () => ConnectionID + +export interface IConnectionManagerOptions { + createConnection: (id: string, baseOptions: {}) => ConnectionType + generateId: IDGenerationFunction +} + +export class ConnectionManager { + constructor(options: IConnectionManagerOptions) { + const { createConnection, generateId } = options + + const connections = new Map() + const closedListeners = new Map() + + function createId() { + do { + const id = generateId() + if (!connections.has(id)) { + return id + } + // eslint-disable-next-line + } while (true) + } + + function deleteConnection(connection: ConnectionType) { + // 1. Remove "closed" listener. + const closedListener = closedListeners.get(connection) + closedListeners.delete(connection) + connection.removeListener('closed', closedListener) + + // 2. Remove the Connection from the Map. + connections.delete(connection.id) + } + + this.createConnection = async () => { + const id = createId() + const connection = createConnection(id, {}) + + // 1. Add the "closed" listener. + function closedListener() { + deleteConnection(connection) + } + closedListeners.set(connection, closedListener) + connection.once('closed', closedListener) + + // 2. Add the Connection to the Map. + connections.set(connection.id, connection) + + return connection + } + + this.getConnection = (id) => { + return connections.get(id) || null + } + + this.getConnections = () => { + return [...connections.values()] + } + } + + createConnection: () => Promise + getConnection: (id: ConnectionID) => ConnectionType | null + getConnections: () => ConnectionType[] + + toJSON() { + return this.getConnections().map((connection) => connection.toJSON()) + } +} diff --git a/src/consumer/webrtcConsumer/connections/webRTCConnection.ts b/src/consumer/webrtcConsumer/connections/webRTCConnection.ts new file mode 100644 index 0000000..f4e3f82 --- /dev/null +++ b/src/consumer/webrtcConsumer/connections/webRTCConnection.ts @@ -0,0 +1,194 @@ +import { RTCPeerConnection as DefaultRTCPeerConnection } from 'wrtc' +import { ConnectionID } from './connectionManager' +import Connection from './connection' + +const TIME_TO_CONNECTED = 10000 +const TIME_TO_HOST_CANDIDATES = 3000 // NOTE(mroberts): Too long. +const TIME_TO_RECONNECTED = 10000 + +export interface IWebRTCConnectionOptions { + createRTCPeerConnection: (config: RTCConfiguration) => DefaultRTCPeerConnection + beforeAnswer: (peerConnection: DefaultRTCPeerConnection) => void + clearTimeout: (timeoutId: NodeJS.Timeout) => void + setTimeout: (callback: (...args: any[]) => void, ms: number, ...args: any[]) => NodeJS.Timeout + timeToConnected: number + timeToHostCandidates: number + timeToReconnected: number +} + +export class WebRTCConnection extends Connection { + private peerConnection: DefaultRTCPeerConnection + private options: IWebRTCConnectionOptions + private connectionTimer: NodeJS.Timeout | null = null + private reconnectionTimer: NodeJS.Timeout | null = null + + constructor(id: ConnectionID, options0: Partial = {}) { + super(id, options0) + + console.log("In WebRTCConnection constructor") + + this.options = { + createRTCPeerConnection: (config: RTCConfiguration) => new DefaultRTCPeerConnection(config), + beforeAnswer() {}, + clearTimeout, + setTimeout, + timeToConnected: TIME_TO_CONNECTED, + timeToHostCandidates: TIME_TO_HOST_CANDIDATES, + timeToReconnected: TIME_TO_RECONNECTED, + ...options0 + } + + const { createRTCPeerConnection, beforeAnswer, timeToConnected } = this.options + + const peerConnection = this.peerConnection = createRTCPeerConnection({}) + // // @ts-ignore + // sdpSemantics: 'unified-plan' + // })) + + console.log("About to call before answer") + beforeAnswer(peerConnection) + + this.connectionTimer = this.options.setTimeout(() => { + if ( + peerConnection.iceConnectionState !== 'connected' && + peerConnection.iceConnectionState !== 'completed' + ) { + this.close() + } + }, timeToConnected) + + peerConnection.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange) + } + + private onIceConnectionStateChange = () => { + const peerConnection = this.peerConnection + const { setTimeout, clearTimeout, timeToReconnected } = this.options + + if ( + peerConnection.iceConnectionState === 'connected' || + peerConnection.iceConnectionState === 'completed' + ) { + if (this.connectionTimer) { + clearTimeout(this.connectionTimer) + this.connectionTimer = null + } + if (this.reconnectionTimer) { + clearTimeout(this.reconnectionTimer) + this.reconnectionTimer = null + } + } else if ( + peerConnection.iceConnectionState === 'disconnected' || + peerConnection.iceConnectionState === 'failed' + ) { + if (!this.connectionTimer && !this.reconnectionTimer) { + const self = this + this.reconnectionTimer = setTimeout(() => { + self.close() + }, timeToReconnected) + } + } + } + + doAnswer = async () => { + const offer = await this.peerConnection.createAnswer() + await this.peerConnection.setLocalDescription(offer) + // try { + // await waitUntilIceGatheringStateComplete(this.peerConnection, this.options) + // } catch (error) { + // this.close() + // throw error + // } + } + + applyOffer = async (offer: RTCSessionDescriptionInit) => { + await this.peerConnection.setRemoteDescription(offer) + } + + close = () => { + const peerConnection = this.peerConnection + const { clearTimeout } = this.options + + peerConnection.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange) + if (this.connectionTimer) { + clearTimeout(this.connectionTimer) + this.connectionTimer = null + } + if (this.reconnectionTimer) { + clearTimeout(this.reconnectionTimer) + this.reconnectionTimer = null + } + peerConnection.close() + super.close() + } + + toJSON = () => { + return { + ...super.toJSON(), + iceConnectionState: this.iceConnectionState, + localDescription: this.localDescription, + remoteDescription: this.remoteDescription, + signalingState: this.signalingState + } + } + + get iceConnectionState() { + return this.peerConnection.iceConnectionState + } + + get localDescription() { + return descriptionToJSON(this.peerConnection.localDescription, false) + } + + get remoteDescription() { + return descriptionToJSON(this.peerConnection.remoteDescription) + } + + get signalingState() { + return this.peerConnection.signalingState + } +} + +function descriptionToJSON( + description: RTCSessionDescription | null, + shouldDisableTrickleIce?: boolean +) { + return !description + ? {} + : { + type: description.type, + sdp: shouldDisableTrickleIce ? disableTrickleIce(description.sdp) : description.sdp + } +} + +function disableTrickleIce(sdp: string) { + return sdp.replace(/\r\na=ice-options:trickle/g, '') +} + +// async function waitUntilIceGatheringStateComplete< +// RTCPeerConnectionType extends DefaultRTCPeerConnection +// >(peerConnection: RTCPeerConnectionType, options: IOptions) { +// if (peerConnection.iceGatheringState === 'complete') { +// return +// } + +// const { timeToHostCandidates } = options + +// const promise = new Promise((resolve, reject) => { +// const timeout = options.setTimeout(() => { +// peerConnection.removeEventListener('icecandidate', onIceCandidate) +// reject(new Error('Timed out waiting for host candidates')) +// }, timeToHostCandidates) + +// function onIceCandidate({ candidate }: { candidate: RTCIceCandidate }) { +// if (!candidate) { +// options.clearTimeout(timeout) +// peerConnection.removeEventListener('icecandidate', onIceCandidate) +// resolve() +// } +// } + +// peerConnection.addEventListener('icecandidate', onIceCandidate) +// }) + +// await promise +// } diff --git a/src/consumer/webrtcConsumer/connections/webRTCConnectionManager.ts b/src/consumer/webrtcConsumer/connections/webRTCConnectionManager.ts new file mode 100644 index 0000000..f43ee5d --- /dev/null +++ b/src/consumer/webrtcConsumer/connections/webRTCConnectionManager.ts @@ -0,0 +1,28 @@ +import { ConnectionID, IConnectionManagerOptions, ConnectionManager } from './connectionManager' +import { WebRTCConnection } from './webRTCConnection' + +export class WebRtcConnectionManager { + private connectionManager: ConnectionManager + + constructor(options: IConnectionManagerOptions) { + this.connectionManager = new ConnectionManager(options) + } + + createConnection = async (offer?: RTCSessionDescription) => { + const connection = await this.connectionManager.createConnection() + console.log("In WebRTC connection manager createConnection.") + if (offer) { + await connection.applyOffer(offer) + console.log("Applied offer ... time to answer") + await connection.doAnswer() + console.log("Generated answer", connection.localDescription) + } + return connection + } + + getConnection = (id: ConnectionID) => this.connectionManager.getConnection(id) + + getConnections = (): RTCPeerConnectionType[] => this.connectionManager.getConnections() + + toJSON = () => this.getConnections().map((connection) => connection.toJSON()) +} diff --git a/src/consumer/webrtcConsumer/index.ts b/src/consumer/webrtcConsumer/index.ts new file mode 100644 index 0000000..2db0d8e --- /dev/null +++ b/src/consumer/webrtcConsumer/index.ts @@ -0,0 +1,384 @@ +/* + Phaneron - Clustered, accelerated and cloud-fit video server, pre-assembled and in kit form. + Copyright (C) 2020 Streampunk Media Ltd. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + https://www.streampunk.media/ mailto:furnace@streampunk.media + 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. +*/ + +import { clContext as nodenCLContext, OpenCLBuffer } from 'nodencl' +import { RedioPipe, RedioEnd, nil, isValue, Valve, Spout } from 'redioactive' +import { Frame, Filterer, filterer } from 'beamcoder' +import { ConsumerFactory, Consumer } from '../consumer' +import { FromRGBA } from '../../process/io' +import { Writer } from '../../process/yuv420p' +import { ConfigParams, VideoFormat, DeviceConfig } from '../../config' +import { ClJobs } from '../../clJobQueue' +import { + MediaStreamTrack, + nonstandard as WebRTCNonstandard, + RTCAudioData, + RTCPeerConnection, + RTCVideoFrame, + MediaStream +} from 'wrtc' +import { PeerManager } from './peerManager' +const { RTCVideoSource, /* rgbaToI420,*/ RTCAudioSource } = WebRTCNonstandard +import { v4 as uuidv4 } from 'uuid' +import { Channel } from '../../channel' + +interface AudioBuffer { + buffer: Buffer + timestamp: number +} + +export class WebRTCConsumer implements Consumer { + private readonly clContext: nodenCLContext + private readonly channel: Channel + private readonly chanID: string + private readonly rtcUuid: string + private readonly params: ConfigParams + private readonly format: VideoFormat + private readonly clJobs: ClJobs + private fromRGBA: FromRGBA | undefined + private readonly audioOutChannels: number + private readonly audioTimebase: number[] + private readonly videoTimebase: number[] + private audFilterer: Filterer | undefined + private peerManager: PeerManager + // private rtcVideoSources: Map< + // RTCPeerConnection, + // { source: WebRTCNonstandard.RTCVideoSource; track: MediaStreamTrack } + // > = new Map() + private rtcVideoSource: WebRTCNonstandard.RTCVideoSource + private rtcVideoTrack: MediaStreamTrack + private rtcAudioSource: WebRTCNonstandard.RTCAudioSource + private rtcAudioTrack: MediaStreamTrack + private mediaStream: MediaStream + private timer: [number, number] + private frameCount: number + + constructor( + context: nodenCLContext, + channel: Channel, + chanID: string, + params: ConfigParams, + format: VideoFormat, + clJobs: ClJobs + ) { + this.clContext = context + this.channel = channel + this.chanID = `${chanID} WebRTC` + this.rtcUuid = `${chanID}-${uuidv4()}` + this.params = params + this.format = format + this.clJobs = clJobs + this.audioOutChannels = 2 + this.audioTimebase = [1, this.format.audioSampleRate] + this.videoTimebase = [this.format.duration, this.format.timescale] + this.timer = [0, 0] + this.frameCount = 0 + + if (Object.keys(this.params).length > 1) + console.log('WebRTC consumer - unused params', this.params) + + this.rtcVideoSource = new RTCVideoSource() + console.log(this.rtcVideoSource) + this.rtcVideoTrack = this.rtcVideoSource.createTrack() + console.log(this.rtcVideoTrack) + this.rtcAudioSource = new RTCAudioSource() + this.rtcAudioTrack = this.rtcAudioSource.createTrack() + + this.mediaStream = new MediaStream({ id: `phaneron` }) + this.mediaStream.addTrack(this.rtcVideoTrack) + this.mediaStream.addTrack(this.rtcAudioTrack) + + this.peerManager = PeerManager.singleton() + this.peerManager.registerSource({ + id: this.rtcUuid, + description: this.chanID, + newPeer: this.newPeer, + peerClose: this.peerClose, + channel: this.channel + }) + + // this.peerManager.on('newPeer', this.newPeer) + // this.peerManager.on('peerClose', this.peerClose) + } + + // TODO - hook this up to be called frm somewhere + async destroy(): Promise { + this.peerManager.destroySource(this.rtcUuid) + this.rtcVideoTrack.stop() + } + + async initialise(): Promise { + const sampleRate = this.audioTimebase[1] + const audInLayout = `${this.format.audioChannels}c` + const audOutLayout = `${this.audioOutChannels}c` + // !!! Needs more work to handle 59.94 frame rates !!! + const samplesPerFrame = + (this.format.audioSampleRate * this.format.duration) / this.format.timescale + const outSampleFormat = 's16' + + this.audFilterer = await filterer({ + filterType: 'audio', + inputParams: [ + { + name: 'in0:a', + timeBase: this.audioTimebase, + sampleRate: sampleRate, + sampleFormat: 'fltp', + channelLayout: audInLayout + } + ], + outputParams: [ + { + name: 'out0:a', + sampleRate: this.format.audioSampleRate, + sampleFormat: outSampleFormat, + channelLayout: audOutLayout + } + ], + filterSpec: `[in0:a] aformat=sample_fmts=${outSampleFormat}:sample_rates=${this.format.audioSampleRate}:channel_layouts=${audOutLayout},asetnsamples=n=${samplesPerFrame}:p=1 [out0:a]` + }) + console.log('\nScreen consumer audio:\n', this.audFilterer.graph.dump()) + + const width = this.format.width + const height = this.format.height + this.fromRGBA = new FromRGBA( + this.clContext, + '709', + new Writer(width, height, false), + this.clJobs + ) + await this.fromRGBA.init() + + console.log('Created WebRTC consumer') + return Promise.resolve() + } + + newPeer = ({ peerConnection }: { peerConnection: RTCPeerConnection }) => { + // const source = new RTCVideoSource() + // const track = source.createTrack() + peerConnection.addTrack(this.rtcVideoTrack, this.mediaStream) + peerConnection.addTrack(this.rtcAudioTrack, this.mediaStream) + // this.rtcVideoSources.set(peerConnection, { source: this.rtcVideoSource, track: this.rtcVideoTrack }) + } + peerClose = ({}: { peerConnection: RTCPeerConnection }) => { + // const descriptor = this.rtcVideoSources.get(peerConnection) + // if (descriptor) { + // // descriptor.track.stop() + // this.rtcVideoSources.delete(peerConnection) + // } + } + + connect( + combineAudio: RedioPipe, + combineVideo: RedioPipe + ): void { + const audFilter: Valve = async (frame) => { + // console.log('Hello from aud filterer', isValue(frame), isValue(frame) && { + // format: frame.format, sample_rate: frame.sample_rate, + // layout: frame.channel_layout, channels: frame.channels }) + if (isValue(frame)) { + if (!this.audFilterer) return nil + const audFilt = this.audFilterer as Filterer + const ff = await audFilt.filter([{ name: 'in0:a', frames: [frame] }]) + // console.log(ff) + const result: AudioBuffer[] = ff[0].frames.map((f) => ({ + buffer: f.data[0], + timestamp: f.pts + })) + return result.length > 0 ? result : nil + } else { + return frame + } + } + + const vidProcess: Valve = async (frame) => { + if (isValue(frame)) { + const fromRGBA = this.fromRGBA as FromRGBA + const clDests = await fromRGBA.createDests() + clDests.forEach((d) => (d.timestamp = frame.timestamp)) + fromRGBA.processFrame(this.chanID, frame, clDests, 0) + await this.clJobs.runQueue({ source: this.chanID, timestamp: frame.timestamp }) + return clDests + } else { + this.clJobs.clearQueue(this.chanID) + return frame + } + } + + const vidSaver: Valve = async ( + frames + ) => { + if (isValue(frames)) { + const fromRGBA = this.fromRGBA as FromRGBA + await Promise.all(frames.map((f) => fromRGBA.saveFrame(f, this.clContext.queue.unload))) + await this.clContext.waitFinish(this.clContext.queue.unload) + return frames + } else { + return frames + } + } + + const screenSpout: Spout< + [(OpenCLBuffer[] | RedioEnd | undefined)?, (AudioBuffer | RedioEnd | undefined)?] | RedioEnd + > = async (frame) => { + // console.log('Hello from screen spout', isValue(frame)) + if (isValue(frame)) { + if (this.frameCount === 0) { + this.timer = process.hrtime() + } + const vidBufs = frame[0] + const audBuf = frame[1] + if (!(audBuf && isValue(audBuf) && vidBufs && isValue(vidBufs))) { + console.log('One-legged zipper:', audBuf, vidBufs) + if (vidBufs && isValue(vidBufs)) vidBufs.forEach(x =>x.release()) + return Promise.resolve() + } + + const atb = this.audioTimebase + const ats = (audBuf.timestamp * atb[0]) / atb[1] + const vtb = this.videoTimebase + const vts = (vidBufs[0].timestamp * vtb[0]) / vtb[1] + if (Math.abs(ats - vts) > 0.1) + console.log('WebRTC audio and video timestamp mismatch - aud:', ats, ' vid:', vts) + + // const write = (_data: Buffer, cb: () => void) => { + // // if ( + // // !this.audioOut.write(data, (err: Error | null | undefined) => { + // // if (err) console.log('Write Error:', err) + // // }) + // // ) { + // // this.audioOut.once('drain', cb) + // // } else { + // process.nextTick(cb) + // // } + // } + + return new Promise((resolve) => { + const lumaBytes = this.format.width * this.format.height + const chromaBytes = lumaBytes / 4 + const frame = Buffer.alloc(lumaBytes + chromaBytes * 2) + vidBufs[0].copy(frame) + vidBufs[1].copy(frame, lumaBytes) + vidBufs[2].copy(frame, lumaBytes + chromaBytes) + vidBufs.forEach(x => x.release()) + + // const i420frame: RTCVideoFrame = { + // width: this.format.width, + // height: this.format.height, + // data: new Uint8ClampedArray(1.5 * this.format.width * this.format.height) + // } + // rgbaToI420( + // { + // width: this.format.width, + // height: this.format.height, + // data: new Uint8ClampedArray(frame) + // }, + // i420frame + // ) + const i420frame: RTCVideoFrame = { + width: this.format.width, + height: this.format.height, + data: new Uint8ClampedArray(frame) + } + + // new Int16Array( + // floatBuffer, + // (i * floatBuffer.length) / 4, + // floatBuffer.length / 4 + + // const samples = + // (this.format.audioSampleRate * this.format.duration) / this.format.timescale + // console.log(`SAMPLES: ${samples}`) + // console.log(`CHANNELS: ${this.format.audioChannels}`) + // console.log(`BUFFER: ${audBuf.buffer.length}`) + for (let i = 0; i < 2; i++) { + const start = (i * audBuf.buffer.length) / 2 + const end = start + audBuf.buffer.length / 2 + const slicedAudio = audBuf.buffer.buffer.slice(start, end) + const audioBuffer: RTCAudioData = { + samples: slicedAudio, + sampleRate: this.format.audioSampleRate, + // // bitsPerSample default is 16 + bitsPerSample: 16, + channelCount: this.audioOutChannels, + // number of frames + numberOfFrames: this.format.audioSampleRate / 100 + } + this.rtcAudioSource.onData(audioBuffer) + } + + // console.log('Calling rtcVideoSource.onFrame()', i420frame) + this.rtcVideoSource.onFrame(i420frame) + + const [gaps, gapn] = process.hrtime(this.timer) + this.frameCount++ + const timeFromStartMS = gaps * 1000 + (gapn / 1000000 | 0) + const nextFrameTime = this.frameCount * 1000 * this.videoTimebase[0] / this.videoTimebase[1] | 0 + const waitForIt = nextFrameTime - timeFromStartMS + // console.log(this.frameCount, waitForIt) + if (waitForIt > 0) { + setTimeout(resolve, waitForIt) + } else { + resolve() + } + + // write(audBuf.buffer, () => { + // vidBuf.release() + // resolve() + // }) + }) + } else { + // this.clContext.logBuffers() + return Promise.resolve() + } + } + + // this.audioOut.start() + + combineVideo + .valve(vidProcess) + // .doto(x => console.log(x)) + .valve(vidSaver) + // .doto(x => console.log(x)) + .zip(combineAudio.valve(audFilter, { oneToMany: true })) + // .doto(x => console.log(x)) + .spout(screenSpout) + } +} + +export class WebRTCConsumerFactory implements ConsumerFactory { + private readonly clContext: nodenCLContext + + constructor(clContext: nodenCLContext) { + this.clContext = clContext + } + + createConsumer( + channel: Channel, + chanID: string, + params: ConfigParams, + format: VideoFormat, + _device: DeviceConfig, + clJobs: ClJobs + ): WebRTCConsumer { + const consumer = new WebRTCConsumer(this.clContext, channel, chanID, params, format, clJobs) + return consumer + } +} diff --git a/src/consumer/webrtcConsumer/peerManager.ts b/src/consumer/webrtcConsumer/peerManager.ts new file mode 100644 index 0000000..2642aa3 --- /dev/null +++ b/src/consumer/webrtcConsumer/peerManager.ts @@ -0,0 +1,140 @@ +import Koa from 'koa' +import cors from '@koa/cors' +import bodyParser from 'koa-bodyparser' +import serve from 'koa-static' +import { v4 as uuidv4 } from 'uuid' + +import connectionManagerApi from './api' +import { RTCPeerConnection } from 'wrtc' +import { WebRtcConnectionManager } from './connections/webRTCConnectionManager' +import { WebRTCConnection } from './connections/webRTCConnection' +import { processCommand } from '../../AMCP/server' +import { Channel } from '../../channel' +let peerManagerSingleton: PeerManager + +interface ConsumerInfo { + readonly id: string + readonly description: string + newPeer: (peer: { peerConnection: RTCPeerConnection }) => void + peerClose: (peer: { peerConnection: RTCPeerConnection }) => void + readonly channel: Channel +} + +export interface ConsumerInfoExt extends ConsumerInfo { + readonly connectionManager: WebRtcConnectionManager +} + +export class PeerManager { + // private connectionManager: WebRtcConnectionManager + private consumers: Map + private kapp: Koa + // private allPeerConnections: RTCPeerConnection[] = [] + + constructor() { + this.consumers = new Map(); + + this.kapp = new Koa() + this.kapp.use(cors()) + this.kapp.use(bodyParser({ enableTypes: [ 'json' ] })) + connectionManagerApi(this.kapp, this.consumers) + this.kapp.use(serve("static/")) + + const server = this.kapp.listen(3002) + process.on('SIGHUP', server.close) + } + + registerSource(info: ConsumerInfo): void { + if(this.consumers.get(info.id)) { + throw new Error(`WebRTC Consumer "${info.id}" already registered`) + } + let receiveChannel + + const beforeAnswer = (peerConnection: RTCPeerConnection) => { + // let that = this + // this.allPeerConnections.push(peerConnection) + info.newPeer({ peerConnection }) + + // NOTE(mroberts): This is a hack so that we can get a callback when the + // RTCPeerConnection is closed. In the future, we can subscribe to + // "connectionstatechange" events. + const { close } = peerConnection + peerConnection.close = function (...args) { + info.peerClose({ peerConnection }) + return close.apply(this, args) + } + + peerConnection.ondatachannel = (event) => { + console.log("Data channel time", event) + receiveChannel = event.channel + receiveChannel.onmessage = onReceiveMessageCallback + } + } + + let paused = false + let channel = info.channel + let firstMessage = true + + function onReceiveMessageCallback(event: MessageEvent) { + console.log('Received Message', event.data); + + if (event.data === 'PLAY') { + processCommand(['PLAY', '1-1', 'AS11_DPP_HD_EXAMPLE_1.MXF', 'SEEK', '200']) + paused = false + } + if (event.data === 'PAUSE') { + if (!paused) { + processCommand(['PAUSE', '1-1']) + paused = true + } else { + processCommand(['RESUME', '1-1']) + paused = false + } + } + if (event.data === 'STOP') { + processCommand(['STOP', '1-1']) + paused = false + } + + if (event.data.startsWith('ROTATION')) { + if (firstMessage) { + channel.anchor(1, ['0.5', '0.5']) + firstMessage = false + } + channel.rotation(1, [event.data.slice(9)]) + } + + if (event.data.startsWith('FILL')) { + if (firstMessage) { + channel.anchor(1, ['0.5', '0.5']) + firstMessage = false + } + channel.fill(1, ['0', '0', ...event.data.split(' ').slice(1) ]) + } + } + + this.consumers.set(info.id, { + ...info, + connectionManager: new WebRtcConnectionManager({ + createConnection: (id, baseOptions) => + new WebRTCConnection(id, { + beforeAnswer, + ...baseOptions + }), + generateId: uuidv4 + }) + }) + } + + destroySource(id: string) { + if (this.consumers.delete(id)) { + // TODO + } + } + + static singleton() { + if (!peerManagerSingleton) { + peerManagerSingleton = new PeerManager() + } + return peerManagerSingleton + } +} diff --git a/src/consumer/webrtcConsumer/types/wrtc.d.ts b/src/consumer/webrtcConsumer/types/wrtc.d.ts new file mode 100644 index 0000000..f9d717f --- /dev/null +++ b/src/consumer/webrtcConsumer/types/wrtc.d.ts @@ -0,0 +1,634 @@ +// Type definitions for WebRTC, copied to the "wrtc" module + nonstandard APIs +// Project: http://dev.w3.org/2011/webrtc/ +// Definitions by: Ken Smith +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +// import { WriteVResult } from "node:fs" +// import { WebRtcConnectionManager } from "../connections/webRTCConnectionManager" + +// Taken from http://dev.w3.org/2011/webrtc/editor/getusermedia.html +// version: W3C Editor's Draft 29 June 2015 + +type CustomWebRTCEventListener = + | ((event: Event & { candidate: RTCIceCandidate }) => void) + | (EventListenerObject & { + handleEvent(evt: Event & T): void + }) + | null + +declare module 'wrtc' { + interface ConstrainBooleanParameters { + exact?: boolean + ideal?: boolean + } + + interface NumberRange { + max?: number + min?: number + } + + interface ConstrainNumberRange extends NumberRange { + exact?: number + ideal?: number + } + + interface ConstrainStringParameters { + exact?: string | string[] + ideal?: string | string[] + } + + interface MediaStreamConstraints { + video?: boolean | MediaTrackConstraints + audio?: boolean | MediaTrackConstraints + } + + namespace W3C { + type LongRange = NumberRange + type DoubleRange = NumberRange + type ConstrainBoolean = boolean | ConstrainBooleanParameters + type ConstrainNumber = number | ConstrainNumberRange + type ConstrainLong = ConstrainNumber + type ConstrainDouble = ConstrainNumber + type ConstrainString = string | string[] | ConstrainStringParameters + } + + interface MediaTrackConstraints extends MediaTrackConstraintSet { + advanced?: MediaTrackConstraintSet[] + } + + interface MediaTrackConstraintSet { + width?: W3C.ConstrainLong + height?: W3C.ConstrainLong + aspectRatio?: W3C.ConstrainDouble + frameRate?: W3C.ConstrainDouble + facingMode?: W3C.ConstrainString + volume?: W3C.ConstrainDouble + sampleRate?: W3C.ConstrainLong + sampleSize?: W3C.ConstrainLong + echoCancellation?: W3C.ConstrainBoolean + latency?: W3C.ConstrainDouble + deviceId?: W3C.ConstrainString + groupId?: W3C.ConstrainString + } + + interface MediaTrackSupportedConstraints { + width?: boolean + height?: boolean + aspectRatio?: boolean + frameRate?: boolean + facingMode?: boolean + volume?: boolean + sampleRate?: boolean + sampleSize?: boolean + echoCancellation?: boolean + latency?: boolean + deviceId?: boolean + groupId?: boolean + } + + class MediaStream extends EventTarget { + //id: string; + //active: boolean; + + //onactive: EventListener; + //oninactive: EventListener; + //onaddtrack: (event: MediaStreamTrackEvent) => any; + //onremovetrack: (event: MediaStreamTrackEvent) => any; + constructor(options: { id: string }) + + clone(): MediaStream + stop(): void + + getAudioTracks(): MediaStreamTrack[] + getVideoTracks(): MediaStreamTrack[] + getTracks(): MediaStreamTrack[] + + getTrackById(trackId: string): MediaStreamTrack + + addTrack(track: MediaStreamTrack): void + removeTrack(track: MediaStreamTrack): void + } + + interface MediaStreamTrackEvent extends Event { + //track: MediaStreamTrack; + } + + interface MediaStreamTrack extends EventTarget { + //id: string; + //kind: string; + //label: string; + enabled: boolean + //muted: boolean; + //remote: boolean; + //readyState: MediaStreamTrackState; + + //onmute: EventListener; + //onunmute: EventListener; + //onended: EventListener; + //onoverconstrained: EventListener; + + clone(): MediaStreamTrack + + stop(): void + + getCapabilities(): MediaTrackCapabilities + getConstraints(): MediaTrackConstraints + getSettings(): MediaTrackSettings + applyConstraints(constraints: MediaTrackConstraints): Promise + } + + interface MediaTrackCapabilities { + //width: number | W3C.LongRange; + //height: number | W3C.LongRange; + //aspectRatio: number | W3C.DoubleRange; + //frameRate: number | W3C.DoubleRange; + //facingMode: string; + //volume: number | W3C.DoubleRange; + //sampleRate: number | W3C.LongRange; + //sampleSize: number | W3C.LongRange; + //echoCancellation: boolean[]; + latency?: W3C.DoubleRange + //deviceId: string; + //groupId: string; + } + + interface MediaTrackSettings { + //width: number; + //height: number; + //aspectRatio: number; + //frameRate: number; + //facingMode: string; + //volume: number; + //sampleRate: number; + //sampleSize: number; + //echoCancellation: boolean; + latency?: number + //deviceId: string; + //groupId: string; + } + + interface MediaStreamError { + //name: string; + //message: string; + //constraintName: string; + } + + interface NavigatorGetUserMedia { + ( + constraints: MediaStreamConstraints, + successCallback: (stream: MediaStream) => void, + errorCallback: (error: MediaStreamError) => void + ): void + } + + interface MediaDevices { + getSupportedConstraints(): MediaTrackSupportedConstraints + + getUserMedia(constraints: MediaStreamConstraints): Promise + enumerateDevices(): Promise + } + + interface MediaDeviceInfo { + //label: string; + //deviceId: string; + //kind: string; + //groupId: string; + } + + // Type definitions for WebRTC 2016-09-13 + // Project: https://www.w3.org/TR/webrtc/ + // Definitions by: Danilo Bargen + // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + // + // W3 Spec: https://www.w3.org/TR/webrtc/ + // + // Note: Commented out definitions clash with definitions in lib.es6.d.ts. I + // still kept them in here though, as sometimes they're more specific than the + // ES6 library ones. + + /// + + // https://www.w3.org/TR/webrtc/#idl-def-rtcofferansweroptions + interface RTCOfferAnswerOptions { + voiceActivityDetection?: boolean // default = true + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcofferoptions + interface RTCOfferOptions extends RTCOfferAnswerOptions { + iceRestart?: boolean // default = false + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcansweroptions + interface RTCAnswerOptions extends RTCOfferAnswerOptions {} + + // https://www.w3.org/TR/webrtc/#idl-def-rtciceserver + interface RTCIceServer { + //urls: string | string[]; + credentialType?: RTCIceCredentialType // default = 'password' + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtciceparameters + interface RTCIceParameters { + //usernameFragment: string; + //password: string; + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcicetransport + type IceTransportEventHandler = ((this: RTCIceTransport, ev: Event) => any) | null + interface RTCIceTransport { + //readonly role: RTCIceRole; + //readonly component: RTCIceComponent; + //readonly state: RTCIceTransportState; + readonly gatheringState: RTCIceGatheringState + getLocalCandidates(): RTCIceCandidate[] + getRemoteCandidates(): RTCIceCandidate[] + getSelectedCandidatePair(): RTCIceCandidatePair | null + getLocalParameters(): RTCIceParameters | null + getRemoteParameters(): RTCIceParameters | null + onstatechange: IceTransportEventHandler + ongatheringstatechange: IceTransportEventHandler + onselectedcandidatepairchange: IceTransportEventHandler + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcdtlstransport + type DtlsTransportEventHandler = ((this: RTCDtlsTransport, ev: Event) => any) | null + interface RTCDtlsTransport { + readonly transport: RTCIceTransport + //readonly state: RTCDtlsTransportState; + getRemoteCertificates(): ArrayBuffer[] + onstatechange: DtlsTransportEventHandler + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcrtpcodeccapability + interface RTCRtpCodecCapability { + mimeType: string + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcrtpheaderextensioncapability + interface RTCRtpHeaderExtensionCapability { + uri?: string + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcrtpcapabilities + interface RTCRtpCapabilities { + //codecs: RTCRtpCodecCapability[]; + //headerExtensions: RTCRtpHeaderExtensionCapability[]; + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcrtprtxparameters + interface RTCRtpRtxParameters { + //ssrc: number; + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcrtpfecparameters + interface RTCRtpFecParameters { + //ssrc: number; + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcrtpencodingparameters + interface RTCRtpEncodingParameters { + //ssrc: number; + //rtx: RTCRtpRtxParameters; + //fec: RTCRtpFecParameters; + dtx?: RTCDtxStatus + //active: boolean; + //priority: RTCPriorityType; + //maxBitrate: number; + rid: string + scaleResolutionDownBy?: number // default = 1 + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcrtpheaderextensionparameters + interface RTCRtpHeaderExtensionParameters { + //uri: string; + //id: number; + encrypted?: boolean + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcrtcpparameters + interface RTCRtcpParameters { + //cname: string; + //reducedSize: boolean; + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcrtpcodecparameters + interface RTCRtpCodecParameters { + //payloadType: number; + mimeType: string + //clockRate: number; + channels?: number // default = 1 + sdpFmtpLine?: string + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcrtpparameters + interface RTCRtpParameters { + transactionId: string + //encodings: RTCRtpEncodingParameters[]; + //headerExtensions: RTCRtpHeaderExtensionParameters[]; + //rtcp: RTCRtcpParameters; + //codecs: RTCRtpCodecParameters[]; + degradationPreference?: RTCDegradationPreference // default = 'balanced' + } + + // https://www.w3.org/TR/webrtc/#dom-rtcrtpcontributingsource + interface RTCRtpContributingSource { + //readonly timestamp: number; + source: number + //readonly audioLevel: number | null; + readonly voiceActivityFlag?: boolean + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcrtpcapabilities + interface RTCRtcCapabilities { + codecs: RTCRtpCodecCapability[] + headerExtensions: RTCRtpHeaderExtensionCapability[] + } + + // https://www.w3.org/TR/webrtc/#dom-rtcrtpsender + interface RTCRtpSender { + //readonly track?: MediaStreamTrack; + //readonly transport?: RTCDtlsTransport; + //readonly rtcpTransport?: RTCDtlsTransport; + setParameters(parameters?: RTCRtpParameters): Promise + getParameters(): RTCRtpParameters + replaceTrack(withTrack: MediaStreamTrack): Promise + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcrtpreceiver + interface RTCRtpReceiver { + //readonly track?: MediaStreamTrack; + //readonly transport?: RTCDtlsTransport; + //readonly rtcpTransport?: RTCDtlsTransport; + getParameters(): RTCRtpParameters + getContributingSources(): RTCRtpContributingSource[] + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcrtptransceiver + interface RTCRtpTransceiver { + readonly mid: string | null + readonly sender: RTCRtpSender + readonly receiver: RTCRtpReceiver + readonly stopped: boolean + direction: RTCRtpTransceiverDirection + setDirection(direction: RTCRtpTransceiverDirection): void + stop(): void + setCodecPreferences(codecs: RTCRtpCodecCapability[]): void + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcrtptransceiverinit + interface RTCRtpTransceiverInit { + direction?: RTCRtpTransceiverDirection // default = 'sendrecv' + streams?: MediaStream[] + sendEncodings?: RTCRtpEncodingParameters[] + } + + // https://www.w3.org/TR/webrtc/#dom-rtccertificate + interface RTCCertificate { + readonly expires: number + getAlgorithm(): string + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcconfiguration + interface RTCConfiguration { + iceServers?: RTCIceServer[] + iceTransportPolicy?: RTCIceTransportPolicy // default = 'all' + bundlePolicy?: RTCBundlePolicy // default = 'balanced' + rtcpMuxPolicy?: RTCRtcpMuxPolicy // default = 'require' + peerIdentity?: string // default = null + certificates?: RTCCertificate[] + iceCandidatePoolSize?: number // default = 0 + } + + // Compatibility for older definitions on DefinitelyTyped. + type RTCPeerConnectionConfig = RTCConfiguration + + // https://www.w3.org/TR/webrtc/#idl-def-rtcsctptransport + interface RTCSctpTransport { + readonly transport: RTCDtlsTransport + readonly maxMessageSize: number + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcdatachannelinit + interface RTCDataChannelInit { + ordered?: boolean // default = true + maxPacketLifeTime?: number + maxRetransmits?: number + protocol?: string // default = '' + negotiated?: boolean // default = false + id?: number + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcdatachannel + type DataChannelEventHandler = ((this: RTCDataChannel, ev: E) => any) | null + interface RTCDataChannel extends EventTarget { + readonly label: string + readonly ordered: boolean + readonly maxPacketLifeTime: number | null + readonly maxRetransmits: number | null + readonly protocol: string + readonly negotiated: boolean + readonly id: number | null + readonly readyState: RTCDataChannelState + readonly bufferedAmount: number + bufferedAmountLowThreshold: number + binaryType: string + + close(): void + send(data: string | Blob | ArrayBuffer | ArrayBufferView): void + + onopen: DataChannelEventHandler + onmessage: DataChannelEventHandler + onbufferedamountlow: DataChannelEventHandler + onerror: DataChannelEventHandler + onclose: DataChannelEventHandler + } + + // https://www.w3.org/TR/webrtc/#h-rtctrackevent + interface RTCTrackEvent extends Event { + readonly receiver: RTCRtpReceiver + readonly track: MediaStreamTrack + readonly streams: ReadonlyArray + readonly transceiver: RTCRtpTransceiver + } + + // https://www.w3.org/TR/webrtc/#h-rtcpeerconnectioniceevent + interface RTCPeerConnectionIceEvent extends Event { + readonly url: string | null + } + + // https://www.w3.org/TR/webrtc/#h-rtcpeerconnectioniceerrorevent + interface RTCPeerConnectionIceErrorEvent extends Event { + readonly hostCandidate: string + readonly url: string + readonly errorCode: number + readonly errorText: string + } + + // https://www.w3.org/TR/webrtc/#h-rtcdatachannelevent + interface RTCDataChannelEvent extends Event { + readonly channel: RTCDataChannel + } + + namespace nonstandard { + class RTCVideoSink extends EventTarget { + constructor(track: MediaStreamTrack) + stop(): void + readonly stopped: boolean + addEventListener(type: 'frame', handler: EventHandlerNonNull): void + removeEventListener(type: 'frame', handler: EventHandlerNonNull): void + } + + class RTCVideoSource { + constructor() + readonly isScreencast: boolean + readonly needsDenoising?: boolean + createTrack(): MediaStreamTrack + onFrame(frame: RTCVideoFrame): void + } + + class RTCAudioSink extends EventTarget { + constructor(track: MediaStreamTrack) + stop(): void + readonly stopped: boolean + addEventListener(type: 'data', handler: EventHandlerNonNull): void + removeEventListener(type: 'data', handler: EventHandlerNonNull): void + } + + class RTCAudioSource { + constructor() + createTrack(): MediaStreamTrack + onData(frame: RTCAudioData): void + } + + function i420ToRgba(i420Frame: RTCVideoFrame, rgbaFrame: RTCVideoFrame): void + function rgbaToI420(rgbaFrame: RTCVideoFrame, i420Frame: RTCVideoFrame): void + } + + interface RTCVideoFrame { + width: number + height: number + data: Uint8ClampedArray + rotation?: 0 | 90 | 180 | 270 + } + + interface RTCAudioData { + samples: ArrayBuffer + sampleRate: number + // bitsPerSample default is 16 + bitsPerSample?: number + // channelCount default is 1 + channelCount?: number + // number of frames + numberOfFrames: number + } + + // https://www.w3.org/TR/webrtc/#idl-def-rtcpeerconnection + type PeerConnectionEventHandler = + | ((this: RTCPeerConnection, ev: E) => any) + | null + class RTCPeerConnection extends EventTarget { + constructor(configuration?: RTCConfiguration, options?: any) + static readonly defaultIceServers: RTCIceServer[] + + // Extension: https://www.w3.org/TR/webrtc/#sec.cert-mgmt + static generateCertificate(keygenAlgorithm: string): Promise + + createOffer(options?: RTCOfferOptions): Promise + createAnswer(options?: RTCAnswerOptions): Promise + + setLocalDescription(description: RTCSessionDescriptionInit): Promise + readonly localDescription: RTCSessionDescription | null + readonly currentLocalDescription: RTCSessionDescription | null + readonly pendingLocalDescription: RTCSessionDescription | null + + setRemoteDescription(description: RTCSessionDescriptionInit): Promise + readonly remoteDescription: RTCSessionDescription | null + readonly currentRemoteDescription: RTCSessionDescription | null + readonly pendingRemoteDescription: RTCSessionDescription | null + + addIceCandidate(candidate?: RTCIceCandidateInit | RTCIceCandidate): Promise + + readonly signalingState: RTCSignalingState + readonly connectionState: RTCPeerConnectionState + + readonly iceConnectionState: RTCIceConnectionState + readonly iceGatheringState: RTCIceGatheringState + + getConfiguration(): RTCConfiguration + setConfiguration(configuration: RTCConfiguration): void + close(): void + + onicecandidateerror: PeerConnectionEventHandler + onconnectionstatechange: PeerConnectionEventHandler + + // Extension: https://www.w3.org/TR/webrtc/#h-rtcpeerconnection-interface-extensions + getSenders(): RTCRtpSender[] + getReceivers(): RTCRtpReceiver[] + getTransceivers(): RTCRtpTransceiver[] + addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender + removeTrack(sender: RTCRtpSender): void + addTransceiver( + trackOrKind: MediaStreamTrack | string, + init?: RTCRtpTransceiverInit + ): RTCRtpTransceiver + ontrack: PeerConnectionEventHandler + + // Extension: https://www.w3.org/TR/webrtc/#h-rtcpeerconnection-interface-extensions-1 + readonly sctp: RTCSctpTransport | null + createDataChannel(label: string | null, dataChannelDict?: RTCDataChannelInit): RTCDataChannel + ondatachannel: PeerConnectionEventHandler + + // Extension: https://www.w3.org/TR/webrtc/#h-rtcpeerconnection-interface-extensions-2 + getStats(selector?: MediaStreamTrack | null): Promise + + // Extension: https://www.w3.org/TR/webrtc/#legacy-interface-extensions + // Deprecated! + createOffer( + successCallback: RTCSessionDescriptionCallback, + failureCallback: RTCPeerConnectionErrorCallback, + options?: RTCOfferOptions + ): Promise + setLocalDescription( + description: RTCSessionDescriptionInit, + successCallback: () => void, + failureCallback: RTCPeerConnectionErrorCallback + ): Promise + createAnswer( + successCallback: RTCSessionDescriptionCallback, + failureCallback: RTCPeerConnectionErrorCallback + ): Promise + setRemoteDescription( + description: RTCSessionDescriptionInit, + successCallback: () => void, + failureCallback: RTCPeerConnectionErrorCallback + ): Promise + addIceCandidate( + candidate: RTCIceCandidateInit | RTCIceCandidate, + successCallback: () => void, + failureCallback: RTCPeerConnectionErrorCallback + ): Promise + getStats( + selector: MediaStreamTrack | null, + successCallback: RTCStatsCallback, + failureCallback: RTCPeerConnectionErrorCallback + ): Promise + + addEventListener( + type: 'icecandidate', + listener: CustomWebRTCEventListener<{ candidate: RTCIceCandidate }> + ): void + removeEventListener( + type: 'icecandidate', + listener: CustomWebRTCEventListener<{ candidate: RTCIceCandidate }> + ): void + + addEventListener( + type: 'iceconnectionstatechange', + listener: CustomWebRTCEventListener<{}> + ): void + removeEventListener( + type: 'iceconnectionstatechange', + listener: CustomWebRTCEventListener<{}> + ): void + } +} diff --git a/src/index.ts b/src/index.ts index 6ecc05a..89f60d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,7 @@ import { ConsumerConfig, VideoFormats } from './config' import readline from 'readline' import { Osc, OscConfig } from './osc/osc' import { Heads, HeadsConfig } from './heads/heads' +import fs from 'fs' class Config { private readonly videoFormats: VideoFormats @@ -45,8 +46,9 @@ class Config { { format: this.videoFormats.get('1080i5000'), devices: [ - { name: 'decklink', deviceIndex: 1, embeddedAudio: true } - // { name: 'screen', deviceIndex: 0 } + // { name: 'decklink', deviceIndex: 1, embeddedAudio: true }, + // { name: 'screen', deviceIndex: 0 }, + { name: 'webrtc', deviceIndex: 1 } ] }, { @@ -122,6 +124,10 @@ rl.on('SIGINT', () => { process.kill(process.pid, 'SIGTERM') }) +rl.on('close', () => { + process.kill(process.pid, 'SIGTERM') +}) + console.log('\nWelcome to Phaneron\n') const commands = new Commands() @@ -171,5 +177,15 @@ initialiseOpenCL() }) .then(() => start(commands)) .then(console.log, console.error) + .then(async () => { + if (fs.existsSync('startupAMCPCommands')) { + const startupFile = fs.readFileSync('startupAMCPCommands').toString().split('\n') + for (const command of startupFile) { + if (!command.match('^#')) { + await processCommand(command.match(/"[^"]+"|""|\S+/g)) + } + } + } + }) .then(() => rl.prompt()) .catch(console.error) diff --git a/src/producer/ffmpegProducer.ts b/src/producer/ffmpegProducer.ts index b7db8b2..db5d7fc 100644 --- a/src/producer/ffmpegProducer.ts +++ b/src/producer/ffmpegProducer.ts @@ -214,7 +214,7 @@ export class FFmpegProducer implements Producer { if (vidStream) { width = vidStream.codecpar.width height = vidStream.codecpar.height - squareWidth = (width * vidStream.sample_aspect_ratio[0]) / vidStream.sample_aspect_ratio[1] + squareWidth = (width * vidStream.codecpar.sample_aspect_ratio[0]) / vidStream.codecpar.sample_aspect_ratio[1] squareHeight = height vidTimescale = vidStream.time_base[1] * (progressive ? 1 : 2) vidDuration = vidStream.time_base[0] @@ -285,6 +285,7 @@ export class FFmpegProducer implements Producer { const fieldOrder = vidStream.codecpar.field_order progressive = fieldOrder === 'progressive' + // console.log('Is progressive', progressive, vidStream.time_base, filterOutputFormat, fieldOrder) const tff = fieldOrder === 'unknown' || fieldOrder.split(', ', 2)[1] === 'top displayed first' const yadifMode = progressive ? 'send_frame' : 'send_field' yadif = new Yadif( diff --git a/src/producer/webrtcProducer/webRTCProducer.ts b/src/producer/webrtcProducer/webRTCProducer.ts new file mode 100644 index 0000000..692377a --- /dev/null +++ b/src/producer/webrtcProducer/webRTCProducer.ts @@ -0,0 +1,492 @@ +/* + Phaneron - Clustered, accelerated and cloud-fit video server, pre-assembled and in kit form. + Copyright (C) 2020 Streampunk Media Ltd. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + https://www.streampunk.media/ mailto:furnace@streampunk.media + 14 Ormiscaig, Aultbea, Achnasheen, IV22 2JJ U.K. +*/ + +import { ProducerFactory, Producer, InvalidProducerError } from '../producer' +import { clContext as nodenCLContext, OpenCLBuffer } from 'nodencl' +import { + Demuxer, + demuxer, + Decoder, + decoder, + Filterer, + filterer, + Stream, + Packet, + Frame, + frame +} from 'beamcoder' +import redio, { RedioPipe, nil, end, isValue, RedioEnd, Generator, Valve } from 'redioactive' +import { ClJobs } from '../../clJobQueue' +import { LoadParams } from '../../chanLayer' +import { VideoFormat, VideoFormats } from '../../config' +import { ToRGBA } from '../../process/io' +import { Reader as yuv422p10Reader } from '../../process/yuv422p10' +import { Reader as yuv422p8Reader } from '../../process/yuv422p8' +import { Reader as v210Reader } from '../../process/v210' +import { Reader as rgba8Reader } from '../../process/rgba8' +import { Reader as bgra8Reader } from '../../process/bgra8' +import Yadif from '../../process/yadif' +import { PackImpl } from '../../process/packer' +import { Mixer, AudioMixFrame } from '../mixer' + +interface AudioChannel { + name: string + frames: Frame[] +} + +export class WebRTCProducer implements Producer { + private readonly sourceID: string + private readonly loadParams: LoadParams + private readonly clContext: nodenCLContext + private readonly clJobs: ClJobs + private readonly consumerFormat: VideoFormat + private readonly mixer: Mixer + private demuxer: Demuxer | null = null + private format: VideoFormat + private audSource: RedioPipe | undefined + private vidSource: RedioPipe | undefined + private running = true + private paused = false + + constructor(id: number, loadParams: LoadParams, context: nodenCLContext, clJobs: ClJobs, consumerFormat: VideoFormat) { + this.sourceID = `P${id} FFmpeg ${loadParams.url} L${loadParams.layer}` + this.loadParams = loadParams + this.clContext = context + this.clJobs = clJobs + this.format = new VideoFormats().get('1080p5000') // default + this.consumerFormat = consumerFormat + this.mixer = new Mixer(this.clContext, this.consumerFormat, this.clJobs) + } + + async initialise(): Promise { + try { + this.demuxer = await demuxer(this.loadParams.url) + } catch (err) { + console.log(err) + throw new InvalidProducerError(err) + } + if (this.loadParams.seek) await this.demuxer.seek({ time: this.loadParams.seek }) + + const audioStreams: Stream[] = [] + const videoStreams: Stream[] = [] + const audioIndexes: number[] = [] + const videoIndexes: number[] = [] + const decoders: Map = new Map() + const numAudChannels = 8 + const numVidChannels = 1 + this.demuxer.streams.forEach((s) => { + if (s.codecpar.codec_type === 'audio' && audioStreams.length < numAudChannels) { + s.discard = 'default' + audioStreams.push(s) + audioIndexes.push(s.index) + decoders.set(s.index, decoder({ demuxer: this.demuxer as Demuxer, stream_index: s.index })) + } else if (s.codecpar.codec_type === 'video' && videoStreams.length < numVidChannels) { + s.discard = 'default' + videoStreams.push(s) + videoIndexes.push(s.index) + decoders.set(s.index, decoder({ demuxer: this.demuxer as Demuxer, stream_index: s.index })) + } else { + s.discard = 'all' + } + }) + + let silentFrame: Frame | null = null + let audFilterer: Filterer | null = null + const audLayout = `${numAudChannels}c` + // If the file has multiple audio streams each marked as mono then set the channel layout to give them a default position + const allMono = audioStreams.every((s) => s.codecpar.channel_layout === 'mono') + const audChanNames = ['FL', 'FR', 'FC', 'SL', 'SR', 'LFE', 'BL', 'BR'] + const audStream = audioStreams[0] + if (audStream) { + let inStr = '' + const inParams = audioStreams.map((_s, i) => { + inStr += `[in${i}:a]` + return { + name: `in${i}:a`, + timeBase: audStream.time_base, + sampleRate: audStream.codecpar.sample_rate, + sampleFormat: audStream.codecpar.format, + channelLayout: allMono ? audChanNames[i] : audStream.codecpar.channel_layout + } + }) + + audFilterer = await filterer({ + filterType: 'audio', + inputParams: inParams, + outputParams: [ + { + name: 'out0:a', + sampleRate: 48000, + sampleFormat: 'flt', + channelLayout: audLayout + } + ], + filterSpec: `${inStr} amerge=inputs=${audioStreams.length}, asetnsamples=n=1024:p=1 [out0:a]` + }) + } else { + silentFrame = frame({ + nb_samples: 1024, + format: 's32', + pts: 0, + sample_rate: 48000, + channels: numAudChannels, + channel_layout: audLayout, + data: [Buffer.alloc(1024 * numAudChannels * 4)] + }) + + audFilterer = await filterer({ + filterType: 'audio', + inputParams: [ + { + name: 'in0:a', + timeBase: [1, 48000], + sampleRate: 48000, + sampleFormat: 's32', + channelLayout: audLayout + } + ], + outputParams: [ + { + name: 'out0:a', + sampleRate: 48000, + sampleFormat: 'flt', + channelLayout: audLayout + } + ], + filterSpec: '[in0:a] asetpts=N/SR/TB [out0:a]' + }) + } + // console.log('\nFFmpeg producer audio:\n', audFilterer.graph.dump()) + + const vidStream = videoStreams[0] + const width = vidStream.codecpar.width + const height = vidStream.codecpar.height + + let toRGBA: ToRGBA | null = null + let filterOutputFormat = vidStream.codecpar.format + let readImpl: PackImpl + switch (vidStream.codecpar.format) { + case 'yuv422p': + console.log('Using native yuv422p8 loader') + readImpl = new yuv422p8Reader(width, height) + break + case 'yuv422p10le': + console.log('Using native yuv422p10 loader') + readImpl = new yuv422p10Reader(width, height) + break + case 'v210': + console.log('Using native v210 loader') + readImpl = new v210Reader(width, height) + break + case 'rgba': + console.log('Using native rgba8 loader') + readImpl = new rgba8Reader(width, height) + break + case 'bgra': + console.log('Using native bgra8 loader') + readImpl = new bgra8Reader(width, height) + break + default: + if (vidStream.codecpar.format.includes('yuv')) { + console.log(`Non-native loader for ${vidStream.codecpar.format} - using yuv422p10`) + filterOutputFormat = 'yuv422p10le' + readImpl = new yuv422p10Reader(width, height) + } else if (vidStream.codecpar.format.includes('rgb')) { + console.log(`Non-native loader for ${vidStream.codecpar.format} - using rgba8`) + filterOutputFormat = 'rgba' + readImpl = new rgba8Reader(width, height) + } else + throw new Error( + `Unsupported video format '${vidStream.codecpar.format}' from FFmpeg decoder` + ) + } + toRGBA = new ToRGBA(this.clContext, '709', '709', readImpl, this.clJobs) + await toRGBA.init() + const chanTb = [this.consumerFormat.duration, this.consumerFormat.timescale] + const vidFilterer = await filterer({ + filterType: 'video', + inputParams: [ + { + timeBase: vidStream.time_base, + width: width, + height: height, + pixelFormat: vidStream.codecpar.format, + pixelAspect: vidStream.codecpar.sample_aspect_ratio + } + ], + outputParams: [ + { + pixelFormat: filterOutputFormat + } + ], + filterSpec: `fps=fps=${chanTb[1] / 2}/${chanTb[0]}` + }) + // console.log('\nFFmpeg producer video:\n', vidFilterer.graph.dump()) + + let yadif: Yadif | null = null + const fieldOrder = vidStream.codecpar.field_order + const progressive = fieldOrder === 'progressive' + const tff = fieldOrder === 'unknown' || fieldOrder.split(', ', 2)[1] === 'top displayed first' + const yadifMode = progressive ? 'send_frame' : 'send_field' + yadif = new Yadif( + this.clContext, + this.clJobs, + width, + height, + { mode: yadifMode, tff: tff }, + !progressive + ) + await yadif.init() + + const demux: Generator = async () => { + let result: Packet[] | RedioEnd = end + let doneSet = false + + let lastAudTimestamp: number | undefined = undefined + let lastVidTimestamp: number | undefined = undefined + const packets: Packet[] = [] + let doBreak = false + + if (this.demuxer && this.running) { + do { + const packet = await this.demuxer.read() + if (packet) { + if (audioIndexes.includes(packet.stream_index)) { + if (!lastAudTimestamp) lastAudTimestamp = packet.pts + else if (packet.pts !== lastAudTimestamp) doBreak = true + packets.push(packet) + } else if (videoIndexes.includes(packet.stream_index)) { + if (!lastVidTimestamp) lastVidTimestamp = packet.pts + else if (packet.pts !== lastVidTimestamp) doBreak = true + packets.push(packet) + } + + if (doBreak || packets.length === audioStreams.length + videoStreams.length) { + if (doBreak) + console.log( + `Timestamp mismatch - sending ${packets.length} packets, ${ + audioStreams.length + videoStreams.length + } expected` + ) + doneSet = true + result = packets + } + } else { + doneSet = true + result = end + } + } while (!doneSet) + } else this.demuxer = null + + return result + } + + const audPacketFilter: Valve = async (packets) => { + if (isValue(packets)) { + return packets.filter((p) => audioIndexes.includes(p.stream_index)) + } else { + return packets + } + } + + const audDecode: Valve = async (packets) => { + if (isValue(packets)) { + const frames = await Promise.all( + packets.map((p) => (decoders.get(p.stream_index) as Decoder).decode(p)) + ) + return frames.map((f, i) => ({ name: `in${i}:a`, frames: f.frames })) + } else { + return packets + } + } + + const audFilter: Valve = async ( + frames + ) => { + if (isValue(frames) && audFilterer) { + const ff = await audFilterer.filter(frames) + if (!ff[0]) return nil + if (ff.reduce((acc, f) => acc && f.frames && f.frames.length > 0, true)) { + return { frames: ff.map((f) => f.frames), mute: false } + } else return nil + } else { + return frames as RedioEnd + } + } + + const silence: Generator = async () => [ + { name: 'in0:a', frames: [silentFrame] } + ] + + const vidPacketFilter: Valve = async (packets) => { + if (isValue(packets)) { + return packets.filter((p) => videoIndexes.includes(p.stream_index)) + } else { + return packets + } + } + + const vidDecode: Valve = async (packets) => { + if (isValue(packets)) { + if (!packets[0]) return nil + const frm = await (decoders.get(packets[0].stream_index) as Decoder).decode(packets[0]) + return frm.frames.length > 0 ? frm.frames : nil + } else { + return packets + } + } + + const vidFilter: Valve = async (decFrames) => { + if (isValue(decFrames)) { + const ff = await vidFilterer.filter([decFrames]) + if (!ff[0]) return nil + return ff[0].frames.length > 0 ? ff[0].frames : nil + } else { + return decFrames + } + } + + const vidLoader: Valve = async (frame) => { + if (isValue(frame)) { + const convert = toRGBA as ToRGBA + const clSources = await convert.createSources() + clSources.forEach((s) => (s.timestamp = progressive ? frame.pts : frame.pts * 2)) + await convert.loadFrame(frame.data, clSources, this.clContext.queue.load) + await this.clContext.waitFinish(this.clContext.queue.load) + return clSources + } else { + return frame + } + } + + const vidProcess: Valve = async ( + clSources + ) => { + if (isValue(clSources)) { + const convert = toRGBA as ToRGBA + const clDest = await convert.createDest({ width: width, height: height }) + clDest.timestamp = clSources[0].timestamp + convert.processFrame(this.sourceID, clSources, clDest) + return clDest + } else { + toRGBA = null + return clSources + } + } + + const vidDeint: Valve = async (frame) => { + if (isValue(frame)) { + const yadifDests: OpenCLBuffer[] = [] + await yadif?.processFrame(frame, yadifDests, this.sourceID) + return yadifDests.length > 1 ? yadifDests : nil + } else { + yadif?.release() + yadif = null + return frame + } + } + + this.format = { + name: 'ffmpeg', + fields: 1, + width: width, + height: height, + squareWidth: (width * vidStream.sample_aspect_ratio[0]) / vidStream.sample_aspect_ratio[1], + squareHeight: height, + timescale: vidStream.time_base[1] * (progressive ? 1 : 2), + duration: vidStream.time_base[0], + audioSampleRate: 48000, + audioChannels: numAudChannels + } + + const ffPackets = redio(demux, { bufferSizeMax: 10 }) + + let audSrc: RedioPipe | undefined + if (audioStreams.length) { + audSrc = ffPackets + .fork() + .valve(audPacketFilter) + .valve(audDecode) + .valve(audFilter, { oneToMany: true }) + } else { + // eslint-disable-next-line prettier/prettier + audSrc = redio(silence, { bufferSizeMax: 10 }).valve(audFilter, { oneToMany: true }) + } + this.audSource = audSrc.pause((frame) => { + if (this.paused && isValue(frame)) (frame as AudioMixFrame).mute = true + return this.paused + }) + + this.vidSource = ffPackets + .fork() + .valve(vidPacketFilter) + .valve(vidDecode, { oneToMany: true }) + .valve(vidFilter, { oneToMany: true }) + .valve(vidLoader, { bufferSizeMax: 1 }) + .valve(vidProcess) + .valve(vidDeint, { oneToMany: true }) + .pause((frame) => { + if (this.paused && isValue(frame)) (frame as OpenCLBuffer).addRef() + return this.paused + }) + + console.log(`Created WebRTC producer for stream ${this.loadParams.url}`) + } + + getSourceID(): string { + return this.sourceID + } + + getFormat(): VideoFormat { + return this.format + } + + getSourceAudio(): RedioPipe | undefined { + return this.audSource + } + + getSourceVideo(): RedioPipe | undefined { + return this.vidSource + } + + getMixer(): Mixer { + return this.mixer + } + + setPaused(pause: boolean): void { + this.paused = pause + } + + release(): void { + this.running = false + } +} + +export class WebRTCProducerFactory implements ProducerFactory { + private clContext: nodenCLContext + + constructor(clContext: nodenCLContext) { + this.clContext = clContext + } + + createProducer(id: number, loadParams: LoadParams, clJobs: ClJobs, consumerFormat: VideoFormat): WebRTCProducer { + return new WebRTCProducer(id, loadParams, this.clContext, clJobs, consumerFormat) + } +} diff --git a/static/demo-camera.html b/static/demo-camera.html new file mode 100644 index 0000000..08a614d --- /dev/null +++ b/static/demo-camera.html @@ -0,0 +1,78 @@ + + + + + + + + +

Phaneron - camera input demo

+ +
+ +
+ +
+ + + + + +
+ +
+ Codec selection: +
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/static/demo-camera.js b/static/demo-camera.js new file mode 100644 index 0000000..5e731eb --- /dev/null +++ b/static/demo-camera.js @@ -0,0 +1,179 @@ +let startTime; +const connectButton = document.getElementById('connectButton'); +const startButton = document.getElementById('startButton'); +const muteButton = document.getElementById('muteButton'); +const hangupButton = document.getElementById('hangupButton'); + +connectButton.addEventListener('click', async () => { + connectButton.disabled = true; + await startStream(); +}) +startButton.addEventListener('click', startLocal); +muteButton.addEventListener('click', handleMute); +hangupButton.addEventListener('click', hangup); +muteButton.disabled = true; +connectButton.disabled = true; +hangupButton.disabled = true; + +const localVideo = document.getElementById('localVideo'); +let localStream; + +const host = `http://${window.location.hostname}:3002` + +localVideo.addEventListener('loadedmetadata', function() { + console.log(`Local video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); +}); +localVideo.addEventListener('resize', () => { + console.log(`Local video size changed to ${localVideo.videoWidth}x${localVideo.videoHeight}`); + // We'll use the first onsize callback as an indication that video has started + // playing out. + if (startTime) { + const elapsedTime = window.performance.now() - startTime; + console.log('Setup time: ' + elapsedTime.toFixed(3) + 'ms'); + startTime = null; + } +}); + +let peerConnection = null; +let dataChannel = null; +const offerOptions = { + offerToReceiveAudio: 0, + offerToReceiveVideo: 0 +}; + +async function startLocal() { + console.log('Requesting local stream'); + startButton.disabled = true; + try { + const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: { width: 1920, height: 1080 }}); + console.log('Received local stream'); + localVideo.srcObject = stream; + localStream = stream; + connectButton.disabled = false; + muteButton.disabled = false; + localStream.getAudioTracks()[0].enabled = false; + } catch (e) { + alert(`getUserMedia() error: ${e.name}`); + } +} + +function handleMute() { + if (!localStream) return; + if (localStream.getAudioTracks()[0].enabled === true) { + localStream.getAudioTracks()[0].enabled = false; + muteButton.innerHTML = 'Un-mute'; + } else { + localStream.getAudioTracks()[0].enabled = true; + muteButton.innerHTML = 'Mute'; + } +} + +async function startStream(streamId) { + connectButton.disabled = true; + hangupButton.disabled = false; + if (peerConnection) { + stopStream() + } + // if (streamId === undefined) { + // const list = await listStreams(host) + // if (!list || list.length === 0) throw new Error('No streams') + // streamId = list[0].streamId + // } + const videoTracks = localStream.getVideoTracks(); + const audioTracks = localStream.getAudioTracks(); + if (videoTracks.length > 0) { + console.log(`Using video device: ${videoTracks[0].label}`); + } + if (audioTracks.length > 0) { + console.log(`Using audio device: ${audioTracks[0].label}`); + } + + peerConnection = new RTCPeerConnection({}) + // dataChannel = peerConnection.createDataChannel('Phaneron channel') + peerConnection.addEventListener('icecandidate', e => onIceCandidate(peerConnection, e)) + // peerConnection.addEventListener('track', gotRemoteStream) + // dataChannel.addEventListener('open', event => { + // console.log('data channel open') + // }) + + localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream)); + console.log('Added local stream to peer connection'); + + const offer = await peerConnection.createOffer(offerOptions) + await peerConnection.setLocalDescription(offer) + console.log('Sending offer:', offer) + const offerResponse = await fetch(`${host}/streams/${streamId}/connections`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(offer) + }) + console.log('Response is:', offerResponse) + const remoteOffer = await offerResponse.json() + console.log(remoteOffer) + await peerConnection.setRemoteDescription(remoteOffer) + // console.log(peerConnection.getRemoteStreams()) + setTimeout(async () => { + const stats = await peerConnection.getStats(); + stats.forEach(stat => { + if (!(stat.type === 'outbound-rtp' && stat.kind === 'video' || stat.kind === 'audio')) { + return; + } + const codec = stats.get(stat.codecId); + if (codec && stat.kind === 'video') { + document.getElementById('videoCodec').innerText = 'Video codec: ' + codec.mimeType + + ' ' + (codec.sdpFmtpLine ? codec.sdpFmtpLine + ' ' : '') + + ', payloadType=' + codec.payloadType + '.'; + } + if (codec && stat.kind === 'audio') { + document.getElementById('audioCodec').innerText = 'Audio codec: ' + codec.mimeType + + ' ' + (codec.sdpFmtpLine ? codec.sdpFmtpLine + ' ' : '') + + ', payloadType=' + codec.payloadType + ', clockRate=' + codec.clockRate + + ', channels=' + codec.channels + '.'; + } + + }); + }, 1000); + +} + +function stopStream() { + peerConnection.close() + peerConnection = null +} + +async function listStreams () { + const rawConsumersList = await fetch(`${host}/streams`, { + method: 'GET' + }); + const consumersList = await rawConsumersList.json(); + + return consumersList +} + +async function onIceCandidate(pc, event) { + console.log('onIceCandidate:', event) + // try { + // await (getOtherPc(pc).addIceCandidate(event.candidate)); + // onAddIceCandidateSuccess(pc); + // } catch (e) { + // onAddIceCandidateError(pc, e); + // } + // console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); +} + +// function gotRemoteStream(e) { +// if (remoteVideo.srcObject !== e.streams[0]) { +// remoteVideo.srcObject = e.streams[0]; +// console.log('pc2 received remote stream'); +// } +// } + +function hangup() { + console.log('Ending call') + peerConnection.close(); + peerConnection = null; + connectButton.disabled = false; + hangupButton.disabled = true; +} \ No newline at end of file diff --git a/static/demo-control.html b/static/demo-control.html new file mode 100644 index 0000000..9e7a550 --- /dev/null +++ b/static/demo-control.html @@ -0,0 +1,86 @@ + + + + + + + + +

Phaneron - basic controls demo

+
+ + + + + + +
Stream: + + + +
+
+
+ +
+ +
+ + + + +
+ +

rotation: + +

+ +

scale: + + +

+ +
+ Codec selection: +
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/static/demo-control.js b/static/demo-control.js new file mode 100644 index 0000000..460784c --- /dev/null +++ b/static/demo-control.js @@ -0,0 +1,173 @@ +let startTime; +const connectButton = document.getElementById('connectButton'); +const playButton = document.getElementById('playButton'); +const pauseButton = document.getElementById('pauseButton'); +const stopButton = document.getElementById('stopButton'); +const srot = document.getElementById("srot"); +const vrot = document.getElementById("vrot"); +const sscalex = document.getElementById("sscalex"); +const vscalex = document.getElementById("vscalex"); +const sscaley = document.getElementById("sscaley"); +const vscaley = document.getElementById("vscaley"); +vrot.innerHTML = srot.value +vscalex.innerHTML = `${+sscalex.value / 100.0}` +vscaley.innerHTML = `${+sscaley.value / 100.0}` + +playButton.disabled = true; +pauseButton.disabled = true; +stopButton.disabled = true; + +connectButton.addEventListener('click', async () => { + connectButton.disabled = true; + await startStream(); + playButton.disabled = false; +}) + +playButton.addEventListener('click', play); +pauseButton.addEventListener('click', pause); +stopButton.addEventListener('click', stopClick); + +const remoteVideo = document.getElementById('remoteVideo'); + +const host = `http://${window.location.hostname}:3002` + +remoteVideo.addEventListener('loadedmetadata', function() { + console.log(`Remote video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); +}); + +remoteVideo.addEventListener('resize', () => { + console.log(`Remote video size changed to ${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`); + // We'll use the first onsize callback as an indication that video has started + // playing out. + if (startTime) { + const elapsedTime = window.performance.now() - startTime; + console.log('Setup time: ' + elapsedTime.toFixed(3) + 'ms'); + startTime = null; + } +}); + +let peerConnection = null; +let dataChannel = null; +const offerOptions = { + offerToReceiveAudio: 1, + offerToReceiveVideo: 1 +}; +async function startStream(streamId) { + if (peerConnection) { + stopStream() + } + + if (streamId === undefined) { + const list = await listStreams(host) + if (!list || list.length === 0) throw new Error('No streams') + streamId = list[0].streamId + } + peerConnection = new RTCPeerConnection({}) + dataChannel = peerConnection.createDataChannel('Phaneron channel') + peerConnection.addEventListener('icecandidate', e => onIceCandidate(peerConnection, e)) + peerConnection.addEventListener('track', gotRemoteStream) + dataChannel.addEventListener('open', event => { + console.log('data channel open') + }) + const offer = await peerConnection.createOffer(offerOptions) + await peerConnection.setLocalDescription(offer) + console.log('Sending offer:', offer) + const offerResponse = await fetch(`${host}/streams/${streamId}/connections`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(offer) + }) + console.log('Response is:', offerResponse) + const remoteOffer = await offerResponse.json() + console.log(remoteOffer) + await peerConnection.setRemoteDescription(remoteOffer) + console.log(peerConnection.getRemoteStreams()) + setTimeout(async () => { + const stats = await peerConnection.getStats(); + stats.forEach(stat => { + if (!(stat.type === 'inbound-rtp' && stat.kind === 'video' || stat.kind === 'audio')) { + return; + } + const codec = stats.get(stat.codecId); + if (codec && stat.kind === 'video') { + document.getElementById('videoCodec').innerText = 'Video codec: ' + codec.mimeType + + ' ' + (codec.sdpFmtpLine ? codec.sdpFmtpLine + ' ' : '') + + ', payloadType=' + codec.payloadType + '.'; + } + if (codec && stat.kind === 'audio') { + document.getElementById('audioCodec').innerText = 'Audio codec: ' + codec.mimeType + + ' ' + (codec.sdpFmtpLine ? codec.sdpFmtpLine + ' ' : '') + + ', payloadType=' + codec.payloadType + ', clockRate=' + codec.clockRate + + ', channels=' + codec.channels + '.'; + } + + }); + }, 1000); + +} + +function stopStream() { + peerConnection.close() + peerConnection = null +} + +async function listStreams () { + const rawConsumersList = await fetch(`${host}/streams`, { + method: 'GET' + }); + const consumersList = await rawConsumersList.json(); + + return consumersList +} + +async function onIceCandidate(pc, event) { + console.log('onIceCandidate:', event) + // try { + // await (getOtherPc(pc).addIceCandidate(event.candidate)); + // onAddIceCandidateSuccess(pc); + // } catch (e) { + // onAddIceCandidateError(pc, e); + // } + // console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); +} + +function gotRemoteStream(e) { + if (remoteVideo.srcObject !== e.streams[0]) { + remoteVideo.srcObject = e.streams[0]; + console.log('pc2 received remote stream'); + } +} + +function play() { + console.log('Play button!'); + dataChannel.send("PLAY"); + pauseButton.disabled = false; + stopButton.disabled = false; +} + +function pause() { + console.log('Pause button'); + dataChannel.send("PAUSE"); +} + +function stopClick() { + console.log('Pause button'); + dataChannel.send("STOP"); +} + +srot.oninput = function() { + vrot.innerHTML = this.value + ""; + dataChannel.send("ROTATION " + this.value); +} + +sscalex.oninput = function() { + vscalex.innerHTML = `${+this.value / 100.0}`; + dataChannel.send(`FILL ${+this.value / 100} ${+sscaley.value / 100}`); +} + +sscaley.oninput = function() { + vscaley.innerHTML = `${+this.value / 100.0}`; + dataChannel.send(`FILL ${+sscalex.value / 100} ${+this.value / 100} `); +} \ No newline at end of file diff --git a/static/webrtc-player-orig.html b/static/webrtc-player-orig.html new file mode 100644 index 0000000..65e7891 --- /dev/null +++ b/static/webrtc-player-orig.html @@ -0,0 +1,98 @@ + + + + + + + + + +
+ + + + + + +
Stream: + + + +
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/static/webrtc-player-orig.js b/static/webrtc-player-orig.js new file mode 100644 index 0000000..6175c60 --- /dev/null +++ b/static/webrtc-player-orig.js @@ -0,0 +1,94 @@ +class ConnectionClient { + constructor(options = {}) { + this.options = { + host: '', + ...options + }; + } + + async listStreams() { + const { host } = this.options; + + const rawConsumersList = await fetch(`${host}/streams`, { + method: 'GET' + }); + const consumersList = await rawConsumersList.json(); + + return consumersList + } + + async createConnection(streamId, options = {}) { + options = { + beforeAnswer() { }, + stereo: false, + ...options + }; + + const { host } = this.options; + + const { + beforeAnswer, + stereo + } = options; + + // const rawConsumersList = await fetch(`${host}/streams`, { + // method: 'GET' + // }); + // const consumersList = await rawConsumersList.json(); + + // if (consumersList.length === 0) { + // throw new Error('No consumers') + // } + + // const streamId = consumersList[0].streamId + + const response1 = await fetch(`${host}/streams/${streamId}/connections`, { + method: 'POST' + }); + + const remotePeerConnection = await response1.json(); + const { id } = remotePeerConnection; + + const localPeerConnection = new RTCPeerConnection({ + sdpSemantics: 'unified-plan' + }); + + // NOTE(mroberts): This is a hack so that we can get a callback when the + // RTCPeerConnection is closed. In the future, we can subscribe to + // "connectionstatechange" events. + localPeerConnection.close = function () { + fetch(`${host}/streams/${streamId}/connections/${id}`, { method: 'delete' }).catch(() => { }); + return RTCPeerConnection.prototype.close.apply(this, arguments); + }; + + try { + await localPeerConnection.setRemoteDescription(remotePeerConnection.localDescription); + + await beforeAnswer(localPeerConnection); + + const originalAnswer = await localPeerConnection.createAnswer(); + const updatedAnswer = new RTCSessionDescription({ + type: 'answer', + sdp: stereo ? enableStereoOpus(originalAnswer.sdp) : originalAnswer.sdp + }); + await localPeerConnection.setLocalDescription(updatedAnswer); + + await fetch(`${host}/streams/${streamId}/connections/${id}/remote-description`, { + method: 'POST', + body: JSON.stringify(localPeerConnection.localDescription), + headers: { + 'Content-Type': 'application/json' + } + }); + + return localPeerConnection; + } catch (error) { + localPeerConnection.close(); + throw error; + } + }; +} + +function enableStereoOpus(sdp) { + return sdp.replace(/a=fmtp:111/, 'a=fmtp:111 stereo=1\r\na=fmtp:111'); +} diff --git a/static/webrtc-player.html b/static/webrtc-player.html new file mode 100644 index 0000000..4590222 --- /dev/null +++ b/static/webrtc-player.html @@ -0,0 +1,63 @@ + + + + + + + + +
+ + + + + + +
Stream: + + + +
+
+
+ +
+ + + + + + + + \ No newline at end of file diff --git a/static/webrtc-player.js b/static/webrtc-player.js new file mode 100644 index 0000000..ee305a4 --- /dev/null +++ b/static/webrtc-player.js @@ -0,0 +1,84 @@ +let startTime; +const remoteVideo = document.getElementById('remoteVideo'); + +remoteVideo.addEventListener('loadedmetadata', function() { + console.log(`Remote video videoWidth: ${this.videoWidth}px, videoHeight: ${this.videoHeight}px`); +}); + +remoteVideo.addEventListener('resize', () => { + console.log(`Remote video size changed to ${remoteVideo.videoWidth}x${remoteVideo.videoHeight}`); + // We'll use the first onsize callback as an indication that video has started + // playing out. + if (startTime) { + const elapsedTime = window.performance.now() - startTime; + console.log('Setup time: ' + elapsedTime.toFixed(3) + 'ms'); + startTime = null; + } +}); + +let peerConnection = null; +const offerOptions = { + offerToReceiveAudio: 1, + offerToReceiveVideo: 1 +}; +async function startStream(host, streamId) { + if (peerConnection) { + stopStream() + } + + if (streamId === undefined) { + const list = await listStreams(host) + if (!list || list.length === 0) throw new Error('No streams') + streamId = list[0].streamId + } + peerConnection = new RTCPeerConnection({}) + peerConnection.addEventListener('icecandidate', e => onIceCandidate(peerConnection, e)) + peerConnection.addEventListener('track', gotRemoteStream) + const offer = await peerConnection.createOffer(offerOptions) + await peerConnection.setLocalDescription(offer) + console.log('Sending offer:', offer) + const offerResponse = await fetch(`${host}/streams/${streamId}/connections`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(offer) + }) + console.log('Response is:', offerResponse) + const remoteOffer = await offerResponse.json() + console.log(remoteOffer) + await peerConnection.setRemoteDescription(remoteOffer) + console.log(peerConnection.getRemoteStreams()) +} + +function stopStream() { + peerConnection.close() + peerConnection = null +} + +async function listStreams (host) { + const rawConsumersList = await fetch(`${host}/streams`, { + method: 'GET' + }); + const consumersList = await rawConsumersList.json(); + + return consumersList +} + +async function onIceCandidate(pc, event) { + console.log('onIceCandidate:', event) + // try { + // await (getOtherPc(pc).addIceCandidate(event.candidate)); + // onAddIceCandidateSuccess(pc); + // } catch (e) { + // onAddIceCandidateError(pc, e); + // } + // console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`); +} + +function gotRemoteStream(e) { + if (remoteVideo.srcObject !== e.streams[0]) { + remoteVideo.srcObject = e.streams[0]; + console.log('pc2 received remote stream'); + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 7a53466..eed9dbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -166,6 +166,13 @@ resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw== +"@types/koa-bodyparser@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@types/koa-bodyparser/-/koa-bodyparser-4.3.0.tgz#54ecd662c45f3a4fa9de849528de5fc8ab269ba5" + integrity sha512-aB/vwwq4G9FAtKzqZ2p8UHTscXxZvICFKVjuckqxCtkX1Ro7F5KHkTCUqTRZFBgDoEkmeca+bFLI1bIsdPPZTA== + dependencies: + "@types/koa" "*" + "@types/koa-compose@*": version "3.2.5" resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d" @@ -173,21 +180,30 @@ dependencies: "@types/koa" "*" -"@types/koa@*": - version "2.13.1" - resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.1.tgz#e29877a6b5ad3744ab1024f6ec75b8cbf6ec45db" - integrity sha512-Qbno7FWom9nNqu0yHZ6A0+RWt4mrYBhw3wpBAQ3+IuzGcLlfeYkzZrnMq5wsxulN2np8M4KKeUpTodsOsSad5Q== +"@types/koa-route@^3.2.4": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@types/koa-route/-/koa-route-3.2.4.tgz#65fe3308f00bb33c5d0d95a79b441c30a6b1c989" + integrity sha512-jB4I4/HppiHiSFMqQvMC2Px5Zm8CExA7Bw7xW+o60xh4v0LrNTfKV7LMCGuJNRuUuTTiRcJvF1rbq5mINgQ1UQ== dependencies: - "@types/accepts" "*" - "@types/content-disposition" "*" - "@types/cookies" "*" - "@types/http-assert" "*" - "@types/http-errors" "*" - "@types/keygrip" "*" - "@types/koa-compose" "*" - "@types/node" "*" + "@types/koa" "*" + path-to-regexp "^1.7.0" + +"@types/koa-send@*": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@types/koa-send/-/koa-send-4.1.2.tgz#978f8267ad116d12ac6a18fecd8f34c5657e09ad" + integrity sha512-rfqKIv9bFds39Jxvsp8o3YJLnEQVPVriYA14AuO2OY65IHh/4UX4U/iMs5L0wATpcRmm1bbe0BNk23TRwx3VQQ== + dependencies: + "@types/koa" "*" -"@types/koa@2.11.6": +"@types/koa-static@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/koa-static/-/koa-static-4.0.1.tgz#b740d80a549b0a0a7a3b38918daecde88a7a50ec" + integrity sha512-SSpct5fEcAeRkBHa3RiwCIRfDHcD1cZRhwRF///ZfvRt8KhoqRrhK6wpDlYPk/vWHVFE9hPGqh68bhzsHkir4w== + dependencies: + "@types/koa" "*" + "@types/koa-send" "*" + +"@types/koa@*", "@types/koa@2.11.6": version "2.11.6" resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.11.6.tgz#b7030caa6b44af801c2aea13ba77d74aff7484d5" integrity sha512-BhyrMj06eQkk04C97fovEDQMpLpd2IxCB4ecitaXwOKGq78Wi2tooaDOWOFGajPk8IkQOAtMppApgSVkYe1F/A== @@ -246,6 +262,16 @@ "@types/mime" "^1" "@types/node" "*" +"@types/uuid@^8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" + integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== + +"@types/webrtc@^0.0.26": + version "0.0.26" + resolved "https://registry.yarnpkg.com/@types/webrtc/-/webrtc-0.0.26.tgz#f1bae07215eb84577c35ca050866820e412a07e2" + integrity sha512-hTDoPKPYbgcogZA9eqhihPO+HnUs5BNPfnoOyc9bzcuq56eYV28zwJ+3tortPN0uXgmDvNs3f1JaT4oTbtWxqg== + "@typescript-eslint/eslint-plugin@^4.15.1": version "4.16.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz#2caf6a79dd19c3853b8d39769a27fccb24e4e651" @@ -316,6 +342,11 @@ "@typescript-eslint/types" "4.16.1" eslint-visitor-keys "^2.0.0" +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + accepts@^1.3.5: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" @@ -367,6 +398,16 @@ ansi-colors@^4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + ansi-regex@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" @@ -391,6 +432,19 @@ any-promise@^1.1.0: resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -475,6 +529,11 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + cache-content-type@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" @@ -519,16 +578,36 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +co-body@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/co-body/-/co-body-6.1.0.tgz#d87a8efc3564f9bfe3aced8ef5cd04c7a8766547" + integrity sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ== + dependencies: + inflation "^2.0.0" + qs "^6.5.2" + raw-body "^2.3.3" + type-is "^1.6.16" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -558,6 +637,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + content-disposition@~0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" @@ -578,6 +662,16 @@ cookies@~0.8.0: depd "~2.0.0" keygrip "~1.1.0" +copy-to@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/copy-to/-/copy-to-2.0.1.tgz#2680fbb8068a48d08656b6098092bdafc906f4a5" + integrity sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU= + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -592,13 +686,20 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" -debug@^4.0.1, debug@^4.1.1: +debug@*, debug@^4.0.1, debug@^4.1.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== dependencies: ms "2.1.2" +debug@^3.1.0, debug@^3.2.6: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + debug@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" @@ -624,6 +725,11 @@ deep-equal@~1.0.1: resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -649,6 +755,11 @@ destroy@^1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -675,6 +786,13 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +domexception@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" + integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== + dependencies: + webidl-conversions "^4.0.2" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -943,6 +1061,13 @@ fresh@~0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= +fs-minipass@^1.2.5: + version "1.2.7" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" + integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== + dependencies: + minipass "^2.6.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -958,6 +1083,20 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + get-stream@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -1030,6 +1169,11 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -1064,7 +1208,18 @@ http-assert@^1.3.0: deep-equal "~1.0.1" http-errors "~1.7.2" -http-errors@^1.6.3: +http-errors@1.7.3, http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@^1.6.3, http-errors@^1.7.3: version "1.8.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507" integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== @@ -1075,16 +1230,29 @@ http-errors@^1.6.3: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= dependencies: depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +iconv-lite@0.4.24, iconv-lite@^0.4.4: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore-walk@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" + integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== + dependencies: + minimatch "^3.0.4" ignore@^3.3.5: version "3.3.10" @@ -1119,6 +1287,11 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +inflation@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f" + integrity sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8= + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1127,11 +1300,21 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4: +inherits@2, inherits@2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -1149,6 +1332,18 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -1186,6 +1381,16 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -1236,6 +1441,14 @@ kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +koa-bodyparser@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/koa-bodyparser/-/koa-bodyparser-4.3.0.tgz#274c778555ff48fa221ee7f36a9fbdbace22759a" + integrity sha512-uyV8G29KAGwZc4q/0WUAjH+Tsmuv9ImfBUF2oZVyZtaeo0husInagyn/JH85xMSxM0hEk/mbCII5ubLDuqW/Rw== + dependencies: + co-body "^6.0.0" + copy-to "^2.0.1" + koa-compose@^3.0.0: version "3.2.1" resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" @@ -1256,6 +1469,32 @@ koa-convert@^1.2.0: co "^4.6.0" koa-compose "^3.0.0" +koa-route@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/koa-route/-/koa-route-3.2.0.tgz#76298b99a6bcfa9e38cab6fe5c79a8733e758bce" + integrity sha1-dimLmaa8+p44yrb+XHmocz51i84= + dependencies: + debug "*" + methods "~1.1.0" + path-to-regexp "^1.2.0" + +koa-send@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.1.tgz#39dceebfafb395d0d60beaffba3a70b4f543fe79" + integrity sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ== + dependencies: + debug "^4.1.1" + http-errors "^1.7.3" + resolve-path "^1.4.0" + +koa-static@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-5.0.0.tgz#5e92fc96b537ad5219f425319c95b64772776943" + integrity sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ== + dependencies: + debug "^3.1.0" + koa-send "^5.0.0" + koa@^2.13.0: version "2.13.1" resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.1.tgz#6275172875b27bcfe1d454356a5b6b9f5a9b1051" @@ -1380,6 +1619,11 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +methods@~1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + micromatch@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" @@ -1426,6 +1670,33 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" +minimist@^1.2.0, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" + integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.2.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" + integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== + dependencies: + minipass "^2.9.0" + +mkdirp@^0.5.0, mkdirp@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + mount-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mount-point/-/mount-point-3.0.0.tgz#665cb9edebe80d110e658db56c31d0aef51a8f97" @@ -1452,6 +1723,11 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + nan@^2.14.0: version "2.14.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" @@ -1470,11 +1746,36 @@ naudiodon@^2.3.4: bindings "^1.5.0" segfault-handler "^1.3.0" +needle@^2.2.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.5.2.tgz#cf1a8fce382b5a280108bba90a14993c00e4010a" + integrity sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ== + dependencies: + debug "^3.2.6" + iconv-lite "^0.4.4" + sax "^1.2.4" + negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== +node-pre-gyp@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz#df9ab7b68dd6498137717838e4f92a33fc9daa42" + integrity sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ== + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + nodencl@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/nodencl/-/nodencl-1.3.1.tgz#1d750dba63a7a136fc7bf582f6cb7c31352348ff" @@ -1483,6 +1784,14 @@ nodencl@^1.3.1: bindings "^1.5.0" segfault-handler "^1.3.0" +nopt@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" + integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== + dependencies: + abbrev "1" + osenv "^0.1.4" + normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -1503,6 +1812,27 @@ normalize-package-data@^3.0.0: semver "^7.3.2" validate-npm-package-license "^3.0.1" +npm-bundled@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" + integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== + dependencies: + npm-normalize-package-bin "^1.0.1" + +npm-normalize-package-bin@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" + integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== + +npm-packlist@^1.1.6: + version "1.4.8" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" + integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + npm-normalize-package-bin "^1.0.1" + npm-run-path@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" @@ -1510,6 +1840,26 @@ npm-run-path@^3.0.0: dependencies: path-key "^3.0.0" +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + on-finished@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -1553,6 +1903,11 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= +os-tmpdir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + osc-min@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/osc-min/-/osc-min-1.1.2.tgz#3dcaff10f804c39b7080cf4c10fd7a4103cd9037" @@ -1560,6 +1915,14 @@ osc-min@^1.1.2: dependencies: binpack "~0" +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + p-finally@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" @@ -1618,7 +1981,7 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0: +path-is-absolute@1.0.1, path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= @@ -1633,6 +1996,13 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-to-regexp@^1.2.0, path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -1689,6 +2059,11 @@ prettier@^2.2.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -1707,16 +2082,41 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@^6.5.2: + version "6.9.4" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" + integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== + queue-microtask@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3" - integrity sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg== + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +raw-body@^2.3.3: + version "2.4.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" + integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== + dependencies: + bytes "3.1.0" + http-errors "1.7.3" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" @@ -1736,6 +2136,19 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" +readable-stream@^2.0.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -1764,6 +2177,14 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve-path@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7" + integrity sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc= + dependencies: + http-errors "~1.6.2" + path-is-absolute "1.0.1" + resolve@^1.10.0, resolve@^1.17.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" @@ -1777,6 +2198,13 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rimraf@^2.6.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -1791,11 +2219,26 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.1.2: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@^5.1.2: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + segfault-handler@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/segfault-handler/-/segfault-handler-1.3.0.tgz#054bc847832fa14f218ba6a79e42877501c8870e" @@ -1804,7 +2247,7 @@ segfault-handler@^1.3.0: bindings "^1.2.1" nan "^2.14.0" -"semver@2 || 3 || 4 || 5": +"semver@2 || 3 || 4 || 5", semver@^5.3.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -1821,6 +2264,16 @@ semver@^7.2.1, semver@^7.3.2: dependencies: lru-cache "^6.0.0" +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + setprototypeof@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" @@ -1843,7 +2296,7 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -signal-exit@^3.0.2: +signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== @@ -1911,11 +2364,28 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -"statuses@>= 1.5.0 < 2", statuses@^1.5.0: +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + string-width@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" @@ -1925,6 +2395,27 @@ string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + strip-ansi@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" @@ -1949,6 +2440,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -1973,6 +2469,19 @@ table@^6.0.4: slice-ansi "^4.0.0" string-width "^4.2.0" +tar@^4: + version "4.4.13" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" + integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== + dependencies: + chownr "^1.1.1" + fs-minipass "^1.2.5" + minipass "^2.8.6" + minizlib "^1.2.1" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.3" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -2088,6 +2597,11 @@ typescript@^4.1.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.2.tgz#1450f020618f872db0ea17317d16d8da8ddb8c4c" integrity sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ== +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -2102,7 +2616,7 @@ user-home@^2.0.0: dependencies: os-homedir "^1.0.0" -util-deprecate@^1.0.2: +util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= @@ -2130,6 +2644,11 @@ vary@^1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -2137,6 +2656,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + word-wrap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" @@ -2147,6 +2673,15 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +wrtc@^0.4.7: + version "0.4.7" + resolved "https://registry.yarnpkg.com/wrtc/-/wrtc-0.4.7.tgz#c61530cd662713e50bffe64b7a78673ce070426c" + integrity sha512-P6Hn7VT4lfSH49HxLHcHhDq+aFf/jd9dPY7lDHeFhZ22N3858EKuwm2jmnlPzpsRGEPaoF6XwkcxY5SYnt4f/g== + dependencies: + node-pre-gyp "^0.13.0" + optionalDependencies: + domexception "^1.0.1" + xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" @@ -2162,6 +2697,11 @@ xdg-trashdir@^3.1.0: user-home "^2.0.0" xdg-basedir "^4.0.0" +yallist@^3.0.0, yallist@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"