First Commit

First Commit
This commit is contained in:
knicolas22 2025-06-16 10:30:42 +02:00
commit 830b4e3d06
23 changed files with 4720 additions and 0 deletions

8
.gitignore vendored Executable file
View File

@ -0,0 +1,8 @@
node_modules/
package-lock.json
/pkg
/pkg.tgz
/dist
DEBUG-*
/.yarn
version_javascript/

2
.prettierignore Executable file
View File

@ -0,0 +1,2 @@
package.json
/LICENSE.md

1
.yarnrc.yml Executable file
View File

@ -0,0 +1 @@
nodeLinker: node-modules

21
LICENSE Executable file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Bitfocus AS - Open Source
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

11
README.md Executable file
View File

@ -0,0 +1,11 @@
# companion-module-[replace with module name]
See [HELP.md](./companion/HELP.md) and [LICENSE](./LICENSE)
## Getting started
Executing a `yarn` command should perform all necessary steps to develop the module, if it does not then follow the steps below.
The module can be built once with `yarn build`. This should be enough to get the module to be loadable by companion.
While developing the module, by using `yarn dev` the compiler will be run in watch mode to recompile the files on change.

3
companion/HELP.md Executable file
View File

@ -0,0 +1,3 @@
## Your module
Write some help for your users here!

37
companion/manifest.json Executable file
View File

@ -0,0 +1,37 @@
{
"id": "modulopi-moduloplayer",
"name": "modulopi-moduloplayer",
"shortname": "Modulo Player",
"description": "Modulo Player plugin for Companion",
"version": "4.0.0",
"license": "MIT",
"repository": "git+https://github.com/bitfocus/companion-module-modulopi-moduloplayer.git",
"bugs": "https://github.com/bitfocus/companion-module-modulopi-moduloplayer/issues",
"maintainers": [
{
"name": "Nicolas Keesst",
"email": "nkeesst@pixap.fr"
},
{
"name": "Per Røine",
"email": "per.roine@gmail.com"
}
],
"runtime": {
"type": "node22",
"api": "nodejs-ipc",
"apiVersion": "0.0.0",
"entrypoint": "../dist/main.js"
},
"legacyIds": [
"modulo"
],
"manufacturer": "Modulo Pi",
"products": [
"Modulo Player"
],
"keywords": [
"Software",
"Media Server"
]
}

5
eslint.config.mjs Executable file
View File

@ -0,0 +1,5 @@
import { generateEslintConfig } from '@companion-module/tools/eslint/config.mjs'
export default generateEslintConfig({
enableTypescript: true,
})

51
package.json Executable file
View File

@ -0,0 +1,51 @@
{
"name": "modulopi-moduloplayer",
"version": "4.0.0",
"main": "dist/main.js",
"type": "module",
"scripts": {
"postinstall": "husky",
"format": "prettier -w .",
"package": "run build && companion-module-build",
"build": "rimraf dist && run build:main",
"build:main": "tsc -p tsconfig.build.json",
"dev": "tsc -p tsconfig.build.json --watch",
"lint:raw": "eslint",
"lint": "run lint:raw ."
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/bitfocus/companion-module-modulopi-moduloplayer.git"
},
"engines": {
"node": "^22.14",
"yarn": "^4"
},
"dependencies": {
"@companion-module/base": "~1.11.3",
"ws": "^8.18.0"
},
"devDependencies": {
"@companion-module/tools": "^2.3.0",
"@types/node": "^22.14.1",
"@types/ws": "^8.18.1",
"eslint": "^9.24.0",
"husky": "^9.1.7",
"lint-staged": "^15.5.1",
"prettier": "^3.5.3",
"rimraf": "^6.0.1",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1"
},
"prettier": "@companion-module/tools/.prettierrc.json",
"lint-staged": {
"*.{css,json,md,scss}": [
"prettier --write"
],
"*.{ts,tsx,js,jsx}": [
"yarn lint:raw --fix"
]
},
"packageManager": "yarn@4.9.1"
}

88
src/actions.ts Executable file
View File

@ -0,0 +1,88 @@
import type { MPinstance } from './main.js'
export function UpdateActions(self: MPinstance): void {
self.setActionDefinitions({
// LAUNCH TASK
launch_task: {
name: 'Launch Task (ID)',
options: [
{
id: 'task',
type: 'textinput',
label: 'Task ID',
default: '',
},
],
callback: async (event) => {
console.log('Launch Task ID: ' + event.options.task)
self.mpConnection.sendMessageLunchTask(event.options.task, 2)
},
},
// GOTO CUE
goto_cue: {
name: 'Launch Cue (ID) on Playlist (ID)',
options: [
{
id: 'index',
type: 'number',
label: 'Cue ID',
default: 1,
min: 1,
max: 10000,
},
{
id: 'cue',
type: 'textinput',
label: 'Cue UUID',
default: '',
},
{
id: 'pl',
type: 'textinput',
label: 'Playlist ID',
default: '',
},
],
callback: async (event) => {
console.log(`Launch Cue ID: ${event.options.index} from Playlist UUID: ${event.options.pl}`)
self.moduloplayer?.setGotoCue(event.options.pl, event.options.index)
},
},
pl_grand_master_fader: {
name: 'GrandMaster Fader on Playlist (ID)',
options: [
{
id: 'pl',
type: 'textinput',
label: 'Playlist ID',
default: '',
},
{
id: 'value',
type: 'number',
label: 'Value in % (0 to 100)',
default: 100,
min: 0,
max: 100,
},
{
id: 'duration',
type: 'number',
label: 'Duration in ms (max 3600000 = 1 hour)',
default: 2000,
min: 0,
max: 3600000,
},
],
callback: async (event) => {
self.log(
'info',
`SET GRAND MASTER PL: ${event.options.pl} | value: ${event.options.value} | duration: ${event.options.duration}`,
)
self.moduloplayer?.setGrandMasterFader(event.options.pl, event.options.value, event.options.duration)
},
},
})
}

83
src/configFields.ts Executable file
View File

