/* 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 { EventEmitter, once } from 'events' import { clContext as nodenCLContext, OpenCLBuffer } from 'nodencl' import { RedioPipe, RedioEnd } from 'redioactive' import { Frame } from 'beamcoder' import { Producer } from './producer/producer' import { MixerDefaults } from './producer/mixer' import { Transitioner, TransitionSpec } from './transitioner' import { ConsumerConfig } from './config' import { ClJobs } from './clJobQueue' import { SourcePipes } from './routeSource' export const DefaultTransitionSpec = '{ "type": "cut", "len": 0 }' type SourceSpec = { source: Producer | undefined transition: TransitionSpec firstTs: number | undefined } export class Layer { private readonly clContext: nodenCLContext private readonly layerID: string private readonly consumerConfig: ConsumerConfig private readonly clJobs: ClJobs private readonly endEvent: EventEmitter private mixerParams = JSON.parse(MixerDefaults) private curSrcSpec: SourceSpec private nextSrcSpec: SourceSpec private transitioner: Transitioner | null private channelUpdate: () => void private layerTick: ((t: string) => void) | undefined private autoPlay = false constructor( clContext: nodenCLContext, layerID: string, consumerConfig: ConsumerConfig, clJobs: ClJobs ) { this.clContext = clContext this.layerID = layerID this.consumerConfig = consumerConfig this.clJobs = clJobs this.endEvent = new EventEmitter() this.curSrcSpec = { source: undefined, transition: JSON.parse(DefaultTransitionSpec), firstTs: undefined } this.nextSrcSpec = { source: undefined, transition: JSON.parse(DefaultTransitionSpec), firstTs: undefined } // eslint-disable-next-line @typescript-eslint/no-empty-function this.channelUpdate = () => {} this.transitioner = new Transitioner( this.clContext, this.layerID, this.consumerConfig.format, this.clJobs, this.endEvent, this.layerUpdate.bind(this) ) } async initialise(): Promise { await this.transitioner?.initialise() } update(): void { const audioPipes: RedioPipe[] = [] const transitionSpec = this.curSrcSpec.transition if (this.curSrcSpec.source) { audioPipes.push(this.curSrcSpec.source.getMixer().getAudioPipe()) if (transitionSpec.source && transitionSpec.type !== 'cut') { audioPipes.push(transitionSpec.source.getMixer().getAudioPipe()) // if (transitionSpec.mask) audioPipes.push(transitionSpec.mask.getMixer().getAudioPipe()) } } const videoPipes: RedioPipe[] = [] if (this.curSrcSpec.source) { videoPipes.push(this.curSrcSpec.source.getMixer().getVideoPipe()) if (transitionSpec.source && transitionSpec.type !== 'cut') { videoPipes.push(transitionSpec.source.getMixer().getVideoPipe()) if (transitionSpec.mask) videoPipes.push(transitionSpec.mask.getMixer().getVideoPipe()) } } this.transitioner?.update(transitionSpec.type, transitionSpec.len, audioPipes, videoPipes) } layerUpdate(ts: number[]): void { if (this.layerTick && ts.length) this.layerTick('tick') if (this.curSrcSpec.transition.type !== 'cut' && ts.length > 1) { if (!this.curSrcSpec.firstTs) this.curSrcSpec.firstTs = ts[1] const numEnds = ts.reduce((n, t) => (n += t < 0 ? 1 : 0), 0) if (ts[1] - this.curSrcSpec.firstTs === this.curSrcSpec.transition.len - 2) { this.curSrcSpec.transition.mask?.release() this.curSrcSpec.source?.release() } else if ( (this.curSrcSpec.transition.type === 'wipe' && numEnds > 1) || (this.curSrcSpec.transition.type === 'dissolve' && numEnds > 0) ) { this.curSrcSpec.source = this.curSrcSpec.transition.source this.curSrcSpec.transition = JSON.parse(DefaultTransitionSpec) this.update() this.endEvent.emit('transitionComplete') } } else if ( this.curSrcSpec.source && this.curSrcSpec.transition.type === 'cut' && ts.length === 1 && ts[0] < 0 ) { this.curSrcSpec.transition.mask?.release() this.curSrcSpec.source?.release() this.curSrcSpec.source = undefined this.curSrcSpec.transition = JSON.parse(DefaultTransitionSpec) this.endEvent.emit('end') if (this.nextSrcSpec.source === undefined && this.layerTick) this.layerTick('end') } } async load( producer: Producer, transitionSpec: TransitionSpec, preview: boolean, autoPlay: boolean, channelUpdate: () => void ): Promise { this.nextSrcSpec = { source: producer, transition: transitionSpec, firstTs: undefined } this.autoPlay = autoPlay this.channelUpdate = channelUpdate if (this.autoPlay) { if (this.curSrcSpec.source) { this.endEvent.once('end', () => { this.curSrcSpec.source = undefined this.play() }) } else { this.play() } } else if (preview) { if (this.curSrcSpec.source) { this.curSrcSpec.source.release() await once(this.endEvent, 'end') } this.curSrcSpec.source = this.nextSrcSpec.source this.nextSrcSpec.source = undefined await this.update() this.channelUpdate() } return true } async play(ticker?: (t: string) => void): Promise { if (this.nextSrcSpec.source && this.nextSrcSpec.transition?.type === 'cut') { if (this.curSrcSpec.source) { this.curSrcSpec.source.release() await once(this.endEvent, 'end') } this.curSrcSpec.source = this.nextSrcSpec.source } this.curSrcSpec.transition = this.nextSrcSpec.transition if (this.curSrcSpec.transition.type !== 'cut') { this.curSrcSpec.transition.source = this.nextSrcSpec.source } this.nextSrcSpec.source = undefined this.nextSrcSpec.transition = JSON.parse(DefaultTransitionSpec) this.autoPlay = false this.layerTick = ticker this.curSrcSpec.source?.setPaused(false) this.curSrcSpec.transition?.source?.setPaused(false) this.curSrcSpec.transition?.mask?.setPaused(false) await this.update() this.channelUpdate() // delay further commands until any transition has completed - reduces demand on cpu/gpu if (this.curSrcSpec.transition.type !== 'cut') await once(this.endEvent, 'transitionComplete') } pause(): void { this.curSrcSpec.source?.setPaused(true) } resume(): void { this.curSrcSpec.source?.setPaused(false) } async stop(): Promise { if (this.curSrcSpec.source) { this.curSrcSpec.source.release() await once(this.endEvent, 'end') } this.curSrcSpec.source = undefined this.autoPlay = false } anchor(params: string[]): void { const mixer = this.curSrcSpec.source ? this.curSrcSpec.source.getMixer() : this.nextSrcSpec.source?.getMixer() if (params.length) { this.mixerParams.anchor = { x: +params[0], y: +params[1] } mixer?.setMixParams(this.mixerParams) } else { console.dir(this.mixerParams.anchor, { colors: true }) } } rotation(params: string[]): void { const mixer = this.curSrcSpec.source ? this.curSrcSpec.source.getMixer() : this.nextSrcSpec.source?.getMixer() if (params.length) { this.mixerParams.rotation = +params[0] mixer?.setMixParams(this.mixerParams) } else { console.dir(this.mixerParams.rotation, { colors: true }) } } fill(params: string[]): void { const mixer = this.curSrcSpec.source ? this.curSrcSpec.source.getMixer() : this.nextSrcSpec.source?.getMixer() if (params.length) { this.mixerParams.fill = { xOffset: +params[0], yOffset: +params[1], xScale: +params[2], yScale: +params[3] } mixer?.setMixParams(this.mixerParams) } else { console.dir(this.mixerParams.fill, { colors: true }) } } volume(params: string[]): void { const mixer = this.curSrcSpec.source ? this.curSrcSpec.source.getMixer() : this.nextSrcSpec.source?.getMixer() if (params.length) { this.mixerParams.volume = +params[0] mixer?.setMixParams(this.mixerParams) } else { console.dir(this.mixerParams.volume, { colors: true }) } } async getSourcePipes(): Promise { return this.curSrcSpec.source?.getSourcePipes() } getAudioPipe(): RedioPipe | undefined { return this.transitioner?.getAudioPipe() } getVideoPipe(): RedioPipe | undefined { return this.transitioner?.getVideoPipe() } getEndEvent(): EventEmitter { return this.endEvent } async release(): Promise { this.curSrcSpec.source = undefined await this.transitioner?.release() this.transitioner = null } }