/*
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 { CmdList, CmdSet } from './commands'
import { ChanLayer, LoadParams } from '../chanLayer'
import { Channel } from '../channel'
import { ConsumerRegistry } from '../consumer/consumer'
import { ClJobs } from '../clJobQueue'
import { ConfigParams } from '../config'
export class BasicCmds implements CmdList {
private readonly consumerRegistry: ConsumerRegistry
private readonly channels: Array
private readonly clJobs: ClJobs
constructor(consumerRegistry: ConsumerRegistry, channels: Array, clJobs: ClJobs) {
this.consumerRegistry = consumerRegistry
this.channels = channels
this.clJobs = clJobs
}
list(): CmdSet {
return {
group: '',
entries: [
{ cmd: 'LOADBG', fn: this.loadbg.bind(this) },
{ cmd: 'LOAD', fn: this.load.bind(this) },
{ cmd: 'PLAY', fn: this.play.bind(this) },
{ cmd: 'PAUSE', fn: this.pause.bind(this) },
{ cmd: 'RESUME', fn: this.resume.bind(this) },
{ cmd: 'STOP', fn: this.stop.bind(this) },
{ cmd: 'CLEAR', fn: this.clear.bind(this) },
{ cmd: 'ADD', fn: this.add.bind(this) },
{ cmd: 'REMOVE', fn: this.remove.bind(this) }
]
}
}
parseParams(params: string[]): ConfigParams {
const paramsObj: ConfigParams = {}
const paramStr = params.join(' ')
const re = /(?[^-\s]+)(\s+(?[^\s]+))?/g
let matches: RegExpExecArray | null = null
while ((matches = re.exec(paramStr)) && matches.groups) {
if (matches.groups.value) {
const value = parseInt(matches.groups.value)
paramsObj[matches.groups.name.toLowerCase()] = isNaN(value)
? matches.groups.value.toLowerCase()
: value
}
}
return paramsObj
}
async doLoad(chanLay: ChanLayer, params: string[], preview: boolean): Promise {
if (!chanLay.valid) return Promise.resolve(false)
const channel = this.channels[chanLay.channel - 1]
if (!channel) return Promise.resolve(false)
const url = params[0]
const chanNum = url === 'DECKLINK' ? +params[1] : 0
const loop = params.find((param) => param === 'LOOP') !== undefined
const autoPlay = params.find((param) => param === 'AUTO') !== undefined
const seekIndex = params.findIndex((param) => param === 'SEEK')
const seek = seekIndex < 0 ? 0 : +params[seekIndex + 1]
const lengthIndex = params.findIndex((param) => param === 'LENGTH')
const length = seekIndex < 0 ? 0 : +params[lengthIndex + 1]
const loadParams: LoadParams = {
url: url,
layer: chanLay.layer,
channel: chanNum,
loop: loop,
preview: preview,
autoPlay: autoPlay,
seek: seek,
length: length
}
return channel.loadSource(loadParams)
}
/**
* Loads a producer in the background and prepares it for playout. If no layer is specified the default layer index will be used.
*
* _clip_ will be parsed by available registered producer factories. If a successfully match is found, the producer will be loaded into the background.
* If a file with the same name (extension excluded) but with the additional postfix _a is found this file will be used as key for the main clip.
*
* _loop_ will cause the clip to loop.
* When playing and looping the clip will start at _frame_.
* When playing and loop the clip will end after _frames_ number of frames.
*
* _auto_ will cause the clip to automatically start when foreground clip has ended (without play).
* The clip is considered "started" after the optional transition has ended.
*
* Note: only one clip can be queued to play automatically per layer.
*/
async loadbg(chanLay: ChanLayer, params: string[]): Promise {
return this.doLoad(chanLay, params, false)
}
/**
* Loads a clip to the foreground and plays the first frame before pausing.
* If any clip is playing on the target foreground then this clip will be replaced.
*/
async load(chanLay: ChanLayer, params: string[]): Promise {
return this.doLoad(chanLay, params, true)
}
/**
* Moves clip from background to foreground and starts playing it.
* If a transition (see LOADBG) is prepared, it will be executed.
* If additional parameters (see LOADBG) are provided then the provided clip will first be loaded to the background.
*/
async play(chanLay: ChanLayer, params: string[]): Promise {
if (!chanLay.valid) return Promise.resolve(false)
const channel = this.channels[chanLay.channel - 1]
if (!channel) return Promise.resolve(false)
if (params.length !== 0) await this.loadbg(chanLay, params)
return channel.play(chanLay.layer)
}
/**
* Pauses playback of the foreground clip on the specified layer.
* The RESUME command can be used to resume playback again.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async pause(chanLay: ChanLayer, _params: string[]): Promise {
if (!chanLay.valid) return Promise.resolve(false)
const channel = this.channels[chanLay.channel - 1]
if (!channel) return Promise.resolve(false)
return channel.pause(chanLay.layer)
}
/** Resumes playback of a foreground clip previously paused with the PAUSE command. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async resume(chanLay: ChanLayer, _params: string[]): Promise {
if (!chanLay.valid) return Promise.resolve(false)
const channel = this.channels[chanLay.channel - 1]
if (!channel) return Promise.resolve(false)
return channel.resume(chanLay.layer)
}
/** Removes the foreground clip of the specified layer */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async stop(chanLay: ChanLayer, _params: string[]): Promise {
if (!chanLay.valid) return Promise.resolve(false)
const channel = this.channels[chanLay.channel - 1]
if (!channel) return Promise.resolve(false)
return channel.stop(chanLay.layer)
}
/**
* Removes all clips (both foreground and background) of the specified layer.
* If no layer is specified then all layers in the specified video_channel are cleared.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async clear(chanLay: ChanLayer, _params: string[]): Promise {
if (!chanLay.valid) return Promise.resolve(false)
const channel = this.channels[chanLay.channel - 1]
if (!channel) return Promise.resolve(false)
return channel.clear(chanLay.layer)
}
async add(chanLay: ChanLayer, params: string[]): Promise {
if (!chanLay.valid) return Promise.resolve(false)
const channel = this.channels[chanLay.channel - 1]
if (!channel) return Promise.resolve(false)
if (params.length === 0) return Promise.resolve(false)
let consumerName = params[0].toLowerCase()
if (consumerName === 'file' || consumerName === 'stream') consumerName = 'ffmpeg'
const consumerIndex = chanLay.layer ? chanLay.layer : -1
const deviceIndex = +params[1] || 0
try {
const consumer = this.consumerRegistry.createConsumer(
chanLay.channel,
consumerIndex,
this.parseParams(params),
{ name: consumerName, deviceIndex: deviceIndex },
this.clJobs
)
await consumer.initialise()
channel.addConsumer(consumer)
} catch (err) {
console.log(`Error adding consumer to configured channel ${chanLay.channel}: ${err.message}`)
return Promise.resolve(false)
}
return Promise.resolve(true)
}
async remove(chanLay: ChanLayer, params: string[]): Promise {
if (!chanLay.valid) return Promise.resolve(false)
const channel = this.channels[chanLay.channel - 1]
if (!channel) return Promise.resolve(false)
const consumerIndex = chanLay.layer ? chanLay.layer : -1
try {
this.consumerRegistry.removeConsumer(
chanLay.channel,
channel,
consumerIndex,
this.parseParams(params)
)
} catch (err) {
console.log(
`Error removing consumer from configured channel ${chanLay.channel}: ${err.message}`
)
return Promise.resolve(false)
}
return Promise.resolve(false)
}
}