@ -0,0 +1,83 @@
import { Regex, type SomeCompanionConfigField } from '@companion-module/base'
export interface ModuloPlayConfig {
host: string
mpPort: number
sdPort: number
sdEnable: boolean
pollInterval: number
}
export function GetConfigFields(): SomeCompanionConfigField[] {
return [
{
type: 'static-text',
id: 'websocket',
width: 12,
label: '',
value: `<h5>Websockets</h5>
Modulo Player Websockets
`,
},
{
type: 'textinput',
id: 'host',
label: 'Target IP',
width: 8,
regex: Regex.IP,
},
{
type: 'number',
id: 'mpPort',
label: 'Player Port',
width: 4,
min: 1,
max: 65535,
default: 8080,
},
{
type: 'checkbox',
id: 'sdEnable',
label: 'Enable Spydog',
width: 3,
default: true,
},
{
type: 'static-text',
id: 'spacer01',
label: '',
width: 5,
value: '',
},
{
type: 'number',
id: 'sdPort',
label: 'Spydog Port',
width: 4,
min: 1,
max: 65535,
default: 8081,
},
{
type: 'static-text',
id: 'pollInfo',
width: 12,
label: '',
value: `<h5>Poll Interval warning</h5>
Adjusting the Polling Interval can impact performance.
<br>
<strong>A lower invterval allows for more responsive feedback, but may impact CPU usage.</strong>
<br>
`,
},
{
type: 'number',
id: 'pollInterval',
label: 'Polling interval (ms) (default: 250, min: 100, 0 for disabled)',
width: 12,
default: 1000,
min: 0,
max: 60000,
},
]
}

136
src/feedbacks.ts Executable file
View File

@ -0,0 +1,136 @@
import { combineRgb } from '@companion-module/base'
import type { MPinstance } from './main.js'
const colorGreenMP = [88, 201, 23]
export function UpdateFeedbacks(self: MPinstance): void {
self.setFeedbackDefinitions({
color_cue: {
name: 'PlayList Color Cue',
type: 'boolean',
defaultStyle: {
bgcolor: combineRgb(255, 0, 0),
color: combineRgb(0, 0, 0),
},
options: [
{
id: 'current_Cue',
type: 'number',
label: 'ID',
default: 1,
min: 1,
max: 10000,
},
{
id: 'pl',
type: 'textinput',
label: 'Playlist UUID',
default: '',
},
],
callback: (feedback) => {
console.log(
'FEEDBACK | Color Cue: ' +
feedback.options.current_Cue + " >>> " +
feedback.options.pl + " >>> " +
self.states[`pl_${feedback.options.pl}_currentIndex`]
+ " >>> " + (self.states[`pl_${feedback.options.pl}_currentIndex`] !== feedback.options.current_Cue)
)
if (self.states[`pl_${feedback.options.pl}_currentIndex`] !== feedback.options.current_Cue) {
return true
} else {
return false
}
},
},
current_Cue: {
name: 'PlayList Current Cue',
type: 'boolean',
defaultStyle: {
bgcolor: combineRgb(255, 0, 0),
color: combineRgb(0, 0, 0),
},
options: [
{
id: 'current_Cue',
type: 'number',
label: 'ID',
default: 1,
min: 1,
max: 10000,
},
{
id: 'pl',
type: 'textinput',
label: 'Playlist UUID',
default: '',
},
],
callback: (feedback) => {
// console.log(
// 'FEEDBACK | Current Cue ID Change State: ' +
// feedback.options.pl +
// self.states[`pl_${feedback.options.pl}_currentIndex`] +
// ' / id: ' +
// feedback.options.current_Cue,
// )
if (self.states[`pl_${feedback.options.pl}_currentIndex`] === feedback.options.current_Cue) {
return true
} else {
return false
}
},
},
status: {
name: 'Modulo Player Status',
type: 'boolean',
defaultStyle: {
bgcolor: combineRgb(colorGreenMP[0], colorGreenMP[1], colorGreenMP[2]),
},
options: [
{
id: 'status',
type: 'number',
label: 'Modulo Player Status',
default: 0,
min: 0,
max: 2,
},
],
callback: (feedback) => {
if (feedback.options.status === self.states[`status`]) {
return true
} else {
return false
}
},
},
master: {
name: 'Modulo Player Master',
type: 'boolean',
defaultStyle: {
text: 'Master',
bgcolor: combineRgb(255, 0, 0),
color: combineRgb(0, 0, 0),
},
options: [
{
id: 'master',
type: 'number',
label: 'Master',
default: 1,
min: 0,
max: 1,
},
],
callback: async (feedback) => {
if (feedback.options.master === self.states[`master`]) {
return true
} else {
return false
}
},
},
})
}

150
src/main.ts Executable file
View File

