/* Phaneron - Clustered, accelerated and cloud-fit video server, pre-assembled and in kit form. Copyright (C) 2021 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 { Channel } from '../channel' import { Osc } from '../osc/osc' import fs from 'fs' import { StreamParams, TransitionParams } from '../chanLayer' type HeadsControls = { load?: string; take?: string } export type HeadsConfig = { channel: number controls: HeadsControls url?: string } type LayerSpec = { layerNum: number url: string streams?: StreamParams seek?: number length?: number transition?: TransitionParams } type EventSpec = { duration: number layers: LayerSpec[] } type HeadsSpec = { tickLayer: number events: EventSpec[] } const isJsonString = (str: string): boolean => { try { const json = JSON.parse(str) return typeof json === 'object' } catch (e) { return false } } export class Heads { private readonly osc: Osc private readonly channel: Channel private readonly eventDone: EventEmitter private lastSpec: string | undefined private headsSpec: HeadsSpec | undefined private eventTimeout: NodeJS.Timeout | undefined private running = false constructor(osc: Osc, channel: Channel, controls: HeadsControls) { this.osc = osc this.channel = channel this.eventDone = new EventEmitter() if (controls.load) this.osc.addControl(controls.load, (msg) => { if (msg.value !== 0) { const spec = typeof msg.value === 'string' ? msg.value : this.lastSpec if (spec) this.loadSpec(spec) } }) if (controls.take) this.osc.addControl(controls.take, (msg) => { if (msg.value !== 0) this.next() }) } loadSpec(urlOrJson: string): void { if (this.running) { this.running = false this.eventDone.emit('done') this.channel.clear(0) } if (isJsonString(urlOrJson)) { this.headsSpec = JSON.parse(urlOrJson) } else if (fs.existsSync(urlOrJson)) { const json = fs.readFileSync(urlOrJson).toString() this.headsSpec = JSON.parse(json) } else { console.log(`Heads: source URL or JSON '${urlOrJson}' could not be loaded`) } this.lastSpec = urlOrJson } async loadEvent(eventSpec: EventSpec): Promise { for (const l of eventSpec.layers) { // console.log('load event:', l.url, l.transition ? l.transition.type : 'cut') await this.channel.loadSource({ url: l.url, streams: l.streams, layer: l.layerNum, loop: false, preview: false, autoPlay: false, seek: l.seek, length: l.length, transition: l.transition }) } } async runEvent(eventSpec: EventSpec): Promise { let frameCount = 0 // Event is slightly long because it takes a few frames to start the next event const ticker = () => (frameCount++ === eventSpec.duration ? this.eventDone.emit('done') : {}) await Promise.all( eventSpec.layers.map((l) => this.channel.play(l.layerNum, l.layerNum === this.headsSpec?.tickLayer ? ticker : undefined) ) ) } async runEvents(): Promise { if (this.headsSpec) { this.running = true let eventId = 0 await this.loadEvent(this.headsSpec.events[eventId]) while (this.running && eventId < this.headsSpec.events.length) { await this.runEvent(this.headsSpec.events[eventId]) eventId++ if (eventId < this.headsSpec.events.length) await this.loadEvent(this.headsSpec.events[eventId]) await once(this.eventDone, 'done') if (eventId === this.headsSpec.events.length) { await this.channel.clear(0) this.running = false } } } } run(): void { this.runEvents() } next(): void { if (this.eventTimeout) clearTimeout(this.eventTimeout) if (this.running) this.eventDone.emit('done') else this.run() } }