From 4231939d7d493cb3f5b1ef4a90a8f444016af985 Mon Sep 17 00:00:00 2001 From: Steve Therrien Date: Wed, 27 Oct 2021 19:34:22 -0700 Subject: [PATCH 1/4] Fix: Fixed problem creating new instance. Also now supports connecting to PVP by IP or hostname. --- HELP.md | 10 +++++----- index.js | 27 ++++++++++++--------------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/HELP.md b/HELP.md index d6b4112..ab45663 100644 --- a/HELP.md +++ b/HELP.md @@ -21,8 +21,8 @@ Open Preferences in PVP, switch to the Network tab, and enable network API suppo Enter in the IP address and port PVP is running on: -- **PVP IP**: The IP address of the PVP instance you want to control. -- **HTTPS Connection**: Check if PVP's `Use HTTPS Connection` is checked. +- **PVP Host**: The IP address or hostname of the PVP instance you want to control. +- **Use HTTPS** Check if PVP's `Use HTTPS Connection` is checked. - **Authentication Token**: The `Authentication Token` as shown in PVP's Network Preferences, **only if** PVP's `Require Authentication` is checked. - **Port**: The port PVP is running on. @@ -34,8 +34,8 @@ If you only have a single PVP instance, leave the following fields empty. If you have two PVP installations that are running in a primary/backup mode, you may want actions to go to each PVP install to keep them in sync. Instead of creating two PVP instances in Companion and adding the same action for both instances, just add the backup PVP's connection information and let the module handle the work for you: -- **PVP IP (Backup instance)**: The IP address of the backup PVP instance. -- **HTTPS Connection**: Check if the backup instance requires an `HTTP Connection`. +- **PVP Host (Backup instance)**: The IP address of the backup PVP instance. +- **Use HTTPS**: Check if the backup instance requires an `HTTP Connection`. - **Authentication Token**: The `Authentication Token` for the backup instance. - **Port**: The port the backup instance. @@ -163,4 +163,4 @@ The included Presets can be used to quickly create basic buttons for Layers and ------ -For additional actions, please raise a feature request on [GitHub](https://github.com/bitfocus/companion-module-pvp/). \ No newline at end of file +For additional actions, please raise a feature request on [GitHub](https://github.com/bitfocus/companion-module-renewedvision-pvp). \ No newline at end of file diff --git a/index.js b/index.js index 4928911..6c0fd9d 100644 --- a/index.js +++ b/index.js @@ -84,14 +84,14 @@ instance.prototype.config_fields = function() { { type: 'textinput', id: 'host', - label: 'PVP IP', + label: 'PVP Host', width: 4, - regex: self.REGEX_IP + required: true }, { type: 'checkbox', id: 'https', - label: 'HTTPS Connection', + label: 'Use HTTPS', width: 2, tooltip: 'Check if PVP requires an HTTPS connection.', default: false @@ -103,13 +103,12 @@ instance.prototype.config_fields = function() { width: 4 }, { - type: 'number', + type: 'textinput', id: 'port', label: 'Port', - min: 1, - max: 65535, width: 2, - required: true + required: true, + regex: this.REGEX_PORT }, { type: 'text', @@ -121,15 +120,14 @@ instance.prototype.config_fields = function() { { type: 'textinput', id: 'host_backup', - label: 'PVP IP (Backup instance)', + label: 'PVP Host (Backup instance)', width: 4, - // Regex borrowed from instance_skel's REGEX_IP, but made optional - regex: '/^((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))?$/' + required: false }, { type: 'checkbox', id: 'https_backup', - label: 'HTTPS Connection', + label: 'Use HTTPS', width: 2, tooltip: 'Check if PVP requires an HTTPS connection.', default: false @@ -141,13 +139,12 @@ instance.prototype.config_fields = function() { width: 4 }, { - type: 'number', + type: 'textinput', id: 'port_backup', label: 'Port', width: 2, - min: 1, - max: 65535, - required: false + required: false, + regex: this.REGEX_PORT, } ]; From 621d64acca5d23ac0db552af8aaa021adaf7f9f5 Mon Sep 17 00:00:00 2001 From: Steve Therrien Date: Wed, 27 Oct 2021 20:03:49 -0700 Subject: [PATCH 2/4] Feature: Added new actions. - Go to Layer Offset (Seconds) - Layer Transition Duration (Seconds) - Pause Layer - Play Layer - Skip Media in Layer (Seconds) - Workspace Transition Duration (Seconds) Resolves bitfocus/companion-module-renewedvision-pvp#14, resolves bitfocus/companion-module-renewedvision-pvp#15 --- HELP.md | 16 +++++-- index.js | 138 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 5 deletions(-) diff --git a/HELP.md b/HELP.md index ab45663..10e40d9 100644 --- a/HELP.md +++ b/HELP.md @@ -95,7 +95,7 @@ Number IDs are always interpreted as indexes, even if a layer or playlist has a | Action | Description | | --------------- | ------------------------------------------------------------ | -| Select Layer | Selects the layer in PVP.
Set `Target Layer`to `Yes` to make the layer the target of untargeted media. | +| Select Layer | Selects the layer in PVP.
Set `Target Layer`to `Yes` to make the layer the target of untargeted media. | | Select Playlist | Selects the playlist. | @@ -115,13 +115,19 @@ Number IDs are always interpreted as indexes, even if a layer or playlist has a | Action | Description | | ----------------------- | ------------------------------------------------------------ | +| Go to Layer Offset (Seconds) | Skips the layer's playing media to a specific offset. Positive numbers indicate the offset is from the start, and negative numbers from the end.
Examples: `5` moves to 5 seconds from the start. `-5` moves to 5 seconds from the end. `0` moves to the start. `-0` moves to the end. | | Layer Blend Mode | Sets the layer's blend mode. The default blend mode is `Normal`. | -| Layer Opacity | Sets the layer's opacity by percentage; a whole number from `0` to `100`.
You can also make relative opacity adjustments by prefixing the value with a `+` or `-`. | +| Layer Opacity | Sets the layer's opacity by percentage; a whole number from `0` to `100`.
You can also make relative opacity adjustments by prefixing the value with a `+` or `-`. | | Layer Preset | Applies a preset to the specified layer. Leave the `Preset Name` option empty to unlink the layer's preset. | -| Layer Target Set | Changes the layer's target set.
A PVP bug prevents target sets from being addressed by index. It can only be addressed by its name. | -| Layer Effect Preset | Sets the layer's effect preset by its name.
Leave the `Effect Preset Name` field empty to clear all effects. | +| Layer Target Set | Changes the layer's target set.
A PVP bug prevents target sets from being addressed by index. It can only be addressed by its name. | +| Layer Transition Duration (Seconds) | Sets the transition duration of a layer. | +| Layer Effect Preset | Sets the layer's effect preset by its name.
Leave the `Effect Preset Name` field empty to clear all effects. | +| Pause Layer | Pauses the media playing in the layer. | +| Play Layer | Plays/resumes the media playing in the layer. | +| Skip Media in Layer (Seconds) | Skips the layer's playing media back or forward some number of seconds. Negative _Seconds_ skips back.
Use decimals to skip back fractions of seconds; `-1.5` will skip back one-and-a-half seconds. | | Track Matte | (PVP 3.3+) Sets the layer's blend mode (how it blends with the layer immediately under it). Can't be used on the base layer. The `White Matte` mode doesn't support the `Invert Matte` option. | -| Workspace Effect Preset | Sets the workspace's effect preset by its name.
Leave the `Effect Preset Name` field empty to clear all effects. | +| Workspace Effect Preset | Sets the workspace's effect preset by its name.
Leave the `Effect Preset Name` field empty to clear all effects. | +| Workspace Transition Duration (Seconds) | Sets the transition duration of the workspace. | ## Error Codes diff --git a/index.js b/index.js index 6c0fd9d..8e99639 100644 --- a/index.js +++ b/index.js @@ -770,6 +770,101 @@ instance.prototype.actions = function(system) { default: '' } ] + }, + + + 'pauseLayer': { + label: "Pause Layer", + options: [ + { + type: 'textinput', + label: 'Layer ID', + id: 'idx', + default: '0' + } + ] + }, + + 'playLayer': { + label: "Play Layer", + options: [ + { + type: 'textinput', + label: 'Layer ID', + id: 'idx', + default: '0' + } + ] + }, + + 'goToLayerOffset': { + label: "Go to Layer Offset (Seconds)", + options: [ + { + type: 'textinput', + label: 'Layer ID', + id: 'idx', + default: '0' + }, + { + type: 'textinput', + label: 'Offset (+/- seconds)', + id: 'offset', + default: '-10', + regex : '/^-?\\d+(.\\d+)?$/' + } + ] + }, + + 'skipLayerSeconds': { + label: "Skip Media in Layer (Seconds)", + options: [ + { + type: 'textinput', + label: 'Layer ID', + id: 'idx', + default: '0' + }, + { + type: 'textinput', + label: 'Seconds (+/- seconds)', + id: 'sec', + default: '15.0', + regex : '/^-?\\d+(.\\d+)?$/' + } + ] + }, + + 'layerTransitionDuration': { + label: "Layer Transition Duration (Seconds)", + options: [ + { + type: 'textinput', + label: 'Layer ID', + id: 'idx', + default: '0' + }, + { + type: 'textinput', + label: 'Seconds (0-5 seconds)', + id: 'sec', + default: '1.0', + regex : '/^\\d+(.\\d+)?$/' + } + ] + }, + + 'workspaceTransitionDuration': { + label: "Workspace Transition Duration (Seconds)", + options: [ + { + type: 'textinput', + label: 'Seconds (0-5 seconds)', + id: 'sec', + default: '1.0', + regex : '/^\\d+(.\\d+)?$/' + } + ] } }); @@ -893,6 +988,8 @@ instance.prototype.action = function(action) { var self = this; var opt = action.options; + let sec; + try { switch (action.action) { @@ -1040,6 +1137,47 @@ instance.prototype.action = function(action) { self.doCommand('/layerBlend/layer/' + self.checkLayerId(opt.idx), trackMatte); return; + + case 'pauseLayer': + self.doCommand('/pause/layer/' + self.checkLayerId(opt.idx), { }); + return; + + case 'playLayer': + self.doCommand('/play/layer/' + self.checkLayerId(opt.idx), { }); + return; + + case 'skipLayerSeconds': + sec = parseFloat(opt.sec); + const isForwards = sec >= 0 ? 'true' : 'false'; + if (sec !== 0 && !isNaN(sec)) { + self.doCommand('/skip/layer/' + self.checkLayerId(opt.idx) + '?forwards='+isForwards + '&offset='+Math.abs(sec), { }); + } + return; + + case 'goToLayerOffset': + const offset = parseFloat(opt.offset); + // offset >= 0 track the video ahead from the start ("2" tracks to 2 seconds from the start of the video). + // offset < 0 track the video back from the end ("-2" tracks to seconds from the end of the video). + // "-0[.0]" is a special case (which is why it's matched as a string) and it tracks the video to the very end. + const goToDest = opt.offset[0] !== '-' ? 'goToStart' : 'goToEnd'; + if (!isNaN(offset)) { + self.doCommand('/'+goToDest+'/layer/' + self.checkLayerId(opt.idx) + '?offset='+Math.abs(offset), { }); + } + return; + + case 'layerTransitionDuration': + case 'workspaceTransitionDuration': + sec = parseFloat(opt.sec); + // API specifies the value must be between 0.0 and 5.0. + sec = Math.min(Math.max(0.0, sec), 5.0); + + if (action.action === 'layerTransitionDuration') { + self.doCommand('/transitionDuration/layer/' + self.checkLayerId(opt.idx), { "value" : sec }); + } else { + self.doCommand('/transitionDuration/workspace', { "value" : sec }); + } + return; + } } catch (err) { From cd8b6c66062a2ef73d84f650f1b427f98d1688a3 Mon Sep 17 00:00:00 2001 From: Steve Therrien Date: Fri, 29 Oct 2021 19:06:53 -0700 Subject: [PATCH 3/4] Fix: Improved new instance creation error checking/logging. Added two presets. Minor code cleanup (useless removed spaces). --- index.js | 99 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 22 deletions(-) diff --git a/index.js b/index.js index 8e99639..30b5358 100644 --- a/index.js +++ b/index.js @@ -46,16 +46,26 @@ instance.prototype.init = function() { instance.prototype.setPvpIps = function() { var self = this; - // Add the primary IP/port of PVP - self.arrTargets = [{ - host : self.config.host, - https : self.config.https || false, - port : self.config.port, - auth : self.config.auth || '' - }]; + self.arrTargets = []; + + var isPortValid = function(port) { + port = parseInt(port); + return !isNaN(port) && port > 1024 && port <= 65535; + } + + + if (self.config.host && isPortValid(self.config.port)) { + // Add the primary IP/port of PVP + self.arrTargets.push({ + host : self.config.host, + https : self.config.https || false, + port : self.config.port, + auth : self.config.auth || '' + }); + } // If a backup instance was defined, add it too. - if (self.config.host_backup && self.config.port_backup) { + if (self.config.host_backup && isPortValid(self.config.port_backup)) { self.arrTargets.push({ host : self.config.host_backup, https : self.config.https_backup || false, @@ -64,6 +74,10 @@ instance.prototype.setPvpIps = function() { }); } + if (self.arrTargets.length === 0) { + self.log('error', 'No valid PVP instances defined. Check host/port.'); + } + }; @@ -143,8 +157,7 @@ instance.prototype.config_fields = function() { id: 'port_backup', label: 'Port', width: 2, - required: false, - regex: this.REGEX_PORT, + required: false } ]; @@ -355,6 +368,49 @@ instance.prototype.init_presets = function () { ] }, + { + category: 'Layers', + label: 'This button will pause media playing on the selected Layer.', + bank: { + style: 'text', + text: 'Pause\\nLayer #', + size: 'auto', + color: self.rgb(255, 255, 255), + bgcolor: self.rgb(0, 0, 0), + latch: false + }, + actions: [ + { + action: 'pauseLayer', + options: { + idx: 0 + } + } + ] + }, + + { + category: 'Layers', + label: 'This button will play/resume media playing on the selected Layer.', + bank: { + style: 'text', + text: 'Play\\nLayer #', + size: 'auto', + color: self.rgb(255, 255, 255), + bgcolor: self.rgb(0, 0, 0), + latch: false + }, + actions: [ + { + action: 'playLayer', + options: { + idx: 0 + } + } + ] + }, + + /** * Presets for Workspace */ @@ -772,7 +828,6 @@ instance.prototype.actions = function(system) { ] }, - 'pauseLayer': { label: "Pause Layer", options: [ @@ -1149,7 +1204,7 @@ instance.prototype.action = function(action) { case 'skipLayerSeconds': sec = parseFloat(opt.sec); const isForwards = sec >= 0 ? 'true' : 'false'; - if (sec !== 0 && !isNaN(sec)) { + if (sec !== 0 && !isNaN(sec)) { self.doCommand('/skip/layer/' + self.checkLayerId(opt.idx) + '?forwards='+isForwards + '&offset='+Math.abs(sec), { }); } return; @@ -1160,7 +1215,7 @@ instance.prototype.action = function(action) { // offset < 0 track the video back from the end ("-2" tracks to seconds from the end of the video). // "-0[.0]" is a special case (which is why it's matched as a string) and it tracks the video to the very end. const goToDest = opt.offset[0] !== '-' ? 'goToStart' : 'goToEnd'; - if (!isNaN(offset)) { + if (!isNaN(offset)) { self.doCommand('/'+goToDest+'/layer/' + self.checkLayerId(opt.idx) + '?offset='+Math.abs(offset), { }); } return; @@ -1170,14 +1225,14 @@ instance.prototype.action = function(action) { sec = parseFloat(opt.sec); // API specifies the value must be between 0.0 and 5.0. sec = Math.min(Math.max(0.0, sec), 5.0); - + if (action.action === 'layerTransitionDuration') { self.doCommand('/transitionDuration/layer/' + self.checkLayerId(opt.idx), { "value" : sec }); } else { self.doCommand('/transitionDuration/workspace', { "value" : sec }); } return; - + } } catch (err) { @@ -1191,15 +1246,15 @@ instance.prototype.action = function(action) { * Checks if the ID is a number [interpreted as an index in PVP] and, if so, ensures it's not * lower than is allowed. PVP will crash if the ID is a negative index (or <= -2 for playlists, * since -1 means "Video Input"). - * + * * Crashes confirmed in PVP 3.3.2 (50528776) - * + * * @param id The ID to check. May be a a string [name], a UUID, or an integer * @param min The minimum integer to allow * @param errorMessage The error message to throw if the ID is an out of bounds index */ instance.prototype.checkId = function(id, min, errorMessage) { - + // Parse id to an integer. If it (as a string) matches id, then the value is a numeric index. let parsed = parseInt(id, 10); if (parsed.toString() !== id) { @@ -1212,13 +1267,13 @@ instance.prototype.checkId = function(id, min, errorMessage) { } return id; - + }; /** * Checks if the Layer ID is valid. - * + * * @param The ID to check */ instance.prototype.checkLayerId = function(id) { @@ -1228,7 +1283,7 @@ instance.prototype.checkLayerId = function(id) { /** * Checks if the Playlist ID is valid. - * + * * @param The ID to check */ instance.prototype.checkPlaylistId = function(id) { @@ -1238,7 +1293,7 @@ instance.prototype.checkPlaylistId = function(id) { /** * Checks if the Cue ID is valid. - * + * * @param The ID to check */ instance.prototype.checkCueId = function(id) { From 053098fbb63c7b18288127594c1a28490d683698 Mon Sep 17 00:00:00 2001 From: Steve Therrien Date: Fri, 29 Oct 2021 19:09:27 -0700 Subject: [PATCH 4/4] Updated HELP.md to note special layer/workspace duration case. Version bump. --- HELP.md | 12 +++++++++--- package.json | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/HELP.md b/HELP.md index 10e40d9..1712f3d 100644 --- a/HELP.md +++ b/HELP.md @@ -120,14 +120,20 @@ Number IDs are always interpreted as indexes, even if a layer or playlist has a | Layer Opacity | Sets the layer's opacity by percentage; a whole number from `0` to `100`.
You can also make relative opacity adjustments by prefixing the value with a `+` or `-`. | | Layer Preset | Applies a preset to the specified layer. Leave the `Preset Name` option empty to unlink the layer's preset. | | Layer Target Set | Changes the layer's target set.
A PVP bug prevents target sets from being addressed by index. It can only be addressed by its name. | -| Layer Transition Duration (Seconds) | Sets the transition duration of a layer. | +| Layer Transition Duration (Seconds) | Sets the transition duration of a layer. See _Transition Duration Note_ below. | | Layer Effect Preset | Sets the layer's effect preset by its name.
Leave the `Effect Preset Name` field empty to clear all effects. | | Pause Layer | Pauses the media playing in the layer. | | Play Layer | Plays/resumes the media playing in the layer. | | Skip Media in Layer (Seconds) | Skips the layer's playing media back or forward some number of seconds. Negative _Seconds_ skips back.
Use decimals to skip back fractions of seconds; `-1.5` will skip back one-and-a-half seconds. | | Track Matte | (PVP 3.3+) Sets the layer's blend mode (how it blends with the layer immediately under it). Can't be used on the base layer. The `White Matte` mode doesn't support the `Invert Matte` option. | | Workspace Effect Preset | Sets the workspace's effect preset by its name.
Leave the `Effect Preset Name` field empty to clear all effects. | -| Workspace Transition Duration (Seconds) | Sets the transition duration of the workspace. | +| Workspace Transition Duration (Seconds) | Sets the transition duration of the workspace. See _Transition Duration Note_ below. | + + +#### Transition Duration Note +Any layer or workspace that uses the `Default` transition in PVP shares that same duration property. For example, if **Layer 1**, **Layer 2** and **Workspace [Master]** use the `Default` transition, then changing the duration of any one of them will change all the others. + +Give each layer its own transition, like **Dissolve**, if you want to be able to control them separately. ## Error Codes @@ -169,4 +175,4 @@ The included Presets can be used to quickly create basic buttons for Layers and ------ -For additional actions, please raise a feature request on [GitHub](https://github.com/bitfocus/companion-module-renewedvision-pvp). \ No newline at end of file +For additional actions, please raise a feature request on [GitHub](https://github.com/bitfocus/companion-module-renewedvision-pvp). diff --git a/package.json b/package.json index 216389e..c5c5eb3 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "legacy": [ "pvp" ], - "version": "1.0.7", + "version": "1.0.8", "api_version": "1.0.0", "keywords": [ "Software"