@ -0,0 +1,150 @@
import { InstanceBase, runEntrypoint, InstanceStatus, SomeCompanionConfigField } from '@companion-module/base'
import { GetConfigFields, type ModuloPlayConfig } from './configFields.js'
import { UpdateVariableDefinitions } from './variables.js'
import { UpgradeScripts } from './upgrades.js'
import { UpdateActions } from './actions.js'
import { UpdateFeedbacks } from './feedbacks.js'
//import { getPresets } from './presets.js'
import { MPconnection } from './mpconnection.js'
import { SDconnection } from './sdconnection.js'
import { ModuloPlayer } from './moduloplayer.js'
import { SpyDog } from './spydog.js'
interface IStringIndex {
[key: string]: any
}
export class MPinstance extends InstanceBase<ModuloPlayConfig> {
config!: ModuloPlayConfig // Setup in init()
/** reference to the connection with the device */
public mpConnection!: MPconnection
public sdConnection!: SDconnection
public mpConnected = false
public sdConnected = false
moduloplayer: ModuloPlayer | undefined
spydog: SpyDog | undefined
public pollAPI: NodeJS.Timer | null = null
// MODULO PLAYER DATA
public tasksList = []
public playLists = []
public states: IStringIndex = {}
// CONTRUCTOR
constructor(internal: unknown) {
super(internal)
}
async init(config: ModuloPlayConfig): Promise<void> {
this.mpConnection = new MPconnection(this)
this.sdConnection = new SDconnection(this)
this.moduloplayer = new ModuloPlayer(this)
this.spydog = new SpyDog(this)
await this.configUpdated(config)
this.updateActions() // export actions
this.updateFeedbacks() // export feedbacks
this.updateVariableDefinitions()
// SET SPYDOG STATIC INFO
this.setVariableValues({ clusterId: 0 })
this.setVariableValues({ color: "" })
this.setVariableValues({ cpuTemperature: 0 })
this.setVariableValues({ cpuUse: 0 })
this.setVariableValues({ detacastTemperature: "" })
this.setVariableValues({ fps: 0 })
this.setVariableValues({ fpsOk: false })
this.setVariableValues({ gpuTemperature: 0 })
// SET SPYDOG DYNAMIC VARIABLE
this.setVariableValues({ clusterId: 0 })
this.setVariableValues({ color: "" })
this.setVariableValues({ cpuTemperature: 0 })
this.setVariableValues({ cpuUse: 0 })
this.setVariableValues({ detacastTemperature: "" })
this.setVariableValues({ fps: 0 })
this.setVariableValues({ fpsOk: false })
this.setVariableValues({ gpuTemperature: 0 })
this.setVariableValues({ lockStatus: 0 })
this.setVariableValues({ master: true })
this.setVariableValues({ maxAutocalibOutputs: 0 })
this.setVariableValues({ maxOutputs: 0 })
this.setVariableValues({ memoryUse: 0 })
this.setVariableValues({ motherboardTemperature: 0 })
this.setVariableValues({ serverIp: "" })
this.setVariableValues({ serverName: "" })
this.setVariableValues({ serverTime: "" })
this.setVariableValues({ status: 0 })
this.setVariableValues({ upTime: "" })
}
// When module gets deleted
async destroy(): Promise<void> {
this.mpConnection.destroy()
this.sdConnection.destroy()
this.log('debug', 'destroy')
}
async configUpdated(config: ModuloPlayConfig): Promise<void> {
this.updateStatus(InstanceStatus.Disconnected, 'Update')
this.config = config
this.mpConnection.disconnect()
this.sdConnection.disconnect()
this.updateStatus(InstanceStatus.Connecting, `Init Connection`)
await this.mpConnection.connect(this.config.host, this.config.mpPort)
await this.sdConnection.connect(this.config.host, this.config.sdPort)
}
async isConnected() {
if (this.mpConnected && this.sdConnected) {
this.updateStatus(InstanceStatus.Ok, `Connected`)
if (this.mpConnected) this.moduloplayer?.getTaskListModuloPlayer()
if (this.mpConnected) this.moduloplayer?.getPlaylistModuloPlayer()
if (this.sdConnected) this.spydog?.getStaticInfo()
if (this.sdConnected) this.spydog?.getDynamicInfo()
} else if (!this.mpConnected && this.sdConnected) {
this.updateStatus(InstanceStatus.Connecting, `Spydog Connected | Start Modulo Player`)
if (this.mpConnected) this.moduloplayer?.getTaskListModuloPlayer()
if (this.mpConnected) this.moduloplayer?.getPlaylistModuloPlayer()
if (this.sdConnected) this.spydog?.getStaticInfo()
if (this.sdConnected) this.spydog?.getDynamicInfo()
} else {
this.updateStatus(InstanceStatus.Connecting, `Init Connection`)
}
}
async updateInstance() {
this.updateActions() // export actions
this.updateFeedbacks() // export feedbacks
this.updateVariableDefinitions()
}
updatPolling() {
if (this.mpConnected) this.moduloplayer?.getPlaylistsCurrentCues()
if (this.mpConnected) this.moduloplayer?.getPlaylistModuloPlayer()
if (this.sdConnected) this.spydog?.getDynamicInfo()
}
// Return config fields for web config
getConfigFields(): SomeCompanionConfigField[] {
return GetConfigFields()
}
updateActions(): void {
UpdateActions(this)
}
updateFeedbacks(): void {
UpdateFeedbacks(this)
}
updateVariableDefinitions(): void {
UpdateVariableDefinitions(this)
}
}
runEntrypoint(MPinstance, UpgradeScripts)

150
src/moduloplayer.ts Executable file
View File

@ -0,0 +1,150 @@
import { MPinstance } from './main.js'
import { getPresets } from './presets.js'
// JSON ID
// 1 = list Tasks,
// 2 = Launch Task,
// 3 = list Playlist
// 100 = CURRENT CUE LIST
// 110 = ACTION GOTO
// 200 = SPYDOG DYNAMIC INFO
// 201 = SPYDOG STATIC INFO
export class ModuloPlayer {
instance: MPinstance
constructor(instance: MPinstance) {
this.instance = instance
}
public messageManager(data: String): void {
const datas = JSON.parse(data.toString())
this.instance.log('debug', 'MODULO PLAYER | MESSAGE MANAGER | DATA ID >>> ' + datas['id'])
if (datas['id'] == 1) {
//console.log('debug', 'MODULO PLAYER | MESSAGE MANAGER | DATA >>> ' + data.toString())
this.tasksListManager(datas['result'])
} else if (datas['id'] == 3) {
//console.log('debug', 'MODULO PLAYER | MESSAGE MANAGER | DATA >>> ' + data.toString())
this.playListCuesManager(datas['result'])
} else if (datas['id'] == 100) {
//console.log('debug', 'MODULO PLAYER | MESSAGE MANAGER | DATA >>> ' + data.toString())
this.setPlayListCurrentCueIndex(datas['result'])
}
}
// TASK LIST
public tasksListManager(obj: any): void {
this.instance.tasksList = obj
//this.instance.log('info', 'MODULO PLAYER | TESK LIST MANAGER| TESKS LIST >>> ' + JSON.stringify(this.instance.tasksList))
this.instance.setPresetDefinitions(getPresets(this.instance))
}
// PLAY LISTS CUES
public playListCuesManager(obj: any): void {
const plInstance: any[] = this.instance.playLists
const plNew: any[] = obj
const checkPL = areJsonArraysEqual(plInstance, plNew)
if (checkPL) {
} else {
this.instance.playLists = obj
this.instance.setPresetDefinitions(getPresets(this.instance))
this.instance.updateInstance()
}
// this.instance.log(
// 'warn',
// 'MODULO PLAYER |PLAYLISTS & CUES MANAGER| PLAYLISTS >>> ' + JSON.stringify(this.instance.playLists, null, 4),
// )
// for (let plID = 0; plID < obj.length; plID++) {
// const cuesList: any[] = obj[plID]["cues"]
// // this.instance.log(
// // 'warn',
// // 'MODULO PLAYER |PLAYLISTS & CUES MANAGER | CUES LIST >>> ' + JSON.stringify(cuesList, null, 4),
// // )
// for (let cuesID = 0; cuesID < cuesList.length; cuesID++) {
// //const cue = cuesList[cuesID]
// // this.instance.log(
// // 'warn',
// // 'MODULO PLAYER |PLAYLISTS & CUES MANAGER | CUES >>> ' + JSON.stringify(cue, null, 4),
// // )
// // this.instance.setVariableDefinitions([
// // { variableId: `cue_${cue["uuid"]}_color`, name: 'Cue Color' },
// // // { variableId: 'variable2', name: 'My second variable' },
// // // { variableId: 'variable3', name: 'Another variable' },
// // ])
// }
// }
// this.instance.setPresetDefinitions(getPresets(this.instance))
// this.instance.updateInstance()
}
// GET CURRENT CUE INDEX
async getPlaylistsCurrentCues() {
this.instance.log('info', `MODULO PLAYER | GET PLAYLISTS CURRENT CUE !`)
this.instance.mpConnection?.sendMessage('get.list.playlists', 100)
}
async setPlayListCurrentCueIndex(obj: any) {
const pls: any[] = obj
for (let playlist = 0; playlist < pls.length; playlist++) {
let uuid: String = pls[playlist]['uuid']//.slice(1, -1)
this.instance.log('warn', `MODULO PLAYER | GET CURRENT INDEX >>> ${uuid} >>> ` + pls[playlist]['index'])
var obj: any = {
[`pl_${uuid}_currentIndex`]: parseInt(pls[playlist]['index']),
}
this.instance.states[`pl_${uuid}_currentIndex`] = parseInt(pls[playlist]['index'])
this.instance.setVariableValues(obj)
this.instance.checkFeedbacks()
}
}
async setGotoCue(plUUID: any, cueID: any) {
var m = `{
"jsonrpc": "2.0",
"method": "doaction.playlist",
"params": {
"uuid": "${plUUID}",
"action": "goto",
"cue": ${cueID}
},
"id": 110
}`
this.instance.mpConnection.sendJsonMessage(m)
}
async setGrandMasterFader(_pl: any, _value: any, _duration: any) {
var m = `{"jsonrpc":"2.0","method":"doaction.playlist",
"params": {
"uuid": "${_pl}",
"action": "grandMasterFader",
"value": ${_value},
"duration": ${_duration}
},"id": ${110}}`
this.instance.mpConnection.sendJsonMessage(m)
}
async getTaskListModuloPlayer() {
this.instance.log('info', 'GET TASKS LIST')
this.instance.mpConnection?.sendMessage('get.list.tasks', 1)
}
async getPlaylistModuloPlayer() {
this.instance.log('info', 'GET PLAY LIST')
this.instance.mpConnection?.sendMessagePlaylistsCues()
}
}
function areJsonArraysEqual(a: any[], b: any[]): boolean {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
if (JSON.stringify(a[i]) !== JSON.stringify(b[i])) {
return false
}
}
return true
}

168
src/mpconnection.ts Executable file
View File

@ -0,0 +1,168 @@
import { MPinstance } from './main.js'
//import { InstanceStatus } from '@companion-module/base'
import WebSocket from 'ws'
export class MPconnection {
instance: MPinstance
private websocket: WebSocket | undefined | null
private wsTimeout: NodeJS.Timeout | undefined
private mpAddr: string | undefined
private mpPort: any | undefined | null
private readonly reconnectmin = 100
private readonly reconnectmax = 16_500
private reconnectinterval = this.reconnectmin
private shouldBeConnected: boolean
//private pollAPI: NodeJS.Timeout | undefined
constructor(instance: MPinstance) {
this.instance = instance
this.shouldBeConnected = false
}
async connect(addr: string | undefined, port: any): Promise<void> {
this.mpAddr = addr
this.mpPort = port
this.instance!.log('debug', `WEBSOCKET MP CONNECT ${this.mpAddr} ${this.mpPort}`)
if (this.mpAddr === undefined || this.mpPort == undefined) return
this.shouldBeConnected = true
const urlObj = `ws://${this.mpAddr}:${this.mpPort}`
if (urlObj === null) return
try {
const setupMP = async () => {
this.websocket = new WebSocket(urlObj)
this.websocket.on('open', async () => {
this.reconnectinterval = this.reconnectmin
this.instance!.log('info', 'WEBSOCKET MP OPENED ' + this.websocket?.readyState)
this.instance.mpConnected = true
this.instance.isConnected()
//this.initPolling()
})
this.websocket.on('close', (ev: { toString: () => any }) => {
console.log(
'ws closed',
ev.toString(),
this.shouldBeConnected ? 'should be connected' : 'should not be connected',
)
if (this.shouldBeConnected) {
//this.instance.updateStatus(InstanceStatus.Disconnected)
this.instance.isConnected()
if (this.wsTimeout) clearTimeout(this.wsTimeout)
this.wsTimeout = setTimeout(() => {
this.connect(this.mpAddr, this.mpPort)
}, this.reconnectinterval)
this.reconnectinterval *= 1.2
if (this.reconnectinterval > this.reconnectmax) this.reconnectinterval = this.reconnectmax
}
})
this.websocket.on('error', (error: string) => {
this.instance.log('error', 'Socket ' + error)
this.instance.log('warn', 'Check if Modulo Player is started ?')
//this.instance.updateStatus(InstanceStatus.ConnectionFailure)
})
this.websocket.on('message', (data: { toString: () => string }) => {
//console.log('debug', 'incoming MP message ' + data.toString())
this.instance.moduloplayer?.messageManager(data.toString())
})
}
await setupMP()
} catch (error) {
this.disconnect()
if (this.wsTimeout) clearTimeout(this.wsTimeout)
this.wsTimeout = setTimeout(() => {
this.connect(this.mpAddr, this.mpPort)
}, this.reconnectinterval)
this.reconnectinterval *= 1.2
if (this.reconnectinterval > this.reconnectmax) this.reconnectinterval = this.reconnectmax
}
}
sendMessage(method: string, id: any): void {
if (this.websocket?.readyState === 1) {
var m = `{"jsonrpc":"2.0","method":"${method}","id": ${id}}`
this.websocket?.send(m)
this.instance.log('debug', 'SENDING WS MESSAGE ' + this.websocket.url + ' ' + m)
}
}
sendMessageLunchTask(uuid: any, id: any): void {
if (this.websocket?.readyState === 1) {
var m = `{"jsonrpc":"2.0","method":"doaction.task", "params": {
"uuid": "${uuid}",
"action": "launch"
},"id": ${id}}`
this.websocket?.send(m)
this.instance.log('debug', 'SENDING WS MESSAGE LAUNCH TASK ' + this.websocket.url + ' ' + m)
}
}
sendJsonMessage(message: String) {
if (this.websocket?.readyState === 1 && message !== '') {
this.websocket?.send(message)
this.instance.log('debug', 'SENDING WS MESSAGE LAUNCH TASK ' + this.websocket.url + ' ' + message)
}
}
sendMessagePlaylistsCues(): void {
if (this.websocket?.readyState === 1) {
var m = `{"jsonrpc":"2.0","method":"get.list.playlists",
"params": {
"level": "cue"},
"id": 3}`
this.websocket?.send(m)
this.instance.log('debug', 'SENDING WS MESSAGE GET PLAYLISTS CUES ' + this.websocket.url + ' ' + m)
}
}
disconnect(): void {
clearTimeout(this.wsTimeout)
// if (this.pollAPI !== undefined) {
// clearInterval(this.pollAPI)
// }
this.shouldBeConnected = false
this.websocket?.close()
this.instance.mpConnected = false
this.instance.isConnected()
}
destroy(): void {
clearTimeout(this.wsTimeout)
// if (this.pollAPI !== undefined) {
// clearInterval(this.pollAPI)
// }
this.shouldBeConnected = false
this.websocket = null
this.instance.mpConnected = false
this.instance.isConnected()
this.instance!.log('debug', 'Connection has been destroyed due to removal or disable by user')
}
// public readonly initPolling = (): void => {
// //this.instance.log('warn', `CONNECTION| INIT POLLING >>> ${this.pollAPI}`)
// if (this.pollAPI !== undefined) {
// clearInterval(this.pollAPI)
// }
// const pollAPI = () => {
// if (this.websocket?.readyState == 1) {
// this.instance.updatPolling()
// }
// }
// pollAPI()
// // Check if API Polling is disabled
// if (this.instance.config.pollInterval != 0) {
// const pollInterval = this.instance.config.pollInterval < 100 ? 100 : this.instance.config.pollInterval
// this.pollAPI = setInterval(pollAPI, pollInterval)
// }
// }
}

299
src/presets.ts Executable file
View File

@ -0,0 +1,299 @@
import {
combineRgb,
CompanionButtonPresetDefinition,
CompanionTextPresetDefinition,
CompanionPresetDefinitions,
} from '@companion-module/base'
import type { MPinstance } from './main.js'
export type PresetCategory = 'Tasks List' | 'PL'
const colorGrayLight = [100, 100, 100]
const textSize = 18
const colorOrangeMP = [255, 165, 0]
const colorGreenMP = [88, 201, 23]
export function hexToRgb(hex: any) {
const normal = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i)
if (normal) return normal.slice(1).map((e: string) => parseInt(e, 16))
const shorthand = hex.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/i)
if (shorthand) return shorthand.slice(1).map((e: string) => 0x11 * parseInt(e, 16))
return null
}
export type mpPreset = CompanionButtonPresetDefinition | CompanionTextPresetDefinition
type mpPresetArray = mpPreset[] | any
export function getPresets(instance: MPinstance): CompanionPresetDefinitions {
// TASK
const getTasksPresets = (): mpPresetArray => {
const tasksPresets: mpPresetArray = []
//instance.log("warn", "GET TASKS PRESETS 0 >>> " + instance.tasksList.length)
for (let task = 0; task < instance.tasksList.length; task++) {
let color = [0, 0, 0]
if (instance.tasksList[task]['uiColor'] !== '') {
color = hexToRgb(`${instance.tasksList[task]['uiColor']}`)
}
//instance.log("warn", "GET TASKS PRESETS COLOR >>> " + color)
tasksPresets.push({
category: `Tasks List` as PresetCategory,
name: `${instance.tasksList[task]['name']}`,
type: 'button',
style: {
text: `${instance.tasksList[task]['name']}`,
size: '24',
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(color[0], color[1], color[2]),
},
steps: [
{
down: [
{
actionId: 'launch_task',
options: { task: `${instance.tasksList[task]['uuid']}` },
},
],
up: [],
},
],
feedbacks: [
// {
// feedbackId: 'inputPreview',
// options: {
// mix: (mix - 1) as MixOptionEntry,
// mixVariable: '',
// input: input.toString(),
// fg: combineRgb(255, 255, 255),
// bg: combineRgb(0, 255, 0),
// tally: '',
// },
// },
],
})
}
//instance.log("warn", "GET TASKS PRESETS 1 >>> " + tasksPresets.length)
return tasksPresets
}
// PLAYLISTS CUES
const getPlayListsCuesPresets = (): mpPresetArray => {
const playlistsPresets: mpPresetArray = []
const pls: any[] = instance.playLists
//instance.log('warn', 'GET PLAYLISTS PRESETS >>> ' + JSON.stringify(pls, null, 4))
for (let playlist = 0; playlist < pls.length; playlist++) {
let cl: any[] = pls[playlist]['cues']
let uuid: String = pls[playlist]['uuid']//.slice(1, -1)
let plName = pls[playlist]['name']
//instance.log('warn', 'GET CUES PRESETS >>> ' + uuid)
for (let cue = 0; cue < cl.length; cue++) {
// COLOR
let color = [0, 0, 0]
if (cl[cue]['uiColor'] !== '') {
color = hexToRgb(`${cl[cue]['uiColor']}`)
}
//instance.log('warn', 'GET CUES PRESETS COLOR >>> ' + color)
// NAME
let n = cl[cue]['name']
if (n === '') {
n = `Cue ${cue + 1}`
}
//instance.log('warn', 'GET CUES PRESETS NAME >>> ' + n)
playlistsPresets.push({
category: `${pls[playlist]['name']}`,
name: n, //`${cl[cue]['name']}`,
type: 'button',
style: {
text: n, //`${cl[cue]['name']}`,
size: textSize,
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(color[0], color[1], color[2]),
},
steps: [
{
down: [
{
actionId: 'goto_cue',
options: { cue: `${cl[cue]['uuid']}`, pl: `${pls[playlist]['uuid']}`, index: `${cue + 1}` },
},
],
up: [],
},
],
feedbacks: [
{
feedbackId: 'color_cue',
options: {
current_Cue: cue + 1,
pl: uuid,
},
style: {
bgcolor: combineRgb(color[0], color[1], color[2]),
},
},
{
feedbackId: 'current_Cue',
options: {
current_Cue: cue + 1, // pls[playlist]['index'],
pl: uuid,
},
style: {
bgcolor: combineRgb(colorOrangeMP[0], colorOrangeMP[1], colorOrangeMP[2]),
},
},
],
})
}
// GRAND MASTER 0%
playlistsPresets.push({
category: `${pls[playlist]['name']}`,
name: `${plName} GM 0`,
type: 'button',
style: {
text: `${plName} GM\n00`,
size: textSize,
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(colorGrayLight[0], colorGrayLight[1], colorGrayLight[2]),
},
steps: [
{
down: [
{
actionId: 'pl_grand_master_fader',
options: { value: 0, duration: 2000, pl: `${pls[playlist]['uuid']}` },
},
],
up: [],
},
],
feedbacks: [],
})
// GRAND MASTER 100%
playlistsPresets.push({
category: `${pls[playlist]['name']}`,
name: `${plName} GM`,
type: 'button',
style: {
text: `${plName} GM\nFF`,
size: textSize,
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(colorGrayLight[0], colorGrayLight[1], colorGrayLight[2]),
},
steps: [
{
down: [
{
actionId: 'pl_grand_master_fader',
options: { value: 100, duration: 2000, pl: `${pls[playlist]['uuid']}` },
},
],
up: [],
},
],
feedbacks: [],
})
}
//instance.log("warn", "GET TASKS PRESETS 1 >>> " + tasksPresets.length)
return playlistsPresets
}
// VARIABLES CUES
const getVariablesPresets = (): mpPresetArray => {
const variablesPresets: mpPresetArray = []
variablesPresets.push({
category: `Variables`,
name: `$(Modulo_Player:serverName) Master`,
type: 'button',
style: {
text: `$(Modulo_Player:serverName) Master`,
size: textSize,
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(colorGreenMP[0], colorGreenMP[1], colorGreenMP[2]),
},
steps: [
{
down: [],
up: [],
},
],
feedbacks: [
{
feedbackId: 'master',
options: {
master: true,
},
style: {
text: `$(Modulo_Player:serverName) Master`,
},
},
{
feedbackId: 'master',
options: {
master: false,
},
style: {
text: `$(Modulo_Player:serverName) Slave`,
},
},
{
feedbackId: 'status',
options: {
status: 0,
},
style: {
bgcolor: combineRgb(255, 0, 0),
},
},
{
feedbackId: 'status',
options: {
status: 1,
},
style: {
bgcolor: combineRgb(colorOrangeMP[0], colorOrangeMP[1], colorOrangeMP[2]),
},
},
{
feedbackId: 'status',
options: {
status: 2,
},
style: {
bgcolor: combineRgb(colorGreenMP[0], colorGreenMP[1], colorGreenMP[2]),
},
},
],
})
variablesPresets.push({
category: `Variables`,
name: `$(Modulo_Player:serverName)`,
type: 'button',
style: {
text: `$(Modulo_Player:serverName)`,
size: textSize,
color: combineRgb(255, 255, 255),
bgcolor: combineRgb(instance.states['color'][0], instance.states['color'][1], instance.states['color'][2]),
},
steps: [
{
down: [],
up: [],
},
],
feedbacks: [],
})
return variablesPresets
}
const presets: mpPresetArray = [...getTasksPresets(), ...getPlayListsCuesPresets(), ...getVariablesPresets()]
return presets as unknown as CompanionPresetDefinitions
}

139
src/sdconnection.ts Executable file
View File

@ -0,0 +1,139 @@
import { MPinstance } from './main.js'
import { InstanceStatus } from '@companion-module/base'
import WebSocket from 'ws'
export class SDconnection {
instance: MPinstance
private websocket: WebSocket | undefined | null
private wsTimeout: NodeJS.Timeout | undefined
private mpAddr: string | undefined
private sdPort: any | null
private readonly reconnectmin = 100
private readonly reconnectmax = 16_500
private reconnectinterval = this.reconnectmin
private shouldBeConnected: boolean
private pollAPI: NodeJS.Timeout | undefined
constructor(instance: MPinstance) {
this.instance = instance
this.shouldBeConnected = false
}
async connect(addr: string | undefined, port: any): Promise<void> {
this.mpAddr = addr
this.sdPort = port
this.instance!.log('debug', `WEBSOCKET SD CONNECT ${this.mpAddr} ${this.sdPort}`)
if (this.mpAddr === undefined || this.sdPort == null) return
this.shouldBeConnected = true
const urlObj = `ws://${this.mpAddr}:${this.sdPort}`
if (urlObj === null) return
this.instance.updateStatus(InstanceStatus.Connecting, `Init Connection`)
try {
const setupMP = async () => {
this.websocket = new WebSocket(urlObj)
this.websocket.on('open', async () => {
this.reconnectinterval = this.reconnectmin
this.instance!.log('info', 'WEBSOCKET SPYDOG OPENED ' + this.websocket?.readyState)
this.instance.sdConnected = true
this.instance.isConnected()
this.initPolling()
})
this.websocket.on('close', (ev: { toString: () => any }) => {
console.log(
'ws closed',
ev.toString(),
this.shouldBeConnected ? 'should be connected' : 'should not be connected',
)
if (this.shouldBeConnected) {
this.instance.updateStatus(InstanceStatus.Disconnected)
if (this.wsTimeout) clearTimeout(this.wsTimeout)
this.wsTimeout = setTimeout(() => {
this.connect(this.mpAddr, this.sdPort)
}, this.reconnectinterval)
this.reconnectinterval *= 1.2
if (this.reconnectinterval > this.reconnectmax) this.reconnectinterval = this.reconnectmax
}
})
this.websocket.on('error', (error: string) => {
this.instance.log('error', 'Socket ' + error)
this.instance.log('error', 'Check if Modulo Player is started ?')
this.instance.updateStatus(InstanceStatus.ConnectionFailure)
})
this.websocket.on('message', (data: { toString: () => string }) => {
//this.instance.log('debug', 'INCOMMING SPYDOG MESSAGE ' + data.toString())
this.instance.spydog?.messageManager(data.toString())
})
}
await setupMP()
} catch (error) {
this.disconnect()
if (this.wsTimeout) clearTimeout(this.wsTimeout)
this.wsTimeout = setTimeout(() => {
this.connect(this.mpAddr, this.sdPort)
}, this.reconnectinterval)
this.reconnectinterval *= 1.2
if (this.reconnectinterval > this.reconnectmax) this.reconnectinterval = this.reconnectmax
}
}
sendJsonMessage(message: String) {
if (this.websocket?.readyState === 1 && message !== '') {
this.websocket?.send(message)
this.instance.log('debug', 'SENDING WS MESSAGE LAUNCH TASK ' + this.websocket.url + ' ' + message)
}
}
disconnect(): void {
clearTimeout(this.wsTimeout)
if (this.pollAPI !== undefined) {
clearInterval(this.pollAPI)
}
this.shouldBeConnected = false
this.websocket?.close()
this.instance.sdConnected = false
this.instance.isConnected()
}
destroy(): void {
clearTimeout(this.wsTimeout)
if (this.pollAPI !== undefined) {
clearInterval(this.pollAPI)
}
this.shouldBeConnected = false
this.websocket = null
this.instance.sdConnected = false
this.instance.isConnected()
this.instance!.log('debug', 'Connection has been destroyed due to removal or disable by user')
}
public readonly initPolling = (): void => {
//this.instance.log('warn', `CONNECTION| INIT POLLING >>> ${this.pollAPI}`)
if (this.pollAPI !== undefined) {
clearInterval(this.pollAPI)
}
const pollAPI = () => {
if (this.websocket?.readyState == 1) {
this.instance.updatPolling()
}
}
pollAPI()
// Check if API Polling is disabled
if (this.instance.config.pollInterval != 0) {
const pollInterval = this.instance.config.pollInterval < 100 ? 100 : this.instance.config.pollInterval
this.pollAPI = setInterval(pollAPI, pollInterval)
}
}
}

100
src/spydog.ts Executable file
View File

@ -0,0 +1,100 @@
import { MPinstance } from './main.js'
//import { getPresets } from './presets.js'
// JSON ID
// 1 = list Tasks,
// 2 = Launch Task,
// 3 = list Playlist
// 100 = CURRENT CUE LIST
// 110 = ACTION GOTO
// 200 = SPYDOG DYNAMIC INFO
// 201 = SPYDOG STATIC INFO
export function hexToRgb(hex: any) {
const normal = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i)
if (normal) return normal.slice(1).map((e: string) => parseInt(e, 16))
const shorthand = hex.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/i)
if (shorthand) return shorthand.slice(1).map((e: string) => 0x11 * parseInt(e, 16))
return null
}
export class SpyDog {
instance: MPinstance
constructor(instance: MPinstance) {
this.instance = instance
}
public messageManager(data: String): void {
const datas = JSON.parse(data.toString())
//this.instance.log('debug', 'MODULO SPYDOG | MESSAGE MANAGER | DATA ID >>> ' + datas['id'])
if (datas['id'] == 200) {
this.setDynamicInfo(datas['result'])
} else if (datas['id'] == 201) {
this.setStaticInfo(datas['result'])
}
}
async getStaticInfo() {
//this.instance.log('info', 'SPYDOG | GET STATIC INFO')
var m = `{"jsonrpc":"2.0", "method":"get.computer.static.info", "id": ${201}}`
this.instance.sdConnection.sendJsonMessage(m)
}
async getDynamicInfo() {
//this.instance.log('info', 'SPYDOG | GET DYNAMIC INFO')
var m = `{"jsonrpc":"2.0", "method":"get.computer.dynamic.info", "id": ${200}}`
this.instance.sdConnection.sendJsonMessage(m)
}
async setStaticInfo(objs: any) {
for (var key in objs[0]) {
// this.instance.log(
// 'info',
// `MODULO SPYDOG | SET STATIC INFO | ELEMENTS >>> ${key}: ${objs[0][key]} ${typeof objs[0][key]}`,
// )
var objTemp: any = {}
if (typeof objs[0][key] === "number") {
objTemp = { [`${key}`]: parseInt(objs[0][key]), }
} else {
objTemp = { [`${key}`]: objs[0][key], }
}
this.instance.states[`${key}`] = objs[0][key]
this.instance.setVariableValues(objTemp)
this.instance.checkFeedbacks(`${key}`)
}
this.instance.updateInstance()
}
async setDynamicInfo(objs: any) {
for (var key in objs[0]) {
// this.instance.log(
// 'info',
// `MODULO SPYDOG | SET DYNAMIC INFO | ELEMENTS >>> ${key}: ${objs[0][key]} ${typeof objs[0][key]}`,
// )
var objTemp: any = {}
if (typeof objs[0][key] === "number") {
objTemp = { [`${key}`]: parseInt(objs[0][key]), }
} else {
objTemp = { [`${key}`]: objs[0][key], }
}
if (`${key}` === "color") {
if (objs[0][key] == "transparent"){
objTemp = { [`${key}`]: hexToRgb("#000000"), }
this.instance.states[`${key}`] = hexToRgb("#000000")
} else {
objTemp = { [`${key}`]: hexToRgb(objs[0][key]), }
this.instance.states[`${key}`] = hexToRgb(objs[0][key])
}
} else {
this.instance.states[`${key}`] = objs[0][key]
}
this.instance.setVariableValues(objTemp)
this.instance.checkFeedbacks(`${key}`)
}
}
}

16
src/upgrades.ts Executable file
View File

@ -0,0 +1,16 @@
import type { CompanionStaticUpgradeScript } from '@companion-module/base'
import type { ModuloPlayConfig } from './configFields.js'
export const UpgradeScripts: CompanionStaticUpgradeScript<ModuloPlayConfig>[] = [
/*
* Place your upgrade scripts here
* Remember that once it has been added it cannot be removed!
*/
// function (context, props) {
// return {
// updatedConfig: null,
// updatedActions: [],
// updatedFeedbacks: [],
// }
// },
]

51
src/variables.ts Executable file
View File

@ -0,0 +1,51 @@
import type { MPinstance } from './main.js'
export function UpdateVariableDefinitions(instance: MPinstance): void {
instance.log('info', 'VARIABLES DEFINITIONS !')
const pls: any[] = instance.playLists
const variables = []
for (let playlist = 0; playlist < pls.length; playlist++) {
let uuid: String = pls[playlist]['uuid']//.slice(1, -1)
//instance.log('warn', `VARIABLES DEFINITIONS | GET CURRENT INDEX >>> ${uuid} >>> ` + pls[playlist]['index'])
variables.push({ variableId: `pl_${uuid}_currentIndex`, name: `${pls[playlist]['name']} Current Cue ` })
}
// SPYDOG STATIC INFO
variables.push({ variableId: 'CPU', name: 'CPU' })
variables.push({ variableId: 'GpuBrand', name: 'GPU Brand' })
variables.push({ variableId: 'GpuDriver', name: 'GPU Driver' })
variables.push({ variableId: 'GpuName', name: 'GPU Name' })
variables.push({ variableId: 'ModuloPlayer', name: 'ModuloPlayer' })
variables.push({ variableId: 'OS', name: 'OS' })
variables.push({ variableId: 'processorCount', name: 'Processor Count' })
variables.push({ variableId: 'totalMemory', name: 'Total Memory' })
// SPYDOG DYNAMIC INFO
variables.push({ variableId: 'clusterId', name: 'Cluster Id' })
variables.push({ variableId: 'color', name: 'Color' })
variables.push({ variableId: 'cpuTemperature', name: 'CPU Temperature' })
variables.push({ variableId: 'cpuUse', name: 'CPU Use' })
variables.push({ variableId: 'detacastTemperature', name: 'Detacast Temperature' })
variables.push({ variableId: 'fps', name: 'FPS' })
variables.push({ variableId: 'fpsOk', name: 'FPS Ok' })
variables.push({ variableId: 'gpuTemperature', name: 'GPU TEMPARATURE' })
variables.push({ variableId: 'lockStatus', name: 'Lock Status' })
variables.push({ variableId: 'master', name: 'Master' })
variables.push({ variableId: 'maxAutocalibOutputs', name: 'Max Autocalib Outputs' })
variables.push({ variableId: 'maxOutputs', name: 'Max Outputs' })
variables.push({ variableId: 'memoryUse', name: 'Memory Use' })
variables.push({ variableId: 'motherboardTemperature', name: 'Motherboard Temperature' })
variables.push({ variableId: 'serverIp', name: 'Server Ip' })
variables.push({ variableId: 'serverName', name: 'Server Name' })
variables.push({ variableId: 'serverTime', name: 'Server Time' })
variables.push({ variableId: 'status', name: 'Status' })
variables.push({ variableId: 'upTime', name: 'UP Time' })
instance.setVariableDefinitions(variables)
// instance.setVariableDefinitions([
// { variableId: 'variable1', name: 'My first variable' },
// { variableId: 'variable2', name: 'My second variable' },
// { variableId: 'variable3', name: 'Another variable' },
// ])
}

14
tsconfig.build.json Executable file
View File

@ -0,0 +1,14 @@
{
"extends": "@companion-module/tools/tsconfig/node18/recommended",
"include": ["src/**/*.ts"],
"exclude": ["node_modules/**", "src/**/*spec.ts", "src/**/__tests__/*", "src/**/__mocks__/*"],
"compilerOptions": {
"outDir": "./dist",
"baseUrl": "./",
"paths": {
"*": ["./node_modules/*"]
},
"module": "Node16",
"moduleResolution": "Node16"
}
}

8
tsconfig.json Executable file
View File

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.build.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules/**"],
"compilerOptions": {
"types": ["jest", "node"]
}
}

3179
yarn.lock Executable file

File diff suppressed because it is too large Load Diff