├── .gitignore
├── companion
├── documentation
│ └── images
│ │ ├── Settings.png
│ │ ├── MediaBinName.png
│ │ ├── StreamDeck1.png
│ │ ├── ModuleVariables.png
│ │ ├── VideoInputName.png
│ │ ├── specific-slide.png
│ │ ├── ModuleStageSettings.png
│ │ ├── ModuleRequiredSettings.png
│ │ └── specific-slide-playlist-indexing.png
├── manifest.json
└── HELP.md
├── .github
├── workflows
│ └── companion-module-checks.yaml
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── package.json
├── LICENSE
├── README.md
├── actions.js
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | /pkg
3 | /pkg.tgz
--------------------------------------------------------------------------------
/companion/documentation/images/Settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bitfocus/companion-module-renewedvision-propresenter/HEAD/companion/documentation/images/Settings.png
--------------------------------------------------------------------------------
/companion/documentation/images/MediaBinName.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bitfocus/companion-module-renewedvision-propresenter/HEAD/companion/documentation/images/MediaBinName.png
--------------------------------------------------------------------------------
/companion/documentation/images/StreamDeck1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bitfocus/companion-module-renewedvision-propresenter/HEAD/companion/documentation/images/StreamDeck1.png
--------------------------------------------------------------------------------
/companion/documentation/images/ModuleVariables.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bitfocus/companion-module-renewedvision-propresenter/HEAD/companion/documentation/images/ModuleVariables.png
--------------------------------------------------------------------------------
/companion/documentation/images/VideoInputName.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bitfocus/companion-module-renewedvision-propresenter/HEAD/companion/documentation/images/VideoInputName.png
--------------------------------------------------------------------------------
/companion/documentation/images/specific-slide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bitfocus/companion-module-renewedvision-propresenter/HEAD/companion/documentation/images/specific-slide.png
--------------------------------------------------------------------------------
/companion/documentation/images/ModuleStageSettings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bitfocus/companion-module-renewedvision-propresenter/HEAD/companion/documentation/images/ModuleStageSettings.png
--------------------------------------------------------------------------------
/companion/documentation/images/ModuleRequiredSettings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bitfocus/companion-module-renewedvision-propresenter/HEAD/companion/documentation/images/ModuleRequiredSettings.png
--------------------------------------------------------------------------------
/companion/documentation/images/specific-slide-playlist-indexing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bitfocus/companion-module-renewedvision-propresenter/HEAD/companion/documentation/images/specific-slide-playlist-indexing.png
--------------------------------------------------------------------------------
/.github/workflows/companion-module-checks.yaml:
--------------------------------------------------------------------------------
1 | name: Companion Module Checks
2 |
3 | on:
4 | push:
5 |
6 | jobs:
7 | check:
8 | name: Check module
9 |
10 | if: ${{ !contains(github.repository, 'companion-module-template-') }}
11 |
12 | permissions:
13 | packages: read
14 |
15 | uses: bitfocus/actions/.github/workflows/module-checks.yaml@main
16 | # with:
17 | # upload-artifact: true # uncomment this to upload the built package as an artifact to this workflow that you can download and share with others
18 |
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "renewedvision-propresenter",
3 | "version": "3.0.2",
4 | "main": "index.js",
5 | "homepage": "https://github.com/bitfocus/companion-module-renewedvision-propresenter/blob/master/README.md#readme",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "license": "MIT",
10 | "dependencies": {
11 | "@companion-module/base": "~1.3.0",
12 | "ws": "^8.17.1"
13 | },
14 | "bugs": {
15 | "url": "https://github.com/bitfocus/companion-module-renewedvision-propresenter/issues"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/bitfocus/companion-module-renewedvision-propresenter.git"
20 | },
21 | "devDependencies": {
22 | "@companion-module/tools": "^1.2.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/companion/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "renewedvision-propresenter",
3 | "name": "renewedvision-propresenter",
4 | "shortname": "propresenter",
5 | "description": "A Companion Module for ProPresenter",
6 | "version": "3.0.2",
7 | "license": "MIT",
8 | "repository": "git+https://github.com/bitfocus/companion-module-renewedvision-propresenter.git",
9 | "bugs": "https://github.com/bitfocus/companion-module-renewedvision-propresenter/issues",
10 | "maintainers": [
11 | {
12 | "name": "Daniel Owen",
13 | "email": "greyshirtguy@gmail.com"
14 | },
15 | {
16 | "name": "Oliver Herrmann",
17 | "email": "oliver@monoxane.com"
18 | },
19 | {
20 | "name": "Jeffrey Davidsz",
21 | "email": "jeffrey.davidsz@vicreo.eu"
22 | }
23 | ],
24 | "legacyIds": ["propresenter6", "propresenter"],
25 | "runtime": {
26 | "type": "node18",
27 | "api": "nodejs-ipc",
28 | "apiVersion": "0.0.0",
29 | "entrypoint": "../index.js"
30 | },
31 | "manufacturer": "Renewed Vision",
32 | "products": ["ProPresenter"],
33 | "keywords": ["Software", "Presentation"]
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2018 Bitfocus AS, William Viker & Håkon Nessjøen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Report a bug/issue
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug/issue**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. eg: Setup a button with action (as per screenshots below)....
16 | 2. eg: Prepare a presentation etc (as per screenshots below)....
17 | 3. eg: Click Button with action....
18 | 4. eg: See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | Please add screenshots (or full text descriptions) showing:
25 | - ProPresenter playlist/presentations used
26 | - ProPresenter module config.
27 | - The config of any Buttons used.
28 | - Config of any variables used.
29 | - Config of any triggers used.
30 |
31 | **Versions/Environment (please complete the following information):**
32 | - ProPresenter Version [e.g. 7.9.2]
33 | - OS of ProPresenter Machine [e.g. MacOS 11.6/Windows 10]
34 | - Companion Version [e.g. 2.3 ]
35 | - OS of Companion machine [[e.g. MacOS 11.6/Windows 10]
36 | - Debug log output when issue occurs (Turn on debug log)
37 |
38 | **Any Other Context*
39 | Add any other context about the problem here. (eg it used to work when... etc)
40 | Attach presentation files if issue is only occuring with one presentation.
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Companion Module for Renewed Vision's [ProPresenter](https://renewedvision.com/propresenter/)
2 |
3 | See [HELP.md](https://github.com/bitfocus/companion-module-renewedvision-propresenter/blob/master/HELP.md) for instructions
4 |
5 | ## ⚠️ Known Issues (v2.5.5):
6 |
7 | - Pro7.9.2 (and above) on MacOS: Timers no longer feedback in the old way. To workaround this, enable the option "Timer Polling" in the module config.
8 | - Pro7 Windows - currentPresentation gives library path instead of playlist path!! This can affect some Pro7 Windows users when using new actions for slide by label and group slide. Need to determine a possible workaround as this will not be fixed upstream in Pro7 due to work on a new replacement API
9 |
10 | ## ⚠️ Reporting An Issue:
11 |
12 | All issues/bugs are reported in tracked in the [Issues List](https://github.com/bitfocus/companion-module-renewedvision-propresenter/issues) on the Github repo.
13 |
14 | - First, read the "Change Log" below to see if any new (Beta Builds) have been released after the version you are currently running that addresses your issue. Don't be afraid to download and test a Beta build as many people use the beta builds in production. However Do NOT run a live show without testing ALL your buttons/actions for ALL Companion modules you have installed.
15 | - If you cannot see your issue as a fix in a later version the click the link to visit the [Issues List](https://github.com/bitfocus/companion-module-renewedvision-propresenter/issues)
16 | - Read through all the current open Issues to see if your issue is already reported - If so, feel free to add more information to the existing issue - otherwise...
17 | - Create a new issue and please include lots of information with screenshots and instructions/steps for how to reproduce the issue.
18 | Please make sure to include the debug log at the time of the issue and version details for Companion, ProPresenter and your Operating System.
19 |
20 |
21 |
22 | ## 📝 Change Log:
23 |
24 | ### v2.6.0 (Companion v3)
25 |
26 | Changed syntaxt to Companion v3. Removed midi listening option.
27 |
28 | ### v2.5.5 (Companion Beta Build ???)
29 |
30 | - Added "Timer Polling" option to workaround issue for later versions (>=Pro7.9.2) on MacOS that stopped sending timer feedback - enable this to keep geeting timer feedback in those later versions.
31 | - Re-Added MIDI listener to module enable Companion button press (thanks to an updated easymidi 3.0.1 module).
32 | By default it is disabled - you need to enabled in module config.
33 | It allows you to send MIDI (Note-On) messages from ProPresenter to Companipn (this module) and it will press a Companion button where the Note-On value => Index of Page and the Note-On Intensity => Index of Button to press
34 |
35 | ### v2.5.4 (This was reverted/removed from beta builds)
36 |
37 | - Added MIDI listener to module enable Companion button press.
38 | By default it is disabled - you need to enabled in module config.
39 | It allows you to send MIDI (Note-On) messages from ProPresenter to Companipn (this module) and it will press a Companion button where the Note-On value => Index of Page and the Note-On Intensity => Index of Button to press
40 |
41 | ### v2.5.2 (Companion Build 2.2 RC?)
42 |
43 | - (Update) Add module variables: time_since_last_clock_update and connection_timer (Can be used to monitor for connection issues)
44 | - (Update) Naming update for WatchDogTimer (improve code clarity)
45 | - (Update) Improved/extra debug logging (esp important for diagnosing user connection issues)
46 | - (Bugfix) Stop emptying all current state when stage display connection drops (it only manages one var!)
47 |
48 | ### v2.5.1 (Companion Beta Build 3861)
49 |
50 | - (New) Supports targeting multiple groups in 'Specific Slide In A Group' action.
51 | - (New) New Action to generate a random number and store in module variable current_random_number.
52 | - (Update) minor update to tooltip text in some actions
53 | - (Update) Indexes in most actions support variable input.
54 | - (BugFix) Force refresh with 'presentationCurrent' after first connection is authenticated (to ensure we alway have presentationPath)
55 | - (Bugfix) Move self.updateVariable('current_presentation_path', String(objData.presentationPath)) to case 'presentationTriggerIndex':
56 | - (BugFix) Added missing self.updateVariable('current_presentation_path', objData.presentationPath) in case 'presentationCurrent':
57 | - (Bugfix) Bug fix for adding/subracting time to timer with negative value.
58 | - (Bugfix) Group Slide bugfix (added missing foundSlide Check)
59 |
60 | ### v2.5.0 (Companion Beta Build 3822)
61 |
62 | - Added this README.md
63 | - (New) Updated config UI to make more user friendly.
64 | - (New) Added config option to poll Looks to enable feedback of active look from Pro 7 on Windows - (Feedback already works for Pro7 on Mac without polling)
65 | - (New) New action "Specific Slide With Label" - Trigger a slide by specifiying the playlist name, presentation name and the custom slide label that has been applied to the slide. Matches first playlist, presentation and slide label found. Finally you can trigger a slide in a presentation - no matter where it is moved to! (Also works with variables)
66 | - (New) Added feedback for active Look
67 | - (New) New action "Specific Slide In A Group" - Trigger a slide in a specified group name by index (eg 1st slide of "Chorus", 1st slide of "Bridge"). Can target the current presentation or a even specific presentation using a presentationPath. (Also works with variables!)
68 | - (New) Added option for using either Name OR Index with many of the new Network link actions. If you you supply both an index and a name, the index will be used.
69 | - (Update) Removed "- beta" label from Network link actions.
70 | - (Bugfix) Minor bugfix with integer action parameters
71 |
72 | ### v2.4.6 (Companion Beta Build 3804)
73 |
74 | - (New) Module variables to correctly track presentations that target announcement layer: 'Current Announcement slide number' & 'Current Announcement Presentation Path'
75 | - (New) Config option to configure "Manual" Type of Presentation Info Requests that Pro7/Windows users can turn on to avoid performance issues when the option to "Send Presentation Info Requests To ProPresenter" is enabled.
76 | - (New/Bugfix) New config option to configure "Manual" Type of Presentation Info Requests that Pro7/Windows users can turn on to avoid performance issues when the option to "Send Presentation Info Requests To ProPresenter" is enabled.
77 | - (Bugfix) Incorrect values for current presentation path/slide/remaining slide when an annoucement presentation was running in background.
78 | - (Bugfix) Pro6/Windows failed to update vars for (watched/all) clock
79 |
80 | ### v2.4.5 (Companion Beta Build 3803)
81 |
82 | - (New) Added new variable for clock total seconds: 'pro7*clock_n_totalseconds' (where n = clock index) \_This is good for feedback - eg change colour when countdown timer value is <0*
83 | - (New) Allow + or - prefix to increment/decrement clockTimes (based on current value) in Update Clock action.
84 | - (Bugfix) Fixed bug with hourless clock variables (was dropping negative sign for negative times)
85 |
86 | ### v2.4.4 (Companion Beta Build 3791)
87 |
88 | - (Bugfix) Minor bugfix for two new beta actions (Clear Prop, Clear Message)
89 |
90 | ### v2.4.3 (Companion Beta Build 3785)
91 |
92 | - (New) You can now use module or custom variables in the Message TokenValues Field in a send Message action
93 | - (Bugfix) Fixed major bug in 2.4.2 where previous NetworkLink actions were being re-sent with subsquent normal actions (and visa-versa)
94 |
95 | ### v2.4.2 (Companion Beta Build 3766)
96 |
97 | - (New) Allow hostname in config for ProPresenter
98 | - (New) Add module var "current_presentation_path"
99 | - (New) Full support for dynamically added module vars for all stage screens and clocks (now show in Ui and can be used in triggers etc).
100 | - (New) Allow variables to be used in the "Specific Slide" action paremeters - This comes in handy if you store current_slide and current_present_path in custom vars and later use those custom vars as paremeters to recall the stored slide
101 | - (New) Add follwing BETA actions using new Network Link API:
102 | - Specific Slide (Network Link - Beta) - Trigger and slide by name
103 | - Prop Trigger (Network Link - Beta) - Trigger any Prop by name
104 | - Prop Clear (Network Link - Beta) - Clear any specific Prop by name
105 | - Message Clear (Network Link - Beta) - Clear any specifc Message by name
106 | - Trigger Media (Network Link - Beta) - Trigger any Media by name
107 | - Trigger Audio (Network Link - Beta) - Trigger any Audio by name
108 | - Trigger Video Input (Network Link - Beta) - Trigger any Video Input by name
109 | - Custom Action (Network Link - Beta) - Send custom JSON to custom Endpoint Path
110 | - (Bugfix) Fixed issue with follower beta feature not properly tracking when disconnected and causing issues.
111 |
112 | ### v2.4.1 (Companion Beta Build 3693)
113 |
114 | - (New) This version dynamically adds vars for all timers/clocks:
115 | - $(propresenter:pro7_clock_n) = hh:mm:ss for clock with index n
116 | - $(propresenter:pro7_clock_n_hourless) = mm:ss for clock with index n
117 | - (Still keeping old single var $(propresenter:watched_clock_current_time) for backwards compatibility in users setups)
118 |
119 | ### v2.4.0 (Companion Beta Build ????)
120 |
121 | - (New) Pro7.6+ supports triggering macros and setting looks via the remote protocol. Added actions for triggering Looks/Macros!
122 | - (New) Added module var for current_pro7_look_name
123 |
124 | ### v2.3.7 (Companion 2.1.4 Release Build)
125 |
126 | - (New) Added a customAction - where you can type JSON message to send to ProPresenter (allows user to create new action before this mofule is updated - advanced use only - as ProPresenter is easy to crash with invalid messsages!)
127 | - (Bugfix) Pro7.4.2 requires changes to API. Version must be at least 701 - or else connection is refused. Also: "presenationTriggerNext" (and Previous) must now include presentationDestination (which works fine with older versions of ProPresenter)
128 |
129 | ### v2.3.6 (Companion Beta Build ????)
130 |
131 | - (New) Leader-Follower beta config option to mimic Pro6 Master-Slave setup.
132 |
--------------------------------------------------------------------------------
/companion/HELP.md:
--------------------------------------------------------------------------------
1 | This Companion module allows you to remotely control ProPresenter 6 or 7 with the Elgato Stream Deck.
2 | Using this Companion module, each LCD button on your Stream Deck can be setup with custom text (or images) and ProPresenter actions.
3 |
4 | > See README.md for latest version history and instructions to report issues.
5 |
6 | 
7 | _Example. (You can also use Companion without a Stream Deck through it's web buttons/emulator)_
8 |
9 | ## Configure ProPresenter To Be Controlled By Companion:
10 |
11 | This Companion module will connect to ProPresenter via a network connection (using the same connection that the official mobile remote app uses).
12 | You can run Companion on the same computer as ProPresenter itself or on different computer(s) on the same network.
13 |
14 | 1. Open ProPresenter and then open the "ProPresenter" Menu.
15 | 2. Select "Preferences..." to open the ProPresenter Preferences window.
16 | 3. Select the Network tab to configure ProPresenter network preferences.
17 | 4. Check the "Enable Network" option (if it is not already enabled).
18 | 5. Check the "Enable ProPresenter Remote" option (if it is not already enabled).
19 | 6. Check the "Controller" option (if it is not already enabled).
20 | 7. Enter a controller password (or take note of the existing controller password).
21 | 8. Take a note of the Port number and IP Address.
22 | > Tip: If you want to connect to ProPresenter on the _same_ computer that you are running Companion on, you can use the special loopback IP address of 127.0.0.1 _Older versions of ProPresenter do not show the IP address. Use your knowledge of networking (or Google how) to get the IP address (or hostname) of the computer running ProPresenter_.
23 | 9. If you would like your Stream Deck to be able to display "Video CountDown" timers, then you will also need to check the option for "Enable Stage Display App" and enter a new password (or take note of the existing password).
24 | 10. Some of the newer actions in this module require the new network "Link" (Pro7.8+) to also be enabled (you don't need to join any group - just enable Network Link)
25 | > The items in red are bare minimum to get Companion working with ProPresenter 6/7. The items in orange are optional extra settings that enable more features and actions in this module:
26 | > 
27 |
28 | ### Performance Tip
29 |
30 | If the computers running ProPresenter and Companion are separate computers and they have the option of using either a wireless or a wired network connection, then it is recommended to use a wired network connection whenever possible. This is because a wired network connection is _typically_ more reliable and has better latency - which is nice for remote control.
31 |
32 | ## Configure the ProPresenter module in Companion
33 |
34 | Now you have all the info you need to go to the ProPresenter Companion module configuration and enter the IP address of the computer running ProPresenter as well as the ProPresenter port number and controller password that you took note of from ProPresenter network preferences.
35 | 
36 |
37 | If you chose to also enable the stage display app option in ProPresenter preferences (so your StreamDeck can display "Video Countdown" timers) then you can also select "Yes" for the configuration field "Connect to StageDisplay (Only required for video countdown timer)" and enter the stage display password.
38 | 
39 |
40 | > N.B. At the time of writing this module, there is a bug in ProPresenter 6 where if you choose to enter a Port number for the stage display app - it will actually ignore it and use the "main" network port you recorded in step 8 above.
41 |
42 | _⚠️⚠️⚠️⚠️ Alert For Pro7 Users ⚠️⚠️⚠️⚠️
43 | **Newer versions of Pro7** will quickly become unstable and drop the remote control connection when this module tries to make too many requests for presentation infomation.
44 | You need to turn this off if you are running newer versions of Pro7.
45 | The setting is called "Send Presentation Info Requests To ProPresenter" in the module configuration.
46 | Also, for Pro7.9.2 and above, you need to enable "Timer Polling" to get dynamic vars that track the current values of all timers.
47 | 
48 | Pro6 and older Pro7 versions can ignore these work-around setttings.
49 | Note that turning off "Send Presentation Info Requests To ProPresenter" will stop updating the dynamic variables: remaining_slides, total_slides and presentation_name.
50 | However, there is also another option to configure the **type** of the presentation request. Pro7 users on Windows may trial these on their system and find using the "Manual" type works well enough on their system without impacting performance - enabling them to leave the Send Presentation Info Requests To ProPresenter enabled and enjoy the extra dynamic variables it provides._
51 |
52 | # Actions (Commands) - What can you do with ProPresenter?
53 |
54 | ## Slides
55 |
56 | | Command | Description |
57 | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
58 | | Next Slide | Advances to the next slide in the current document. If at the end of a document, will advance to the start of the next document in the playlist. |
59 | | Previous Slide | Moves to the previous slide in the current document. If at the start of a document, will move to the start of the previous document in the playlist. |
60 | | Specific Slide | Moves to that presentation/slide number. Supports variables as inputs. See the `Notes About Specific Slide` section below. |
61 | | Specific Slide With Label | Trigger a specific slide by Playlist name, Presentation name and Slide Label. _Give a custom label to any slide and you can trigger it!_ |
62 | | Specific Slide In A Group | Trigger a specific slide of a given Group Name (eg Slide 1 of the group "Chorus"). Defaults to the current presentation - but you can specify a specific presentation if you want. _You need to click a slide in a presenation to make that presentation "Current" - Simply selecting a presentation without clicking any of its slides will NOT make it current._ |
63 | | Specific Slide (Network Link) | Trigger a specific slide by Playlist name, Presentation name and Slide Index. (Requires Pro7.8+ with Network Link enabled) |
64 |
65 | > ### Notes About Specific Slide
66 | >
67 | > This action has two parameters:
68 | >
69 | > **Slide Number**: Moves to the slide number specified.
70 | >
71 | > A whole number greater than 0 will move the presentation to that slide number.
72 | >
73 | > A slide number of `0` will trigger the current slide, which can be used to bring back a slide that was cleared using `Clear All` or `Clear Slide`.
74 | >
75 | > A relative number (prefixed with `+` or `-`) will move the presentation +/- that many slides. `+3` will jump ahead three slides, and `-2` will jump back two slides. Try to avoid using `-1` or `+1` relative numbers; use the `Next Slide` and `Previous Slide` actions instead, as they perform better.
76 | >
77 | > **Presentation Path**: Lets you trigger a slide in a different presentation, even from a different playlist.
78 | >
79 | > _Important: Presentation path numbering starts at 0, meaning `0` will trigger a slide in the first presentation in the playlist._
80 | >
81 | > A single number, like `3`, will let you trigger a slide in the _fourth_ presentation in the **current playlist**.
82 | >
83 | > A path like `1:3` will trigger presentation #4 in playlist #2.
84 | >
85 | > Note that you can use Companion custom or module variables in the Slide Number and Presentation Path fields.
86 | >
87 | > Playlists in groups (or nested groups) are identified using periods. `1.1.0:2` means "The second playlist is a group. The second item in that group is another group. Select the first playlist in that group. Choose the third presentation in the playlist."
88 | >
89 | > The below image may make this more clear:
90 | >
91 | > 
92 | >
93 | > Note: When indexing presentations in a playlist to determine the presentation path value, you must also include/count any headers.
94 | >
95 | > Example Playlist:
96 | >
97 | > - Header A (index=0)
98 | > - Presentation A (index=1)
99 | > - Header B (index=2)
100 | > - Presentation B (index=3)
101 | > - Presentation C (index=4)
102 |
103 | ## Pro7 Looks
104 |
105 | | Command | Description |
106 | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
107 | | Pro7 Set Look | Choose a Look to set live in Pro7. _The list of Looks is updated after you have connected to Pro7._
👀 Feedback is available to change button colors when a specific Look is live. |
108 |
109 | ## Pro7 Macros
110 |
111 | | Command | Description |
112 | | ---------------------------- | ---------------------------------------------------------------------------------------------------- |
113 | | Pro7 Trigger Macro | Choose a Macro to trigger in Pro7. _The list of Macros is updated after you have connected to Pro7._ |
114 |
115 | ## Audio Cues
116 |
117 | | Command | Description |
118 | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
119 | | Audio Start Cue | Start a specific audio cue in an audio-bin playlist. Uses the same numerical format to specify the path of the audio item (see Presentation Path explanation above) |
120 | | Audio Play/Pause | Pause (or resume playing) the currently playing (or paused) audio. |
121 | | Trigger Audio (Network Link) | Trigger any audio in the audio bin using audio Playlist Name, and _either_ the Index or Name of the audio file. If you specify both, Index will be used. (Requires Pro7.8+ with Network Link enabled) |
122 |
123 | ## Clear/Logo
124 |
125 | | Command | Description |
126 | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
127 | | Clear All | Clears all the layers |
128 | | Clear Audio | Clears the audio track |
129 | | Clear Background | Clears only the background layer |
130 | | Clear Slide | Clears the current slide (foreground and background) |
131 | | Clear Telestrator | Clears all annotations drawn with the telestrator |
132 | | Clear to Logo | Clears all the layers and shows the logo image set in ProPresenter |
133 | | Prop Clear (Network Link) | Clear a Prop, specified by _either_ the Index or Name of the Prop. If you specify both, Index will be used. (Requires Pro7.8+ with Network Link enabled) |
134 | | Message Clear (Network Link) | Clear a Message, specified by _either_ the Index or Name of the Message. If you specify both, Index will be used. (Requires Pro7.8+ with Network Link enabled) |
135 |
136 | > ### Clear All Notes
137 | >
138 | > Note: For some versions of ProPresenter, when the `Clear All` action is triggered against ProPresenter for Windows, the current slide will be lost but on Mac it's preserved.
139 | >
140 | > For example, if you're on slide #5, trigger `Clear All`, and then trigger `Next Slide`:
141 | >
142 | > - On Mac you'll be on slide #6
143 | > - On Windows, you'll be on slide #1
144 | >
145 | > You can work around this PC limitation by using the `Specific Slide` action with a relative slide number of `+1` to move to the next slide. This would move you to slide #6 after the `Clear All` action.
146 |
147 | ## Messages (On Output screen)
148 |
149 | | Command | Description |
150 | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
151 | | Show Message | Shows the message on the stage display output. You can pass values for message tokens, but you must do so very carefully - a typo here can crash ProPresenter. Crashes can cause data loss, and ruin your setup. Learn how to correctly enter message tokens by reading below. Always type carefully and double-check. Get it right on a test machine first! The correct way to pass values for message tokens is as two lists. The two lists work together to form token NAME and token VALUE pairs. The first list is a comma-separated list of token NAMES and the second is a comma-separated list of token VALUES. The number of items in each list should match - e.g. if you supply two token NAMES, then you should supply two token VALUES to make matching pairs. All token names in your list _MUST_ match the token names defined in the message within ProPresenter (or else Pro6 will likely crash). The token values can be any text. You don't have to pass _all_ the token names/values pairs - any name/values that you don't include will be treated as and displayed as blank. You don't have to pass any token names/values if you don't need to. Static messages without any tokens are safe - you can't make a typo if you leave the token names and token values list blank! If one of your token names or token values needs to have a comma in it, you can type a double comma (,,) to insert a literal comma - this works in either a token name or a token value. Again, make certain that your list of token NAMES perfectly match the names of the tokens defined in the message within Pro6 - Pro6 won't crash if they match perfectly - so be careful! Note that you can use a single Companion custom or module variables in the Token values field. |
152 | | Hide Message | Removes a message from output screen. |
153 |
154 | > Messages are identified by Index. Index is a 0-based, where the first message is 0, and then count up through the messages in the order shown in the list of ProPresenter messages.
155 |
156 | ## Stage Display
157 |
158 | | Command | Description |
159 | | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
160 | | Stage Display Message | Shows the message on the stage display output |
161 | | Stage Display Hide Message | Removes the stage display message |
162 | | Stage Display Layout | Sets the stage display layout for a selected stage screen.
👀 Feedback is available for the active stage display on each stage screen (and the watched stage screen if configured). |
163 |
164 | > In Pro6 Stage Displays are identified by index. Index is a 0-based number, where the first layout is 0 and then count up through the stage display layouts in the order shown in ProPresenters list of stage display layouts.
165 | > In Pro7 you can choose which screen and which stage display layout you want to set by name. (The dropdown list of name is NOT refreshed until after you have connected to PRo7).
166 |
167 | ## Clocks (Timers)
168 |
169 | | Command | Description |
170 | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
171 | | Start Clock | Starts clock (timer) - identified by index (0 based) |
172 | | Stop Clock | Stops clock (timer) - identified by index (0 based) |
173 | | Reset Clock | Resets clock (timer) - identified by index (0 based) |
174 | | Update Clock | Update clock/timer with a new duration - identified by index (0 based). You must specify the Name and Type of clock in an update action - as they always update the clock. Countdown durations etc are entered in format HH:MM:SS. You can optionally add a + or - prefix to add or subtract time to/from the current time of the clock. You may also use a shorthand format if you like. You can, if you want, leave out the HH and/or the MM values and they will default to zero - you can also leave out one or both of the ":" to enter just mins and/or seconds. You can control overrun for all clock types. AM/PM is only needed for Countdown To Time clocks. |
175 |
176 | > **Tip: One-Touch Preset CountDown Timers.**
177 | > If you use a lot of timers with commonly used values for duration, you might like to setup a few buttons that automatically reset and restart a count-down timer for your most commonly used durations. To make a single button do that for you, you can chain together the following three actions:
178 | >
179 | > 1. _Update CountDown Clock_ - Set new duration value of the count-down timer. This new value will be used when the timer is next reset.
180 | > 2. _Reset Clock_ - Stop the count-down timer if running and reset current value back to duration. You might like to add a little delay (say 100-300ms) to ensure > ProPresenter has time to process previous action.
181 | > 3. _Start Clock_ - Start the count-down timer running. You might like to add a little delay (say 100-300ms) to ensure ProPresenter has time to process previous action.
182 | >
183 | > Use increasing delay amounts (or relative delays) to ensure these three actions arrive in the correct order.
184 |
185 | ## Props, Media Bin and Audio Bin
186 |
187 | | Command | Description |
188 | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
189 | | Prop Trigger (Network Link - Beta) | Trigger a specifc Prop using _either_ the Index or Name of the Prop. If you specify both, Index will be used. (Requires Pro7.8+ with Network Link enabled) |
190 | | Trigger Media (Network Link - Beta) | Trigger a specific Media Item by Playlist name and _either_ the Index or Name of the Media. If you specify both, Index will be used. (Requires Pro7.8+ with Network Link enabled). |
191 | | Trigger Video Input (Network Link - Beta) | Trigger a specific Video Input by _either_ the Index or Name of the Video Input. If you specify both, Index will be used. (Requires Pro7.8+ with Network Link enabled) |
192 |
193 | > The name of an item in the Video Input Bin is the name under the thumbnail in the bin. (Right-click to rename)
> 
194 | >
195 | > The name of an item in Media Bin is the name under the thumbnail in the bin. (Right-click to rename)
> 
196 | > The playlist name is on left.
197 |
198 | ## Timeline
199 |
200 | | Command | Description |
201 | | ------------------------ | --------------------------------------------------------------------------------------------------------- |
202 | | Timeline Play/Pause | Toggle play/paused state of timeline for a specific presentation (See PresentationPath explanation above) |
203 | | Timeline Rewind | Rewind timeline for a specific presentation (See PresentationPath explanation above) |
204 |
205 | > Please Note: There is NO direct feedback from ProPresenter for when a timeline is playing or paused - so this cannot be shown to users on the StreamDeck!
206 |
207 | ## Custom Actions (Support Use Only)
208 |
209 | | Command | Description |
210 | | ----------------------------------- | ----------------------------------------------------------------------------------------- |
211 | | Custom Action | Send custom JSON to remote websocket (Support use only) |
212 | | Custom Action (Network Link - Beta) | Send custom JSON to custom endpoint (Pro7.8+ with Network Link enabled. Support use only) |
213 |
214 | # Dynamic Variables
215 |
216 | | Variable | Description |
217 | | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
218 | | $(propresenter:current_slide) | The number of the active slide (>= 1), or "N/A" if unknown. |
219 | | $(propresenter:current_presentation_path) | The presentation path of the current presentation. |
220 | | $(propresenter:total_slides) | The total number of slides in the current document, or "N/A" if unknown. |
221 | | $(propresenter:presentation_name) | The name of the current presentation, or "N/A" if unknown. |
222 | | $(propresenter:current_announcement_slide) | The number of the active slide on announcements layer (>= 1), or "N/A" if unknown. |
223 | | $(propresenter:current_announcement_presentation_pathh) | The presentation path of the current presentation on announcements layer |
224 | | $(propresenter:connection_status) | The current connection status to ProPresenter ("Disconnected" or "Connected"). |
225 | | $(propresenter:watched_clock_current_time) | In the config of this module, you can specify the index of a clock (timer) that you want to "watch". This dynamic variable will be updated once per second to the current value of the clock specified. You could use this to display a live timer value on a button! |
226 | | $(propresenter:current_stage_display_index) | Index of the currently selected stage display layout (This is updated whenever a new layout is selected.) |
227 | | $(propresenter:current_stage_display_name) | Name of the currently selected stage display layout (This is updated whenever a new layout is selected.) |
228 | | $(propresenter:video_countdown_timer) | Current value of video countdown timer - automatically updated when a video is playing. (This one variable is only updated when the module is configured to also connect to the Stage Display App port) |
229 | | $(propresenter:current_pro7_stage_layout_name) | The name of the current stage-display layout on the selected stage-display screen (as set in module config) |
230 | | $(propresenter:_StageScreenName_\_pro7_stagelayoutname) | The name of the current stage-display layout on the stage screen with name: "stageScreenName" (Case Sensitive) |
231 | | $(propresenter:pro7_clock_n) | hh:mm:ss for clock with index n |
232 | | $(propresenter:pro7_clock_n_hourless) | mm:ss for clock with index n |
233 | | $(propresenter:pro7_clock_n_totalseconds) | total seconds for clock with index n (can use this for feeback - eg update button colour when clock time <0) |
234 | | $(propresenter:current_random_number) | Current random number (update to new random number with action "New Random Number") |
235 | | $(propresenter:time_since_last_clock_update) | Number of milliseconds since module last heard a clock update message from ProPresenter. Normally sent every 1000ms. If none are received for longer than say 3000 milliseconds, then the connection has probably failed. |
236 | | $(propresenter:connection_timer) | Number of seconds that module has been connected to ProPresenter |
237 |
238 | > You can click the $ symbol in the module list to see the current values of the module variables.
239 | > 
240 |
241 | ## Tips:
242 |
243 | - You can use variables in button feebacks. For example, turning a button red when a timer eg. $(propresenter:pro7_clock_0_totalseconds) drops below 0.
244 | - There are internal actions that you can use set custom variables to modules variables - eg. You could create a "Store Current Slide" button to set custom variables to the current values of $(propresenter:current_presentation_path) and $(propresenter:current_slide) and then create a "Restore Previous Slide" buttons that uses those custom variables as parameters in a "Specific Slide" action to "go back" to the stored slide.
245 |
246 | ## Optional (Beta) Leader-Follower Feature
247 |
248 | You can optionally configure a second connection to a "Follower" ProPresenter 7 computer to have the module automatically forward slide trigger and clears actions to the Follower as you click on slides (or use clear actions) on the main ProPresenter computer (The Leader). This emulates the old Pro6 Master-Control module. It's not complete as some actions cannot be captured by the module to forward to the Follower (the remote protocol does not send notifications for every action - but as it improves, this module will be updated).or now basic slide triggers and some clear actions do work. - This won't really be updated any further as there is now a much better "Network Link" feature in Pro7.8+
249 |
--------------------------------------------------------------------------------
/actions.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | /**
4 | * Register the available actions with Companion.
5 | */
6 |
7 | const { Regex, InstanceStatus } = require('@companion-module/base')
8 | const ActionId = {
9 | enableFollowerControl: 'enableFollowerControl',
10 | next: 'next',
11 | last: 'last',
12 | slideNumber: 'slideNumber',
13 | slideLabel: 'slideLabel',
14 | groupSlide: 'groupSlide',
15 | clearall: 'clearall',
16 | clearslide: 'clearslide',
17 | clearprops: 'clearprops',
18 | clearaudio: 'clearaudio',
19 | clearbackground: 'clearbackground',
20 | cleartelestrator: 'cleartelestrator',
21 | cleartologo: 'cleartologo',
22 | clearAnnouncements: 'clearAnnouncements',
23 | clearMessages: 'clearMessages',
24 | stageDisplayLayout: 'stageDisplayLayout',
25 | pro7StageDisplayLayout: 'pro7StageDisplayLayout',
26 | pro7SetLook: 'pro7SetLook',
27 | pro7TriggerMacro: 'pro7TriggerMacro',
28 | stageDisplayMessage: 'stageDisplayMessage',
29 | stageDisplayHideMessage: 'stageDisplayHideMessage',
30 | clockStart: 'clockStart',
31 | clockStop: 'clockStop',
32 | clockReset: 'clockReset',
33 | clockUpdate: 'clockUpdate',
34 | messageHide: 'messageHide',
35 | messageSend: 'messageSend',
36 | audioStartCue: 'audioStartCue',
37 | audioPlayPause: 'audioPlayPause',
38 | timelinePlayPause: 'timelinePlayPause',
39 | timelineRewind: 'timelineRewind',
40 | customAction: 'customAction',
41 | nwSpecificSlide: 'nwSpecificSlide',
42 | nwPropTrigger: 'nwPropTrigger',
43 | nwPropClear: 'nwPropClear',
44 | nwMessageClear: 'nwMessageClear',
45 | nwTriggerMedia: 'nwTriggerMedia',
46 | nwTriggerAudio: 'nwTriggerAudio',
47 | nwVideoInput: 'nwVideoInput',
48 | newRandomNumber: 'newRandomNumber',
49 | nwCustom: 'nwCustom',
50 | }
51 |
52 | const sendNwCommand = async (nwCmd) => {
53 | nwCmd.data.connection = { rejectUnauthorized: false } // Add this header now, in case of a change to https with invalid certs in future.
54 | this.instance.log(
55 | 'debug',
56 | `Sending: http://${this.config.host}:${this.config.port}${nwCmd.endpointPath} ${JSON.stringify(nwCmd.data)}`
57 | )
58 | // Perform actions that use the new NetworkLink API (These actions are considered beta functionality until the new API is finalized by RV)
59 | const res = await fetch(`http://${this.config.host}:${this.config.port}${nwCmd.endpointPath}`, {
60 | body: JSON.stringify(nwCmd.data),
61 | })
62 | if (res.ok) {
63 | const data = await res.json()
64 | this.instance.log('debug', JSON.stringify(data))
65 | }
66 |
67 | // // fetch()
68 | // this.system.emit(
69 | // 'rest',
70 | // 'http://' + this.config.host + ':' + this.config.port + nwCmd.endpointPath,
71 | // JSON.stringify(nwCmd.data),
72 | // function (err, result) {
73 | // this.instance.log('debug', 'nwCMD.path: ' + nwCmd.endpointPath + ' nwCmd.data: ' + JSON.stringify(nwCmd.data))
74 | // },
75 | // {},
76 | // { connection: { rejectUnauthorized: false } } // Add this header now, in case of a change to https with invalid certs in future.
77 | // )
78 | }
79 |
80 | const sendCommand = async (cmd) => {
81 | // Perform actions that use the current ProRemote API (Websocket)
82 | if (this.instance.currentStatus !== InstanceStatus.UnknownError) {
83 | // Is this the correct check?
84 | try {
85 | const cmdJSON = JSON.stringify(cmd)
86 | this.instance.log('debug', 'Sending JSON: ' + cmdJSON)
87 | this.instance.socket.send(cmdJSON)
88 | } catch (e) {
89 | this.instance.log('debug', 'NETWORK ' + e)
90 | this.instance.updateStatus(InstanceStatus.UnknownError, e.message)
91 | }
92 | } else {
93 | this.instance.log('debug', 'Socket not connected :(')
94 | this.instance.updateStatus(InstanceStatus.ConnectionFailure, 'not connected')
95 | }
96 | }
97 |
98 | module.exports = {
99 | GetActions: (instance) => {
100 | this.instance = instance
101 |
102 | /**
103 | * @type{import('@companion-module/base').CompanionActionDefinitions}
104 | */
105 | const actions = {
106 | [ActionId.next]: {
107 | name: 'Next Slide',
108 | options: [],
109 | callback: async () => {
110 | const cmd = {
111 | action: 'presentationTriggerNext',
112 | presentationDestination: '0', // Pro7.4.2 seems to need this now!
113 | }
114 | await sendCommand(cmd)
115 | },
116 | },
117 | [ActionId.last]: {
118 | name: 'Previous Slide',
119 | options: [],
120 | callback: async () => {
121 | const cmd = {
122 | action: 'presentationTriggerPrevious',
123 | presentationDestination: '0', // Pro7.4.2 seems to need this now!
124 | }
125 | await sendCommand(cmd)
126 | },
127 | },
128 | [ActionId.slideNumber]: {
129 | name: 'Specific Slide',
130 | options: [
131 | {
132 | type: 'textinput',
133 | useVariables: true,
134 | label: 'Slide Number',
135 | id: 'slide',
136 | default: '1',
137 | tooltip: '(Supports variable)',
138 | regex: Regex.SIGNED_NUMBER,
139 | },
140 | {
141 | type: 'textinput',
142 | useVariables: true,
143 | label: 'Presentation Path',
144 | id: 'path',
145 | default: '',
146 | tooltip: 'See the README for more information (Supports variable)',
147 | regex: '/^$|^\\d+$|^\\d+(\\.\\d+)*:\\d+$/',
148 | },
149 | ],
150 | callback: async (action, context) => {
151 | let index = this.instance.currentState.internal.slideIndex // Start with current slide (allows relative jumps using+-)
152 |
153 | // Allow parsing of optional variable in the slide textfield as int
154 | // Picking a var from the dropdown seems to add a space on end (use trim() to ensure field is a just a clean variable)
155 | const optSlideIndex = await context.parseVariablesInString(String(action.options.slide).trim())
156 |
157 | if (action.options.slide[0] === '-' || action.options.slide[0] === '+') {
158 | // Move back/forward a relative number of slides.
159 | index += parseInt(action.options.slide.substring(1), 10) * (action.options.slide[0] === '+' ? 1 : -1)
160 | index = Math.max(0, index)
161 | } else {
162 | // Absolute slide number. Convert to an index.
163 | index = parseInt(optSlideIndex) - 1
164 | }
165 |
166 | if (index < 0) {
167 | // Negative slide indexes are invalid. In such a case use the current slideIndex.
168 | // This allows the "Specific Slide", when set to 0 (thus the index is -1), to
169 | // trigger the current slide again. Can be used to bring back a slide after using
170 | // an action like 'clearAll' or 'clearText'.
171 | index = this.instance.currentState.internal.slideIndex
172 | }
173 |
174 | // Allow parsing of optional variable in the presentationPath textfield as string
175 | // Picking a var from the dropdown seems to add a space on end (use trim() to ensure field is a just a clean variable)
176 | const optPath = await context.parseVariablesInString(String(action.options.path).trim())
177 |
178 | let presentationPath = this.instance.currentState.internal.presentationPath // Default to current stored presentationPath
179 | // TODO: Pro7 Win workaround: If current path is C:/*.pro then find matching path in all playlists and use that instead!
180 | // This users cannot use specific slide with blank path to target presentations in the library (if a match can be found in a playlist we will always assume that is the intention)
181 | // Also, the first match will be win every time - (if the same presentation is in in mulitple playlists)
182 | if (action.options.path !== undefined && String(action.options.path).match(/^\d+$/) !== null) {
183 | // Is a relative presentation path. Refers to the current playlist, so extract it
184 | // from the current presentationPath and append the action.options.path to it.
185 | presentationPath = presentationPath.split(':')[0] + ':' + action.options.path
186 | } else if (action.options.path !== '') {
187 | // Use the path provided. The option's regex validated the format.
188 | presentationPath = optPath
189 | }
190 |
191 | const cmd = {
192 | action: 'presentationTriggerIndex',
193 | slideIndex: String(index),
194 | // Pro 6 for Windows requires 'presentationPath' to be set.
195 | presentationPath: presentationPath,
196 | }
197 | await sendCommand(cmd)
198 | },
199 | },
200 | [ActionId.slideLabel]: {
201 | name: 'Specific Slide With Label',
202 | options: [
203 | {
204 | type: 'textinput',
205 | useVariables: true,
206 | label: 'Playlist Name',
207 | tooltip: 'Find the first playlist with that matches this playlist name (Supports variable)',
208 | id: 'playlistName',
209 | },
210 | {
211 | type: 'textinput',
212 | useVariables: true,
213 | label: 'Presentation Name',
214 | tooltip:
215 | 'Find the first presentation (in above playlist) that matches this presentation name (Supports variable or text with wildcard char *)',
216 | id: 'presentationName',
217 | },
218 | {
219 | type: 'textinput',
220 | useVariables: true,
221 | label: 'Slide With Label',
222 | tooltip:
223 | 'Find the first slide (in above presentation) with matching *Slide Label* and trigger that slide (Supports variable)',
224 | id: 'slideLabel',
225 | },
226 | ],
227 | callback: async (action, context) => {
228 | // Allow parsing of optional variables in all input fields for this action
229 | const playlistName = await context.parseVariablesInString(String(action.options.playlistName).trim())
230 | const presentationName = await context.parseVariablesInString(String(action.options.presentationName).trim())
231 | const slideLabel = await context.parseVariablesInString(String(action.options.slideLabel).trim())
232 |
233 | // Add new request to internal state and issue request for all playlists (later, code the handles response will see the request stored in internal state and perform the work to complete it)
234 | const newSlideByLabelRequest = {
235 | playlistName: playlistName,
236 | presentationName: presentationName,
237 | slideLabel: slideLabel,
238 | }
239 | this.instance.currentState.internal.awaitingSlideByLabelRequest = newSlideByLabelRequest
240 |
241 | const cmd = {
242 | action: 'playlistRequestAll',
243 | }
244 | await sendCommand(cmd)
245 | },
246 | },
247 | [ActionId.groupSlide]: {
248 | name: 'Specific Slide In A Group',
249 | options: [
250 | {
251 | type: 'textinput',
252 | useVariables: true,
253 | label: 'Group(s) Name',
254 | tooltip:
255 | 'Specify the Name of the Group with the slide you want to trigger (Supports variable or multiple group names separated by |)',
256 | id: 'groupName', // Supports multiple group names with | separator
257 | },
258 | {
259 | type: 'textinput',
260 | useVariables: true,
261 | label: 'Slide Number (Within Group)',
262 | default: '1',
263 | tooltip: 'Which slide in the group? (Supports variable)',
264 | id: 'slideNumber',
265 | regex: Regex.NUMBER,
266 | },
267 | {
268 | type: 'textinput',
269 | useVariables: true,
270 | label: 'Presentation Path (Leave Blank for Current)',
271 | id: 'presentationPath',
272 | default: '',
273 | tooltip: 'Leave this blank to target the current presentation (Supports variable)',
274 | regex: '/^$|^\\d+$|^\\d+(\\.\\d+)*:\\d+$/',
275 | },
276 | ],
277 | callback: async (action, context) => {
278 | // Allow parsing of optional variables in all input fields for this action
279 | const groupName = await context.parseVariablesInString(String(action.options.groupName).trim())
280 | const slideNumber = await context.parseVariablesInString(String(action.options.slideNumber).trim())
281 | let presentationPath = await context.parseVariablesInString(String(action.options.presentationPath).trim())
282 |
283 | // If presentationPath was blank then auto set to current presentation.
284 | if (presentationPath.length == 0) {
285 | presentationPath = this.instance.currentState.dynamicVariables['current_presentation_path']
286 | }
287 |
288 | if (presentationPath !== undefined && presentationPath !== 'undefined' && presentationPath.length > 0) {
289 | // Add new request to internal state and issue presentationRequest (later, code the handles the "presentationCurrent" response will see the request stored in internal state and perform the work to complete it)
290 | const newGroupSlideRequest = {
291 | groupName: groupName,
292 | slideNumber: slideNumber,
293 | presentationPath: presentationPath,
294 | }
295 | this.instance.currentState.internal.awaitingGroupSlideRequest = newGroupSlideRequest
296 |
297 | const cmd = {
298 | action: 'presentationRequest',
299 | presentationPath: presentationPath,
300 | presentationSlideQuality: 0,
301 | }
302 | await sendCommand(cmd)
303 | }
304 | },
305 | },
306 | [ActionId.clearall]: {
307 | name: 'Clear All',
308 | options: [],
309 | callback: async () => {
310 | const cmd = {
311 | action: 'clearAll',
312 | }
313 | await sendCommand(cmd)
314 | },
315 | },
316 | [ActionId.clearslide]: {
317 | name: 'Clear Slide',
318 | options: [],
319 | callback: async () => {
320 | const cmd = {
321 | action: 'clearText',
322 | }
323 | await sendCommand(cmd)
324 | },
325 | },
326 | [ActionId.clearprops]: {
327 | name: 'Clear Props',
328 | options: [],
329 | callback: async () => {
330 | const cmd = {
331 | action: 'clearProps',
332 | }
333 | await sendCommand(cmd)
334 | },
335 | },
336 | [ActionId.clearaudio]: {
337 | name: 'Clear Audio',
338 | options: [],
339 | callback: async () => {
340 | const cmd = {
341 | action: 'clearAudio',
342 | }
343 | await sendCommand(cmd)
344 | },
345 | },
346 | [ActionId.clearbackground]: {
347 | name: 'Clear Background',
348 | options: [],
349 | callback: async () => {
350 | const cmd = {
351 | action: 'clearVideo',
352 | }
353 | await sendCommand(cmd)
354 | },
355 | },
356 | [ActionId.cleartelestrator]: {
357 | name: 'Clear Telestrator',
358 | options: [],
359 | callback: async () => {
360 | const cmd = {
361 | action: 'clearTelestrator',
362 | }
363 | await sendCommand(cmd)
364 | },
365 | },
366 | [ActionId.cleartologo]: {
367 | name: 'Clear to Logo',
368 | options: [],
369 | callback: async () => {
370 | const cmd = {
371 | action: 'clearToLogo',
372 | }
373 | await sendCommand(cmd)
374 | },
375 | },
376 | [ActionId.clearAnnouncements]: {
377 | name: 'Clear Announcements',
378 | options: [],
379 | callback: async () => {
380 | const cmd = {
381 | action: 'clearAnnouncements',
382 | }
383 | await sendCommand(cmd)
384 | },
385 | },
386 | [ActionId.clearMessages]: {
387 | name: 'Clear Messages',
388 | options: [],
389 | callback: async () => {
390 | const cmd = {
391 | action: 'clearMessages',
392 | }
393 | await sendCommand(cmd)
394 | },
395 | },
396 | [ActionId.stageDisplayLayout]: {
397 | name: 'Pro6 Stage Display Layout',
398 | options: [
399 | {
400 | type: 'textinput',
401 | label: 'Pro6 Stage Display Index',
402 | id: 'index',
403 | default: '0',
404 | regex: Regex.NUMBER,
405 | },
406 | ],
407 | callback: async (action) => {
408 | const cmd = {
409 | action: 'stageDisplaySetIndex',
410 | stageDisplayIndex: String(action.options.index),
411 | }
412 | await sendCommand(cmd)
413 | },
414 | },
415 | [ActionId.pro7StageDisplayLayout]: {
416 | name: 'Pro7 Stage Display Layout',
417 | options: [
418 | {
419 | type: 'dropdown',
420 | label: 'Pro7 Stage Display Screen',
421 | id: 'pro7StageScreenUUID',
422 | tooltip: 'Choose which stage display screen you want to update layout',
423 | default: '',
424 | choices: this.instance.currentState.internal.pro7StageScreens,
425 | },
426 | {
427 | type: 'dropdown',
428 | label: 'Pro7 Stage Display Layout',
429 | id: 'pro7StageLayoutUUID',
430 | tooltip: 'Choose the new stage display layout to apply',
431 | default: '',
432 | choices: this.instance.currentState.internal.pro7StageLayouts,
433 | },
434 | ],
435 | callback: async (action) => {
436 | // If either option is null, then default to using first items from each list kept in internal state.
437 | const cmd = {
438 | action: 'stageDisplayChangeLayout',
439 | stageScreenUUID: action.options.pro7StageScreenUUID
440 | ? action.options.pro7StageScreenUUID
441 | : this.instance.currentState.internal.pro7StageScreens[0].id,
442 | stageLayoutUUID: action.options.pro7StageLayoutUUID
443 | ? action.options.pro7StageLayoutUUID
444 | : this.instance.currentState.internal.pro7StageLayouts[0].id,
445 | }
446 | await sendCommand(cmd)
447 | },
448 | },
449 | [ActionId.pro7SetLook]: {
450 | name: 'Pro7 Set Look',
451 | options: [
452 | {
453 | type: 'dropdown',
454 | label: 'Look',
455 | id: 'pro7LookUUID',
456 | tooltip: 'Choose which Look to make live',
457 | default: '',
458 | choices: this.instance.currentState.internal.pro7Looks,
459 | },
460 | ],
461 | callback: async (action) => {
462 | // If selected Look is null, then default to using first Look from list kept in internal state
463 | const cmd = {
464 | action: 'looksTrigger',
465 | lookID: action.options.pro7LookUUID
466 | ? action.options.pro7LookUUID
467 | : this.instance.currentState.internal.pro7Looks[0].id,
468 | }
469 | await sendCommand(cmd)
470 | },
471 | },
472 | [ActionId.pro7TriggerMacro]: {
473 | name: 'Pro7 Trigger Macro',
474 | options: [
475 | {
476 | type: 'dropdown',
477 | label: 'Macro',
478 | id: 'pro7MacroUUID',
479 | tooltip: 'Choose which Macro to trigger',
480 | default: '',
481 | choices: this.instance.currentState.internal.pro7Macros,
482 | },
483 | ],
484 | callback: async (action) => {
485 | // If selected Macro is null, then default to using first Macro from list kept in internal state
486 | const cmd = {
487 | action: 'macrosTrigger',
488 | macroID: action.options.pro7MacroUUID
489 | ? action.options.pro7MacroUUID
490 | : this.instance.currentState.internal.pro7Macros[0].id,
491 | }
492 | await sendCommand(cmd)
493 | },
494 | },
495 | [ActionId.stageDisplayMessage]: {
496 | name: 'Stage Display Message',
497 | options: [
498 | {
499 | type: 'textinput',
500 | label: 'Message',
501 | id: 'message',
502 | default: '',
503 | },
504 | ],
505 | callback: async (action) => {
506 | //var message = JSON.stringify(action.options.message);
507 | //cmd = '{"action":"stageDisplaySendMessage","stageDisplayMessage":'+message+'}';
508 | const cmd = {
509 | action: 'stageDisplaySendMessage',
510 | stageDisplayMessage: action.options.message,
511 | }
512 | await sendCommand(cmd)
513 | },
514 | },
515 | [ActionId.stageDisplayHideMessage]: {
516 | name: 'Stage Display Hide Message',
517 | options: [],
518 | callback: async () => {
519 | const cmd = {
520 | action: 'stageDisplayHideMessage',
521 | }
522 | await sendCommand(cmd)
523 | },
524 | },
525 | [ActionId.clockStart]: {
526 | name: 'Start Clock',
527 | options: [
528 | {
529 | type: 'textinput',
530 | label: 'Clock Number',
531 | id: 'clockIndex',
532 | default: '0',
533 | tooltip: 'Zero based index of countdown clock - first one is 0, second one is 1 and so on...',
534 | regex: Regex.NUMBER,
535 | },
536 | ],
537 | callback: async (action) => {
538 | var clockIndex = String(action.options.clockIndex)
539 | const cmd = {
540 | action: 'clockStart',
541 | clockIndex: clockIndex,
542 | }
543 | await sendCommand(cmd)
544 | },
545 | },
546 | [ActionId.clockStop]: {
547 | name: 'Stop Clock',
548 | options: [
549 | {
550 | type: 'textinput',
551 | label: 'Clock Number',
552 | id: 'clockIndex',
553 | default: '0',
554 | tooltip: 'Zero based index of countdown clock - first one is 0, second one is 1 and so on...',
555 | regex: Regex.NUMBER,
556 | },
557 | ],
558 | callback: async (action) => {
559 | var clockIndex = String(action.options.clockIndex)
560 | const cmd = {
561 | action: 'clockStop',
562 | clockIndex: clockIndex,
563 | }
564 | await sendCommand(cmd)
565 | },
566 | },
567 | [ActionId.clockReset]: {
568 | name: 'Reset Clock',
569 | options: [
570 | {
571 | type: 'textinput',
572 | label: 'Clock Number',
573 | id: 'clockIndex',
574 | default: '0',
575 | tooltip: 'Zero based index of countdown clock - first one is 0, second one is 1 and so on...',
576 | regex: Regex.NUMBER,
577 | },
578 | ],
579 | callback: async (action) => {
580 | var clockIndex = String(action.options.clockIndex)
581 | const cmd = {
582 | action: 'clockReset',
583 | clockIndex: clockIndex,
584 | }
585 | await sendCommand(cmd)
586 | },
587 | },
588 | [ActionId.clockUpdate]: {
589 | name: 'Update Clock',
590 | options: [
591 | {
592 | type: 'textinput',
593 | label: 'New Name For Clock', // Help person relise that this will rename clock that is updated.
594 | id: 'clockName',
595 | default: 'Timer',
596 | tooltip:
597 | 'If this does not match the existing clock name, the clock name will be updated/renamed. Enter the existing clock name to leave it unchanged.',
598 | },
599 | {
600 | type: 'textinput',
601 | label: 'Clock Number',
602 | id: 'clockIndex',
603 | default: '0',
604 | tooltip: 'Zero based index of countdown clock - first one is 0, second one is 1 and so on...',
605 | regex: Regex.NUMBER,
606 | },
607 | {
608 | type: 'textinput',
609 | label: 'Countdown Duration, Elapsed Start Time or Countdown To Time',
610 | id: 'clockTime',
611 | default: '00:05:00',
612 | tooltip:
613 | 'New duration (or time) for countdown clocks. Also used as optional starting time for elapsed time clocks. Formatted as HH:MM:SS - but you can also use other (shorthand) formats, see the README for more information',
614 | regex: '/^[-|+]?\\d*:?\\d*:?\\d*$/',
615 | },
616 | {
617 | type: 'dropdown',
618 | label: 'Over Run',
619 | id: 'clockOverRun',
620 | default: 'false',
621 | choices: [
622 | { id: 'false', label: 'False' },
623 | { id: 'true', label: 'True' },
624 | ],
625 | },
626 | {
627 | type: 'dropdown',
628 | label: 'Clock Type',
629 | id: 'clockType',
630 | default: '0',
631 | tooltip:
632 | 'If the clock specified by the Clock Number is not of this type it will be UPDATED/CONVERTED this type.',
633 | choices: [
634 | { id: '0', label: 'Countdown Timer' },
635 | { id: '1', label: 'Countdown To Time' },
636 | { id: '2', label: 'Elapsed Time' },
637 | ],
638 | },
639 | {
640 | type: 'dropdown',
641 | label: 'Clock Time Format',
642 | id: 'clockTimePeriodFormat',
643 | default: '0',
644 | tooltip: 'Only Required for Countdown To Time Clock - otherwise this is ignored.',
645 | choices: [
646 | { id: '0', label: 'AM' },
647 | { id: '1', label: 'PM' },
648 | { id: '2', label: '24Hr (Pro7 Only)' },
649 | ],
650 | },
651 | {
652 | type: 'textinput',
653 | label: 'Elapsed End Time',
654 | id: 'clockElapsedTime',
655 | default: '00:10:00',
656 | tooltip: 'Only Required for Elapsed Time Clock - otherwise this is ignored.',
657 | regex: '/^[-|+]?\\d*:?\\d*:?\\d*$/',
658 | },
659 | ],
660 | callback: async (action) => {
661 | var clockIndex = String(action.options.clockIndex)
662 |
663 | // Protect against option values which may be missing if this action is called from buttons that were previously saved before these options were added to the clockUpdate action!
664 | // If they are missing, then apply default values that result in the oringial bahaviour when it was only updating a countdown timers clockTime and clockOverRun.
665 | if (!action.options.hasOwnProperty('clockType')) {
666 | action.options.clockType = '0'
667 | }
668 | if (!action.options.hasOwnProperty('clockIsPM')) {
669 | action.options.clockIsPM = '0'
670 | }
671 | if (!action.options.hasOwnProperty('clockElapsedTime')) {
672 | action.options.clockElapsedTime = '00:10:00'
673 | }
674 | if (!action.options.hasOwnProperty('clockName')) {
675 | action.options.clockName = ''
676 | }
677 |
678 | // Allow +- prefix to update increment/decrement clockTime
679 | var newClockTime = action.options.clockTime
680 | if (newClockTime.charAt(0) == '-' || newClockTime.charAt(0) == '+') {
681 | var deltaSeconds = this.instance.convertToTotalSeconds(newClockTime)
682 | newClockTime =
683 | '00:00:' +
684 | String(
685 | parseInt(this.instance.currentState.dynamicVariables['pro7_clock_' + clockIndex + '_totalseconds']) +
686 | parseInt(deltaSeconds)
687 | )
688 | var newSeconds =
689 | parseInt(this.instance.currentState.dynamicVariables['pro7_clock_' + clockIndex + '_totalseconds']) +
690 | parseInt(deltaSeconds)
691 | if (newSeconds < 0) {
692 | newClockTime = '-00:00:' + String(newSeconds)
693 | } else {
694 | newClockTime = '00:00:' + String(newSeconds)
695 | }
696 | }
697 |
698 | // Allow +- prefix to update increment/decrement clockElapsedTime
699 | var newclockElapsedTime = action.options.clockElapsedTime
700 | if (newclockElapsedTime.charAt(0) == '-' || newclockElapsedTime.charAt(0) == '+') {
701 | var deltaSeconds = this.instance.convertToTotalSeconds(newclockElapsedTime)
702 | newclockElapsedTime =
703 | '00:00:' +
704 | String(
705 | parseInt(this.instance.currentState.dynamicVariables['pro7_clock_' + clockIndex + '_totalseconds']) +
706 | parseInt(deltaSeconds)
707 | )
708 | var newSeconds =
709 | parseInt(this.instance.currentState.dynamicVariables['pro7_clock_' + clockIndex + '_totalseconds']) +
710 | parseInt(deltaSeconds)
711 | if (newSeconds < 0) {
712 | newclockElapsedTime = '-00:00:' + String(newSeconds)
713 | } else {
714 | newclockElapsedTime = '00:00:' + String(newSeconds)
715 | }
716 | }
717 |
718 | const cmd = {
719 | action: 'clockUpdate',
720 | clockIndex: clockIndex,
721 | clockTime: newClockTime,
722 | clockOverrun: action.options.clockOverRun,
723 | clockType: action.options.clockType,
724 | clockIsPM:
725 | String(action.options.clockTimePeriodFormat) < 2 ? String(action.options.clockTimePeriodFormat) : '2', // Pro6 just wants a 1 (PM) or 0 (AM)
726 | clockTimePeriodFormat: String(action.options.clockTimePeriodFormat),
727 | clockElapsedTime:
728 | action.options.clockType === '1' && this.instance.currentState.internal.proMajorVersion === 7
729 | ? newClockTime
730 | : newclockElapsedTime, // When doing countdown to time (clockType==='1'), Pro7 uses clockElapsed value for the "countdown-to-time", so we grab this from clocktime above where the user has entered it (Pro6 uses clocktime for countdown-to-time value)
731 | clockName: action.options.clockName,
732 | }
733 | await sendCommand(cmd)
734 | },
735 | },
736 | [ActionId.messageSend]: {
737 | name: 'Show Message',
738 | options: [
739 | {
740 | type: 'textinput',
741 | useVariables: true,
742 | label: 'Message Index',
743 | id: 'messageIndex',
744 | default: '0',
745 | tooltip:
746 | 'Zero based index of message to show - first one is 0, second one is 1 and so on...(Supports variable)',
747 | regex: Regex.NUMBER,
748 | },
749 | {
750 | type: 'textinput',
751 | label: 'Comma Separated List Of Message Token Names',
752 | id: 'messageKeys',
753 | default: '',
754 | tooltip:
755 | 'Comma separated, list of message token names used in the message. Associated values are given below. Use double commas (,,) to insert an actual comma in a token name. (WARNING! - A simple typo here could crash and burn ProPresenter)',
756 | },
757 | {
758 | type: 'textinput',
759 | useVariables: true,
760 | label: 'Comma Separated List Of Message Token Values',
761 | id: 'messageValues',
762 | default: '',
763 | tooltip:
764 | 'Comma separated, list of values for each message token above. Use double commas (,,) to insert an actual comma in a token value. You can optionally use a single variable. (Supports variable. WARNING! - A simple typo here could crash and burn ProPresenter)',
765 | },
766 | ],
767 | callback: async (action, context) => {
768 | // Picking a var from the dropdown seems to add a space on end (use trim() to ensure field is a just a clean variable)
769 | const messageIndex = await context.parseVariablesInString(String(action.options.messageIndex).trim())
770 |
771 | // The below "replace...split dance" for messageKeys and MessageValues produces the required array of items from the comma-separated list of values entered by the user. It also allows double commas (,,) to be treated as an escape method for the user to include a literal comma in the values if desired.
772 | // It works by first replacing any double commas with a character 29 (ascii group seperator char), and then replacing any single commas with a character 28 (ascii file seperator char). Then it can safely replace character 29 with a comma and finally split using character 28 as the separator.
773 | // Note that character 28 and 29 are not "normally typed characters" and therefore considered (somewhat) safe to insert into the string as special markers during processing. Also note that CharCode(29) is matched by regex /\u001D/
774 | const cmd = {
775 | action: 'messageSend',
776 | messageIndex:
777 | messageIndex !== 'undefined' && messageIndex !== undefined && parseInt(messageIndex) >= 0
778 | ? String(messageIndex)
779 | : '0',
780 | messageKeys: action.options.messageKeys
781 | .replace(/,,/g, String.fromCharCode(29))
782 | .replace(/,/g, String.fromCharCode(28))
783 | .replace(/\u001D/g, ',')
784 | .split(String.fromCharCode(28)),
785 | messageValues: action.options.messageValues
786 | .replace(/,,/g, String.fromCharCode(29))
787 | .replace(/,/g, String.fromCharCode(28))
788 | .replace(/\u001D/g, ',')
789 | .split(String.fromCharCode(28)),
790 | }
791 | // If there is only one message value - then allow parsing of optional variables...
792 | if (cmd.messageValues.length == 1) {
793 | // Allow parsing of optional variable in the Message values textfield
794 | // Picking a var from the dropdown seems to add a space on end (use trim() to ensure field is a just a clean variable)
795 | cmd.messageValues[0] = await context.parseVariablesInString(String(cmd.messageValues[0]).trim())
796 | }
797 | await sendCommand(cmd)
798 | },
799 | },
800 | [ActionId.messageHide]: {
801 | name: 'Hide Message',
802 | options: [
803 | {
804 | type: 'textinput',
805 | useVariables: true,
806 | label: 'Message Index',
807 | id: 'messageIndex',
808 | default: '0',
809 | tooltip:
810 | 'Zero based index of message to hide - first one is 0, second one is 1 and so on...(Supports variable)',
811 | regex: Regex.NUMBER,
812 | },
813 | ],
814 | callback: async (action, context) => {
815 | // Picking a var from the dropdown seems to add a space on end (use trim() to ensure field is a just a clean variable)
816 | const messageIndex = await context.parseVariablesInString(String(action.options.messageIndex).trim())
817 |
818 | const cmd = {
819 | action: 'messageHide',
820 | messageIndex:
821 | messageIndex !== 'undefined' && messageIndex !== undefined && parseInt(messageIndex) >= 0
822 | ? String(messageIndex)
823 | : '0',
824 | }
825 | await sendCommand(cmd)
826 | },
827 | },
828 | [ActionId.audioStartCue]: {
829 | name: 'Audio Start Cue',
830 | options: [
831 | {
832 | type: 'textinput',
833 | label: 'Audio Item Playlist Path',
834 | id: 'audioChildPath',
835 | default: '',
836 | tooltip: 'PresentationPath format - See the README for more information',
837 | regex: '/^$|^\\d+$|^\\d+(\\.\\d+)*:\\d+$/',
838 | },
839 | ],
840 | callback: async (action) => {
841 | const cmd = {
842 | action: 'audioStartCue',
843 | audioChildPath: action.options.audioChildPath,
844 | }
845 | await sendCommand(cmd)
846 | },
847 | },
848 | [ActionId.audioPlayPause]: {
849 | name: 'Audio Play/Pause',
850 | options: [],
851 | callback: async () => {
852 | const cmd = {
853 | action: 'audioPlayPause',
854 | }
855 | await sendCommand(cmd)
856 | },
857 | },
858 | [ActionId.timelinePlayPause]: {
859 | name: 'Timeline Play/Pause',
860 | options: [
861 | {
862 | type: 'textinput',
863 | label: 'Presentation Path',
864 | id: 'presentationPath',
865 | default: '',
866 | tooltip: 'PresentationPath format - See the README for more information',
867 | regex: '/^$|^\\d+$|^\\d+(\\.\\d+)*:\\d+$/',
868 | },
869 | ],
870 | callback: async (action) => {
871 | const cmd = {
872 | action: 'timelinePlayPause',
873 | presentationPath: action.options.presentationPath,
874 | }
875 | await sendCommand(cmd)
876 | },
877 | },
878 | [ActionId.timelineRewind]: {
879 | name: 'Timeline Rewind',
880 | options: [
881 | {
882 | type: 'textinput',
883 | label: 'Presentation Path',
884 | id: 'presentationPath',
885 | default: '',
886 | tooltip: 'PresentationPath format - See the README for more information',
887 | regex: '/^$|^\\d+$|^\\d+(\\.\\d+)*:\\d+$/',
888 | },
889 | ],
890 | callback: async (action) => {
891 | const cmd = {
892 | action: 'timelineRewind',
893 | presentationPath: action.options.presentationPath,
894 | }
895 | await sendCommand(cmd)
896 | },
897 | },
898 | [ActionId.enableFollowerControl]: {
899 | name: 'Enable Follower Control',
900 | options: [
901 | {
902 | type: 'dropdown',
903 | label: 'Enable Follower Control',
904 | id: 'enableFollowerControl',
905 | default: 'false',
906 | choices: [
907 | { id: 'no', label: 'No' },
908 | { id: 'yes', label: 'Yes' },
909 | ],
910 | },
911 | ],
912 | callback: async (action) => {
913 | this.config.control_follower = action.options.enableFollowerControl
914 | this.checkFeedbacks('propresenter_follower_connected')
915 | },
916 | },
917 | [ActionId.nwSpecificSlide]: {
918 | name: 'Specific Slide (Network Link)',
919 | options: [
920 | {
921 | type: 'textinput',
922 | label: 'Playlist Name',
923 | id: 'playlistName',
924 | tooltip:
925 | 'Name of the PlayList that contains the presentation with the slide you want to trigger (Case Sensitive)',
926 | },
927 | {
928 | type: 'textinput',
929 | label: 'Presentation Name',
930 | id: 'presentationName',
931 | tooltip: 'Name of the presentation with the slide you want to trigger (Case Sensitive)',
932 | },
933 | {
934 | type: 'textinput',
935 | useVariables: true,
936 | label: 'Slide Index',
937 | id: 'slideIndex',
938 | tooltip: 'Index of the slide you want to trigger (1-based. Supports variable)',
939 | regex: Regex.NUMBER,
940 | },
941 | /* Does not seem to do anything (yet)
942 | {
943 | type: 'textinput',
944 | label: 'Slide Name',
945 | id: 'slideName',
946 | tooltip: 'Name of the slide you want to trigger',
947 | }, */
948 | ],
949 | callback: async (action, context) => {
950 | // Picking a var from the dropdown seems to add a space on end (use trim() to ensure field is a just a clean variable)
951 | const slideIndex = await context.parseVariablesInString(String(action.options.slideIndex).trim())
952 |
953 | const nwCmd = {
954 | endpointPath: '/trigger/playlist',
955 | data: {
956 | path: [
957 | {
958 | name: action.options.playlistName,
959 | },
960 | {
961 | name: action.options.presentationName,
962 | },
963 | {
964 | index:
965 | slideIndex !== 'undefined' && slideIndex !== undefined && parseInt(slideIndex) > 0
966 | ? Number(slideIndex) - 1
967 | : null,
968 | },
969 | //name: action.options.slideName !== undefined && String(action.options.slideName).length > 0 ? action.options.slideName : null // Slide name does nothing - maybe one day it will.
970 | ],
971 | },
972 | }
973 | await sendNwCommand(nwCmd)
974 | },
975 | },
976 | [ActionId.nwPropTrigger]: {
977 | name: 'Prop Trigger (Network Link)',
978 | options: [
979 | {
980 | type: 'textinput',
981 | useVariables: true,
982 | label: 'Prop Index',
983 | id: 'propIndex',
984 | tooltip: 'Index of the Prop you want to trigger (1-based. Supports variable)',
985 | regex: Regex.NUMBER,
986 | },
987 | {
988 | type: 'textinput',
989 | label: 'Prop Name',
990 | id: 'propName',
991 | tooltip: 'Name of the Prop you want to trigger (Case Sensitive)',
992 | },
993 | ],
994 | callback: async (action, context) => {
995 | // Picking a var from the dropdown seems to add a space on end (use trim() to ensure field is a just a clean variable)
996 | const propIndex = await context.parseVariablesInString(String(action.options.propIndex).trim())
997 |
998 | const nwCmd = {
999 | endpointPath: '/prop/trigger',
1000 | data: {
1001 | id: {
1002 | index:
1003 | propIndex !== 'undefined' && propIndex !== undefined && parseInt(propIndex) > 0
1004 | ? Number(propIndex) - 1
1005 | : null,
1006 | name:
1007 | action.options.propName !== undefined && String(action.options.propName).length > 0
1008 | ? action.options.propName
1009 | : null,
1010 | },
1011 | },
1012 | }
1013 | await sendNwCommand(nwCmd)
1014 | },
1015 | },
1016 | [ActionId.nwPropClear]: {
1017 | name: 'Prop Clear (Network Link)',
1018 | options: [
1019 | {
1020 | type: 'textinput',
1021 | useVariables: true,
1022 | label: 'Prop Index',
1023 | id: 'propIndex',
1024 | tooltip: 'Index of the Prop you want to clear (1-based. Supports variable)',
1025 | regex: Regex.NUMBER,
1026 | },
1027 | {
1028 | type: 'textinput',
1029 | label: 'Prop Name',
1030 | id: 'propName',
1031 | tooltip: 'Name of the Prop you want to clear (Case Sensitive)',
1032 | },
1033 | ],
1034 | callback: async (action, context) => {
1035 | // Picking a var from the dropdown seems to add a space on end (use trim() to ensure field is a just a clean variable)
1036 | const propIndex = await context.parseVariablesInString(String(action.options.propIndex).trim())
1037 |
1038 | const nwCmd = {
1039 | endpointPath: '/prop/clear',
1040 | data: {
1041 | id: {
1042 | index:
1043 | propIndex !== 'undefined' && propIndex !== undefined && parseInt(propIndex) > 0
1044 | ? Number(propIndex) - 1
1045 | : null,
1046 | name:
1047 | action.options.propName !== undefined && String(action.options.propName).length > 0
1048 | ? action.options.propName
1049 | : null,
1050 | },
1051 | },
1052 | }
1053 | await sendNwCommand(nwCmd)
1054 | },
1055 | },
1056 | [ActionId.nwMessageClear]: {
1057 | name: 'Message Clear (Network Link)',
1058 | options: [
1059 | {
1060 | type: 'textinput',
1061 | useVariables: true,
1062 | label: 'Message Index',
1063 | id: 'messageIndex',
1064 | tooltip: 'Index of the Message you want to clear (1-based. Supports variable)',
1065 | regex: Regex.NUMBER,
1066 | },
1067 | {
1068 | type: 'textinput',
1069 | label: 'Message Name',
1070 | id: 'messageName',
1071 | tooltip: 'Name of the Message you want to clear (Case Sensitive)',
1072 | },
1073 | ],
1074 | callback: async (action, context) => {
1075 | // Picking a var from the dropdown seems to add a space on end (use trim() to ensure field is a just a clean variable)
1076 | const messageIndex = await context.parseVariablesInString(String(action.options.messageIndex).trim())
1077 |
1078 | const nwCmd = {
1079 | endpointPath: '/message/clear',
1080 | data: {
1081 | id: {
1082 | index:
1083 | messageIndex !== 'undefined' && messageIndex !== undefined && parseInt(messageIndex) > 0
1084 | ? Number(messageIndex) - 1
1085 | : null,
1086 | name:
1087 | action.options.messageName !== undefined && String(action.options.messageName).length > 0
1088 | ? action.options.messageName
1089 | : null,
1090 | },
1091 | },
1092 | }
1093 | await sendNwCommand(nwCmd)
1094 | },
1095 | },
1096 | [ActionId.nwTriggerMedia]: {
1097 | name: 'Trigger Media (Network Link)',
1098 | options: [
1099 | {
1100 | type: 'textinput',
1101 | label: 'Media Playlist Name',
1102 | id: 'playlistName',
1103 | tooltip: 'Name of the Media PlayList that contains the media file you want to trigger (Case Sensitive)',
1104 | },
1105 | {
1106 | type: 'textinput',
1107 | useVariables: true,
1108 | label: 'Media Index',
1109 | id: 'mediaIndex',
1110 | tooltip: 'Index of the media file you want to trigger (1-based. Supports variable)',
1111 | regex: Regex.NUMBER,
1112 | },
1113 | {
1114 | type: 'textinput',
1115 | label: 'Media Name',
1116 | id: 'mediaName',
1117 | tooltip: 'Name of the media file you want to trigger (Case Sensitive)',
1118 | },
1119 | ],
1120 | callback: async (action, context) => {
1121 | // Picking a var from the dropdown seems to add a space on end (use trim() to ensure field is a just a clean variable)
1122 | const mediaIndex = await context.parseVariablesInString(String(action.options.mediaIndex).trim())
1123 |
1124 | const nwCmd = {
1125 | endpointPath: '/trigger/media',
1126 | data: {
1127 | path: [
1128 | {
1129 | name: action.options.playlistName,
1130 | },
1131 | {
1132 | index:
1133 | mediaIndex !== 'undefined' && mediaIndex !== undefined && parseInt(mediaIndex) > 0
1134 | ? Number(mediaIndex) - 1
1135 | : null,
1136 | name:
1137 | action.options.mediaName !== undefined && String(action.options.mediaName).length > 0
1138 | ? action.options.mediaName
1139 | : null,
1140 | },
1141 | ],
1142 | },
1143 | }
1144 | await sendNwCommand(nwCmd)
1145 | },
1146 | },
1147 | [ActionId.nwTriggerAudio]: {
1148 | name: 'Trigger Audio (Network Link)',
1149 | options: [
1150 | {
1151 | type: 'textinput',
1152 | label: 'Audio Playlist Name',
1153 | id: 'playlistName',
1154 | tooltip: 'Name of the Audio PlayList that contains the audio file you want to trigger (Case Sensitive)',
1155 | },
1156 | {
1157 | type: 'textinput',
1158 | useVariables: true,
1159 | label: 'Audio Index',
1160 | id: 'audioIndex',
1161 | tooltip: 'Index of the audio file you want to trigger (1-based. Supports variable)',
1162 | regex: Regex.NUMBER,
1163 | },
1164 | {
1165 | type: 'textinput',
1166 | label: 'Audio Name',
1167 | id: 'audioName',
1168 | tooltip: 'Name of the audio file you want to trigger (Case Sensitive)',
1169 | },
1170 | ],
1171 | callback: async (action, context) => {
1172 | // Picking a var from the dropdown seems to add a space on end (use trim() to ensure field is a just a clean variable)
1173 | const audioIndex = await context.parseVariablesInString(String(action.options.audioIndex).trim())
1174 |
1175 | const nwCmd = {
1176 | endpointPath: '/trigger/audio',
1177 | data: {
1178 | path: [
1179 | {
1180 | name: action.options.playlistName,
1181 | },
1182 | {
1183 | index:
1184 | audioIndex !== 'undefined' && audioIndex !== undefined && parseInt(audioIndex) > 0
1185 | ? Number(audioIndex) - 1
1186 | : null,
1187 | name:
1188 | action.options.audioName !== undefined && String(action.options.audioName).length > 0
1189 | ? action.options.audioName
1190 | : null,
1191 | },
1192 | ],
1193 | },
1194 | }
1195 | await sendNwCommand(nwCmd)
1196 | },
1197 | },
1198 | [ActionId.nwVideoInput]: {
1199 | name: 'Trigger Video Input (Network Link)',
1200 | options: [
1201 | {
1202 | type: 'textinput',
1203 | useVariables: true,
1204 | label: 'Video Index',
1205 | id: 'videoInputIndex',
1206 | tooltip: 'Index of the video input you want to trigger (1-based. Supports variable)',
1207 | regex: Regex.NUMBER,
1208 | },
1209 | {
1210 | type: 'textinput',
1211 | label: 'Video Input Name',
1212 | id: 'videoInputName',
1213 | tooltip: 'Name of the video input you want to trigger (Case Sensitive)',
1214 | },
1215 | ],
1216 | callback: async (action, context) => {
1217 | // Picking a var from the dropdown seems to add a space on end (use trim() to ensure field is a just a clean variable)
1218 | const videoInputIndex = await context.parseVariablesInString(String(action.options.videoInputIndex).trim())
1219 |
1220 | const nwCmd = {
1221 | endpointPath: '/trigger/video_input',
1222 | data: {
1223 | id: {
1224 | index:
1225 | videoInputIndex !== 'undefined' && videoInputIndex !== undefined && parseInt(videoInputIndex) > 0
1226 | ? Number(videoInputIndex) - 1
1227 | : null,
1228 | name:
1229 | action.options.videoInputName !== undefined && String(action.options.videoInputName).length > 0
1230 | ? action.options.videoInputName
1231 | : null,
1232 | },
1233 | },
1234 | }
1235 | await sendNwCommand(nwCmd)
1236 | },
1237 | },
1238 | [ActionId.newRandomNumber]: {
1239 | name: 'New Random Number',
1240 | options: [
1241 | {
1242 | type: 'textinput',
1243 | label: 'New Random Number Between 1 And:',
1244 | id: 'randomLimit',
1245 | default: '10',
1246 | tooltip:
1247 | 'Updates the module variable current_random_number with a new random number up to the limit your enter. (Supports variable)',
1248 | regex: Regex.NUMBER,
1249 | },
1250 | ],
1251 | callback: async (action, context) => {
1252 | // Picking a var from the dropdown seems to add a space on end (use trim() to ensure field is a just a clean variable)
1253 | const randomLimit = await context.parseVariablesInString(String(action.options.randomLimit).trim())
1254 |
1255 | this.updateVariable('current_random_number', Math.floor(Math.random() * parseInt(randomLimit)) + 1)
1256 | },
1257 | },
1258 | [ActionId.nwCustom]: {
1259 | name: 'Custom Action (Network Link - Support Use Only)',
1260 | options: [
1261 | {
1262 | type: 'textinput',
1263 | label: 'Endpoint Path',
1264 | id: 'endpointPath',
1265 | tooltip: 'REST Endpoint path (must start with /)',
1266 | },
1267 | {
1268 | type: 'textinput',
1269 | label: 'JSON Data',
1270 | id: 'jsonData',
1271 | tooltip: 'JSON Data (no single quotes, no trailing commas)',
1272 | },
1273 | ],
1274 | callback: async (action) => {
1275 | const nwCmd = {
1276 | endpointPath: action.options.endpointPath,
1277 | data: JSON.parse(String(action.options.jsonData)),
1278 | }
1279 | await sendNwCommand(nwCmd)
1280 | },
1281 | },
1282 | [ActionId.customAction]: {
1283 | name: 'Custom Action (Support Use Only)',
1284 | options: [
1285 | {
1286 | type: 'textinput',
1287 | label: 'Custom Action',
1288 | id: 'customAction',
1289 | default: '{"action":"customAction","customProperty":"customValue"}',
1290 | tooltip:
1291 | 'Advanced use only. Must be a valid JSON action message that ProPresenter understands. An invalid message or even one little mistake can lead to crashes and data loss.',
1292 | },
1293 | ],
1294 | callback: async (action) => {
1295 | let cmd
1296 | try {
1297 | cmd = JSON.parse(String(action.options.customAction))
1298 | } catch (err) {
1299 | this.instance.log(
1300 | 'debug',
1301 | 'Failed to convert custom action: ' + action.options.customAction + ' to valid JS object: ' + err.message
1302 | )
1303 | return
1304 | }
1305 | await sendCommand(cmd)
1306 | },
1307 | },
1308 | }
1309 | return actions
1310 | },
1311 | ActionId: ActionId,
1312 | }
1313 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | const { WebSocket } = require('ws')
4 | const { InstanceBase, runEntrypoint, combineRgb, InstanceStatus, Regex } = require('@companion-module/base')
5 | const { GetActions } = require('./actions')
6 |
7 | class ProPresenterInstance extends InstanceBase {
8 | constructor(internal) {
9 | super(internal)
10 | }
11 |
12 | /**
13 | * Module is starting up.
14 | */
15 | async init(config) {
16 | this.initVariables()
17 |
18 | await this.configUpdated(config)
19 |
20 | if (this.config.host !== '' && this.config.port !== '') {
21 | this.connectToProPresenter()
22 | this.startConnectionTimer()
23 |
24 | // Enabled Looks polling timer (which will only send looksRequests if option is enabled)
25 | this.startWatchDogTimer()
26 |
27 | if (this.config.use_sd === 'yes') {
28 | this.startSDConnectionTimer()
29 | this.connectToProPresenterSD()
30 | }
31 | if (this.config.control_follower === 'yes') {
32 | this.startFollowerConnectionTimer()
33 | this.connectToFollowerProPresenter()
34 | }
35 | }
36 | this.awaiting_reply = false
37 | this.command_queue = []
38 |
39 | this.setActionDefinitions(GetActions(this))
40 | }
41 |
42 | /**
43 | * When the module gets deleted.
44 | */
45 | async destroy() {
46 | this.disconnectFromProPresenter()
47 | this.disconnectFromProPresenterSD()
48 | this.stopConnectionTimer()
49 | this.stopSDConnectionTimer()
50 | this.stopWatchDogTimer()
51 |
52 | this.log('debug', 'destroy: ' + this.id)
53 | }
54 |
55 | /**
56 | * The current state of ProPresenter.
57 | * Initially populated by emptyCurrentState().
58 | *
59 | * .internal contains the internal state of the module
60 | * .dynamicVariable contains the values of the dynamic variables
61 | * .dynamicVariablesDefs contains the definitions of the dynamic variables - this list is passed to this.setVariableDefinitions() so WebUI etc can know what the module vars are.
62 | */
63 | currentState = {
64 | internal: {},
65 | dynamicVariables: {},
66 | dynamicVariablesDefs: [],
67 | }
68 |
69 | /**
70 | * Return config fields for web config
71 | */
72 | getConfigFields() {
73 | return [
74 | // ********** Required Settings ************
75 | {
76 | type: 'static-text',
77 | id: 'info',
78 | width: 12,
79 | label: '',
80 | value: '
', // Dummy space to separate settings into obvious sections
81 | },
82 | {
83 | type: 'static-text',
84 | id: 'info',
85 | width: 12,
86 | label: 'Required Settings',
87 | value:
88 | "These settings are required by this module to communicate with Renewed Vision's ProPresenter 6 or 7.
Make sure to enable Network and ProPresenter Remote Controller Password in ProPresenter Preferences",
89 | },
90 | {
91 | type: 'textinput',
92 | id: 'host',
93 | label: 'ProPresenter IP (or hostname)',
94 | width: 6,
95 | default: '',
96 | },
97 | {
98 | type: 'textinput',
99 | id: 'port',
100 | label: 'ProPresenter Port',
101 | width: 6,
102 | default: '20652',
103 | regex: Regex.PORT,
104 | },
105 | {
106 | type: 'textinput',
107 | id: 'pass',
108 | label: 'ProPresenter Remote Controller Password',
109 | width: 6,
110 | },
111 | {
112 | type: 'static-text',
113 | id: 'info',
114 | width: 12,
115 | label: '',
116 | value: '
', // Dummy space to separate settings into obvious sections
117 | },
118 | // ********** Stage Display Settings ************
119 | {
120 | type: 'static-text',
121 | id: 'info',
122 | width: 12,
123 | label: 'Stage Display Settings (Optional)',
124 | value:
125 | 'The following fields are only needed if you want to track the video countdown timer in a module variable.',
126 | },
127 | {
128 | type: 'dropdown',
129 | label: 'Connect to Stage Display?',
130 | id: 'use_sd',
131 | default: 'no',
132 | width: 6,
133 | choices: [
134 | { id: 'no', label: 'No' },
135 | { id: 'yes', label: 'Yes' },
136 | ],
137 | },
138 | {
139 | type: 'textinput',
140 | id: 'sdport',
141 | label: 'Stage Display App Port',
142 | tooltip: 'Optionally set in ProPresenter Preferences. ProPresenter Port (above) will be used if left blank.',
143 | width: 6,
144 | default: '',
145 | // regex from instance_skel.js, but modified to make the port optional
146 | regex:
147 | '/^([1-9]|[1-8][0-9]|9[0-9]|[1-8][0-9]{2}|9[0-8][0-9]|99[0-9]|[1-8][0-9]{3}|9[0-8][0-9]{2}|99[0-8][0-9]|999[0-9]|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-4])$|^$/',
148 | },
149 | {
150 | type: 'textinput',
151 | id: 'sdpass',
152 | label: 'Stage Display App Password',
153 | width: 6,
154 | },
155 | {
156 | type: 'static-text',
157 | id: 'info',
158 | width: 12,
159 | label: '',
160 | value: '
', // Dummy space to separate settings into obvious sections
161 | },
162 | // ********** Backwards Compatibility Settings ************
163 | {
164 | type: 'static-text',
165 | id: 'info',
166 | width: 12,
167 | label: 'Backwards Compatibility Settings (Optional)',
168 | value:
169 | 'These settings are optional. They provide backwards compatibility for older features that are not longer required for new users/setups and newer features have been added that supersede them',
170 | },
171 | {
172 | type: 'textinput',
173 | id: 'indexOfClockToWatch',
174 | label: 'Index of Clock to Watch',
175 | tooltip:
176 | 'Index of clock to watch. Dynamic variable "watched_clock_current_time" will be updated with current value once every second.',
177 | default: '0',
178 | width: 4,
179 | regex: Regex.NUMBER,
180 | },
181 | {
182 | type: 'dropdown',
183 | id: 'GUIDOfStageDisplayScreenToWatch',
184 | label: 'Pro7 Stage Display Screen To Monitor Layout',
185 | tooltip:
186 | 'Pro7 Stage Display Screen To Monitor Layout - (This list is refreshed the next time you EDIT config, after a succesful connection)',
187 | default: '',
188 | width: 6,
189 | choices: this.currentState.internal.pro7StageScreens,
190 | },
191 | {
192 | type: 'static-text',
193 | id: 'info',
194 | width: 12,
195 | label: '',
196 | value: '
', // Dummy space to separate settings into obvious sections
197 | },
198 | // ********** Workaround Settings ************
199 | {
200 | type: 'static-text',
201 | id: 'info',
202 | width: 12,
203 | label: 'Workaround Settings (Optional)',
204 | value: 'These settings are optional. They provide "Workarounds" that might be needed for some setups.',
205 | },
206 | {
207 | type: 'dropdown',
208 | id: 'sendPresentationCurrentMsgs',
209 | label: 'Send Presentation Info Requests To ProPresenter',
210 | tooltip:
211 | 'You may want to turn this off for Pro7 as it can cause performance issues - Turning it off will mean the module does not update the dynamic variables: remaining_slides, total_slides or presentation_name',
212 | default: 'yes',
213 | width: 6,
214 | choices: [
215 | { id: 'no', label: 'No' },
216 | { id: 'yes', label: 'Yes' },
217 | ],
218 | },
219 | {
220 | type: 'dropdown',
221 | id: 'typeOfPresentationRequest',
222 | label: 'Type of Presentation Info Requests',
223 | default: 'auto',
224 | tooltip: 'Manual may workaround performance issues for some users - give it a try',
225 | width: 6,
226 | choices: [
227 | { id: 'auto', label: 'Automatic' },
228 | { id: 'manual', label: 'Manual' },
229 | ],
230 | },
231 | {
232 | type: 'textinput',
233 | id: 'clientVersion',
234 | label: 'ProRemote Client Version',
235 | tooltip:
236 | 'No need to update this - unless trying to work around an issue with connectivity for future Pro7 releases.',
237 | width: 6,
238 | default: '701',
239 | },
240 | {
241 | type: 'dropdown',
242 | id: 'looksPolling', // Pro 7.8 on MacOs already sends notifications for look changes - but Pro 7.8.2 does not - so added this optional poll to enabled look feedback
243 | label: 'Looks Polling',
244 | default: 'disabled',
245 | tooltip: 'Poll ProPresenter Looks info once per second to enable Feedback for Active Look',
246 | width: 6,
247 | choices: [
248 | { id: 'disabled', label: 'Disabled' },
249 | { id: 'enabled', label: 'Enabled' },
250 | ],
251 | },
252 | {
253 | type: 'dropdown',
254 | id: 'timerPolling', // Pro 7.92 onwards on MacOs no longer sends timer updates when clockStartSendingCurrentTime action is sent - instead, we must manually poll timers
255 | label: 'Timer Polling',
256 | default: 'disabled',
257 | tooltip:
258 | 'Poll ProPresenter Timers values once per second to enable timer feedback. This workaround is only needed for some versions of Pro7 on MacOS - eg 7.9.2, 7.10, 7.10.2...',
259 | width: 6,
260 | choices: [
261 | { id: 'disabled', label: 'Disabled' },
262 | { id: 'enabled', label: 'Enabled' },
263 | ],
264 | },
265 | {
266 | type: 'static-text',
267 | id: 'info',
268 | width: 12,
269 | label: '',
270 | value: '
', // Dummy space to separate settings into obvious sections
271 | },
272 | // ********** Pro7 Follower Settings ************
273 | {
274 | type: 'static-text',
275 | id: 'info2',
276 | width: 12,
277 | label: 'Pro7 Follower Settings (Optional)',
278 | value: 'Optional *Beta* feature to mimic Pro6 Master-Control module. (No longer needed for Pro7.8+ users)',
279 | },
280 | {
281 | type: 'dropdown',
282 | label: 'Auto-Control Follower ProPresenter?',
283 | id: 'control_follower',
284 | default: 'no',
285 | width: 6,
286 | choices: [
287 | { id: 'no', label: 'No' },
288 | { id: 'yes', label: 'Yes' },
289 | ],
290 | },
291 | {
292 | type: 'textinput',
293 | id: 'followerhost',
294 | label: 'Follower-ProPresenter IP',
295 | width: 6,
296 | default: '',
297 | regex: Regex.IP,
298 | },
299 | {
300 | type: 'textinput',
301 | id: 'followerport',
302 | label: 'Follower-ProPresenter Port',
303 | width: 6,
304 | default: '20652',
305 | regex: Regex.PORT,
306 | },
307 | {
308 | type: 'textinput',
309 | id: 'followerpass',
310 | label: 'Follower-ProPresenter Remote Password',
311 | width: 6,
312 | },
313 | ]
314 | }
315 |
316 | /**
317 | * The user changed the config options for this modules.
318 | */
319 | async configUpdated(config) {
320 | this.config = config
321 | this.init_presets()
322 | this.disconnectFromProPresenter()
323 | this.disconnectFromProPresenterSD()
324 | this.connectToProPresenter()
325 | this.startConnectionTimer()
326 | if (this.config.use_sd === 'yes') {
327 | this.connectToProPresenterSD()
328 | this.startSDConnectionTimer()
329 | } else {
330 | this.stopSDConnectionTimer()
331 | }
332 |
333 | if (this.config.control_follower === 'yes') {
334 | this.connectToFollowerProPresenter()
335 | this.startFollowerConnectionTimer()
336 | } else {
337 | this.stopFollowerConnectionTimer()
338 | }
339 | }
340 |
341 | /**
342 | * Define button presets
343 | */
344 | init_presets() {
345 | const presets = {}
346 |
347 | presets['displayname'] = {
348 | type: 'button',
349 | category: 'Stage Display',
350 | name: 'This button displays the name of current stage display layout. Pressing it will toggle back and forth between the two selected stage display layouts in the down and up actions.',
351 | style: {
352 | text: '$(propresenter:current_stage_display_name)',
353 | size: '18',
354 | color: combineRgb(255, 255, 255),
355 | bgcolor: combineRgb(153, 0, 255),
356 | },
357 | steps: [
358 | {
359 | down: [
360 | {
361 | actionId: 'stageDisplayLayout',
362 | options: {
363 | index: 0,
364 | },
365 | },
366 | ],
367 | up: [],
368 | },
369 | {
370 | down: [
371 | {
372 | actionId: 'stageDisplayLayout',
373 | options: {
374 | index: 1,
375 | },
376 | },
377 | ],
378 | up: [],
379 | },
380 | ],
381 | feedbacks: [],
382 | }
383 | presets['select_layout'] = {
384 | type: 'button',
385 |
386 | category: 'Stage Display',
387 | name: 'This button will activate the selected (by index) stage display layout.',
388 | style: {
389 | text: 'Select Layout',
390 | size: '18',
391 | color: combineRgb(255, 255, 255),
392 | bgcolor: combineRgb(153, 0, 255),
393 | },
394 | steps: [
395 | {
396 | down: [
397 | {
398 | actionId: 'stageDisplayLayout',
399 | options: {
400 | index: 0,
401 | },
402 | },
403 | ],
404 | up: [],
405 | },
406 | ],
407 | feedbacks: [],
408 | }
409 | presets['clock_5min'] = {
410 | type: 'button',
411 |
412 | category: 'Countdown Clocks',
413 | name: 'This button will reset a selected (by index) clock to a 5 min countdown clock and automatically start it.',
414 | style: {
415 | text: 'Clock ' + this.config.indexOfClockToWatch + '\\n5 mins',
416 | size: '18',
417 | color: combineRgb(255, 255, 255),
418 | bgcolor: combineRgb(0, 153, 51),
419 | },
420 | steps: [
421 | {
422 | down: [
423 | {
424 | actionId: 'clockUpdate',
425 | options: {
426 | clockIndex: this.config.indexOfClockToWatch, // N.B. If user updates indexOfClockToWatch, this preset default will not be updated until module is reloaded.
427 | clockTime: '00:05:00',
428 | clockOverRun: 'false',
429 | clockType: 0,
430 | },
431 | },
432 | {
433 | actionId: 'clockReset',
434 | delay: 100,
435 | options: {
436 | clockIndex: this.config.indexOfClockToWatch, // N.B. If user updates indexOfClockToWatch, this preset default will not be updated until module is reloaded.
437 | },
438 | },
439 | {
440 | actionId: 'clockStart',
441 | delay: 200,
442 | options: {
443 | clockIndex: this.config.indexOfClockToWatch, // N.B. If user updates indexOfClockToWatch, this preset default will not be updated until module is reloaded.
444 | },
445 | },
446 | ],
447 | },
448 | ],
449 | feedbacks: [],
450 | }
451 | presets['clock_start'] = {
452 | type: 'button',
453 |
454 | category: 'Countdown Clocks',
455 | name: 'This button will START a clock selected by index (0-based). If you change the index, and still want to display the current time on the button, make sure to also update the index of the clock to watch in this modules config to match.',
456 | style: {
457 | text: 'Start\\nClock ' + this.config.indexOfClockToWatch + '\\n$(propresenter:watched_clock_current_time)',
458 | size: '14',
459 | color: combineRgb(255, 255, 255),
460 | bgcolor: combineRgb(0, 153, 51),
461 | },
462 | steps: [
463 | {
464 | down: [
465 | {
466 | actionId: 'clockStart',
467 | options: {
468 | clockIndex: this.config.indexOfClockToWatch, // N.B. If user updates indexOfClockToWatch, this preset default will not be updated until module is reloaded.
469 | },
470 | },
471 | ],
472 | },
473 | ],
474 | feedbacks: [],
475 | }
476 | presets['clock_stop'] = {
477 | type: 'button',
478 |
479 | category: 'Countdown Clocks',
480 | name: 'This button will STOP a clock selected by index (0-based). If you change the index, and still want to display the current time on the button, make sure to also update the index of the clock to watch in this modules config to match.',
481 | style: {
482 | text: 'Stop\\nClock ' + this.config.indexOfClockToWatch + '\\n$(propresenter:watched_clock_current_time)',
483 | size: '14',
484 | color: combineRgb(255, 255, 255),
485 | bgcolor: combineRgb(204, 0, 0),
486 | },
487 | steps: [
488 | {
489 | down: [
490 | {
491 | actionId: 'clockStop',
492 | options: {
493 | clockIndex: this.config.indexOfClockToWatch, // N.B. If user updates indexOfClockToWatch, this preset default will not be updated until module is reloaded.
494 | },
495 | },
496 | ],
497 | },
498 | ],
499 | feedbacks: [],
500 | }
501 | ;(presets['clock_reset'] = {
502 | type: 'button',
503 | category: 'Countdown Clocks',
504 | name: 'This button will RESET a clock selected by index (0-based). If you change the index, and still want to display the current time on the button, make sure to also update the index of the clock to watch in this modules config to match.',
505 | style: {
506 | text: 'Reset\\nClock ' + this.config.indexOfClockToWatch + '\\n$(propresenter:watched_clock_current_time)',
507 | size: '14',
508 | color: combineRgb(255, 255, 255),
509 | bgcolor: combineRgb(255, 102, 0),
510 | },
511 | steps: [
512 | {
513 | down: [
514 | {
515 | actionId: 'clockReset',
516 | options: {
517 | clockIndex: this.config.indexOfClockToWatch, // N.B. If user updates indexOfClockToWatch, this preset default will not be updated until module is reloaded.
518 | },
519 | },
520 | ],
521 | },
522 | ],
523 | feedbacks: [],
524 | }),
525 | this.setPresetDefinitions(presets)
526 | }
527 |
528 | /**
529 | * Initialize an empty current state.
530 | */
531 | emptyCurrentState() {
532 | this.log('debug', 'emptyCurrentState')
533 |
534 | // Reinitialize the currentState variable, otherwise this variable (and the module's
535 | // state) will be shared between multiple instances of this module.
536 | this.currentState = {}
537 |
538 | // The internal state of the connection to ProPresenter
539 | this.currentState.internal = {
540 | wsConnected: false,
541 | wsSDConnected: false,
542 | wsFollowerConnected: false,
543 | presentationPath: '-',
544 | slideIndex: 0,
545 | proMajorVersion: 6, // Behaviour is slightly different between the two major versions of ProPresenter (6 & 7). Use this flag to run version-specific code where required. Default to 6 - Pro7 can be detected once authenticated.
546 | pro7StageLayouts: [{ id: '0', label: 'Connect to Pro7 to Update' }],
547 | pro7StageScreens: [{ id: '0', label: 'Connect to Pro7 to Update' }],
548 | previousTimeOfLeaderClearMessage: null,
549 | pro7Looks: [{ id: '0', label: 'Connect to Pro7 to Update' }],
550 | pro7Macros: [{ id: '0', label: 'Connect to Pro7 to Update' }],
551 | current_pro7_look_id: null,
552 | awaitingSlideByLabelRequest: {}, // When user triggers action to find slide by label and trigger it, this module must first get and search the playlist. So the request is stored here until response for playlistRequestAll is received and thee action can then be completed using the returned playlist data.
553 | matchingPlaylistItemFound: false, // Flag used accross recursive calls to recursivelyScanPlaylistsObjToTriggerSlideByLabel()
554 | awaitingGroupSlideRequest: {}, // When user triggers a new GroupSlide request, this module must first get the presentation and then search for the group slide. So the request is stored here until response for presentationRequest is received and the action can then be completed using hte returned presentation data.
555 | timeOfLastClockUpdate: 0, // Keep track since last 'clockCurrentTimes' message was received - there should be one every second.
556 | timeOfLastConnection: 0, // Keep track of last connection time
557 | }
558 |
559 | // The dynamic variable exposed to Companion
560 | this.currentState.dynamicVariables = {
561 | current_slide: 'N/A',
562 | current_presentation_path: 'N/A',
563 | current_announcement_slide: 'N/A',
564 | current_announcement_presentation_path: 'N/A',
565 | remaining_slides: 'N/A',
566 | total_slides: 'N/A',
567 | presentation_name: 'N/A',
568 | connection_status: 'Disconnected',
569 | sd_connection_status: 'Disconnected',
570 | follower_connection_status: 'Disconnected',
571 | video_countdown_timer: 'N/A',
572 | video_countdown_timer_hourless: 'N/A',
573 | video_countdown_timer_totalseconds: 'N/A',
574 | watched_clock_current_time: 'N/A',
575 | current_stage_display_name: 'N/A',
576 | current_stage_display_index: 'N/A',
577 | current_pro7_stage_layout_name: 'N/A',
578 | current_pro7_look_name: 'N/A',
579 | current_random_number: Math.floor(Math.random() * 10) + 1,
580 | time_since_last_clock_update: 'N/A',
581 | connection_timer: '0',
582 | }
583 |
584 | this.currentState.dynamicVariablesDefs = [
585 | {
586 | name: 'Current Slide number',
587 | variableId: 'current_slide',
588 | },
589 | {
590 | name: 'Current Presentation Path',
591 | variableId: 'current_presentation_path',
592 | },
593 | {
594 | name: 'Remaining Slides',
595 | variableId: 'remaining_slides',
596 | },
597 | {
598 | name: 'Total slides in presentation',
599 | variableId: 'total_slides',
600 | },
601 | {
602 | name: 'Current Announcement slide number',
603 | variableId: 'current_announcement_slide',
604 | },
605 | {
606 | name: 'Current Announcement Presentation Path',
607 | variableId: 'current_announcement_presentation_path',
608 | },
609 | {
610 | name: 'Presentation name',
611 | variableId: 'presentation_name',
612 | },
613 | {
614 | name: 'Connection status',
615 | variableId: 'connection_status',
616 | },
617 | {
618 | name: 'Watched Clock, Current Time',
619 | variableId: 'watched_clock_current_time',
620 | },
621 | {
622 | name: 'Current Stage Display Index',
623 | variableId: 'current_stage_display_index',
624 | },
625 | {
626 | name: 'Current Pro7 Stage Layout Name',
627 | variableId: 'current_pro7_stage_layout_name',
628 | },
629 | {
630 | name: 'Current Pro7 Look Name',
631 | variableId: 'current_pro7_look_name',
632 | },
633 | {
634 | name: 'Current Stage Display Name',
635 | variableId: 'current_stage_display_name',
636 | },
637 | {
638 | name: 'Video Countdown Timer',
639 | variableId: 'video_countdown_timer',
640 | },
641 | {
642 | name: 'Video Countdown Timer Hourless',
643 | variableId: 'video_countdown_timer_hourless',
644 | },
645 | {
646 | name: 'Video Countdown Timer Total Seconds',
647 | variableId: 'video_countdown_timer_totalseconds',
648 | },
649 | {
650 | name: 'Follower Connection Status',
651 | variableId: 'follower_connection_status',
652 | },
653 | {
654 | name: 'Current Random Number',
655 | variableId: 'current_random_number',
656 | },
657 | {
658 | name: 'Time Since Last Clock-Update',
659 | variableId: 'time_since_last_clock_update', // Allows user to monitor "health" of the websocket connection (since we expect timer updates every second, if we track time since last timer update, we can infer when "normal" communication has failed.)
660 | },
661 | {
662 | name: 'Connection Timer',
663 | variableId: 'connection_timer',
664 | },
665 | ]
666 | }
667 |
668 | /**
669 | * Initialize the available variables. (These are listed in the module config UI)
670 | */
671 | initVariables() {
672 | // Initialize the current state and update Companion with the variables.
673 | this.emptyCurrentState()
674 | this.setVariableDefinitions(this.currentState.dynamicVariablesDefs) // Make sure to call this after this.emptyCurrentState() as it intializes this.currentState.dynamicVariablesDefs
675 | this.setVariableValues(this.currentState.dynamicVariables)
676 | }
677 |
678 | /**
679 | * Updates the dynamic variable and records the internal state of that variable.
680 | *
681 | * Will log a warning if the variable doesn't exist.
682 | */
683 | updateVariable(name, value) {
684 | if (!name.includes('_clock_') && !name.includes('time_since_last_clock_update') && !name.includes('_timer')) {
685 | // Avoid flooding log with timer updates by filtering out variables that update every second
686 | this.log('debug', 'updateVariable: ' + name + ' to ' + value)
687 | }
688 |
689 | if (this.currentState.dynamicVariables[name] === undefined) {
690 | this.log('warn', 'Variable ' + name + ' does not exist')
691 | return
692 | }
693 |
694 | this.currentState.dynamicVariables[name] = value
695 |
696 | this.setVariableValues({ [name]: value })
697 |
698 | if (name === 'connection_status') {
699 | this.checkFeedbacks('propresenter_module_connected')
700 | }
701 | }
702 |
703 | startWatchDogTimer() {
704 | this.log('debug', 'Starting Watch Dog Timer')
705 |
706 | // Create watchdog timer to perform various checks/updates once per second.
707 | this.watchDogTimer = setInterval(() => {
708 | if (this.config.looksPolling == 'enabled' && this.socket.readyState == 1 /*OPEN*/) {
709 | // only send when option is enabled AND socket is OPEN
710 | try {
711 | this.socket.send('{"action": "looksRequest"}')
712 | } catch (e) {
713 | this.log('debug', 'NETWORK ' + e)
714 | this.updateStatus(InstanceStatus.UnknownError, e.message)
715 | }
716 | }
717 |
718 | if (this.config.timerPolling == 'enabled' && this.socket.readyState == 1 /*OPEN*/) {
719 | // only send when option is enabled AND socket is OPEN
720 | try {
721 | this.socket.send('{"action": "clockRequest"}')
722 | } catch (e) {
723 | this.log('debug', 'NETWORK ' + e)
724 | this.updateStatus(InstanceStatus.UnknownError, e.message)
725 | }
726 | }
727 |
728 | // Keep track of how long since last clock update was received.
729 | if (this.currentState.internal.timeOfLastClockUpdate > 0) {
730 | this.updateVariable(
731 | 'time_since_last_clock_update',
732 | Date.now() - this.currentState.internal.timeOfLastClockUpdate
733 | )
734 | }
735 |
736 | // Keep track for how long since last connected.
737 | if (this.currentState.internal.timeOfLastConnection > 0) {
738 | this.updateVariable(
739 | 'connection_timer',
740 | Math.floor((Date.now() - this.currentState.internal.timeOfLastConnection) / 1000)
741 | )
742 | }
743 | }, 1000)
744 | }
745 |
746 | /**
747 | * Create a timer to connect to ProPresenter.
748 | */
749 | startConnectionTimer() {
750 | // Stop the timer if it was already running
751 | this.stopConnectionTimer()
752 |
753 | // Create a reconnect timer to watch the socket. If disconnected try to connect.
754 | this.log('info', 'Starting ConnectionTimer')
755 | this.reconTimer = setInterval(() => {
756 | if (this.socket === undefined || this.socket.readyState === 3 /*CLOSED*/) {
757 | // Not connected. Try to connect again.
758 | this.connectToProPresenter()
759 | } else {
760 | this.currentState.internal.wsConnected = true
761 | }
762 | }, 3000)
763 | }
764 |
765 | /**
766 | * Stops the reconnection timer.
767 | */
768 | stopConnectionTimer() {
769 | this.log('debug', 'Stopping ConnectionTimer')
770 | if (this.reconTimer !== undefined) {
771 | clearInterval(this.reconTimer)
772 | delete this.reconTimer
773 | }
774 | }
775 |
776 | /**
777 | * Stops the Watch Dog Timer.
778 | */
779 | stopWatchDogTimer() {
780 | this.log('debug', 'Stopping watchDogTimer')
781 | if (this.watchDogTimer !== undefined) {
782 | clearInterval(this.watchDogTimer)
783 | delete this.watchDogTimer
784 | }
785 | }
786 |
787 | /**
788 | * Create a timer to connect to ProPresenter stage display.
789 | */
790 | startSDConnectionTimer() {
791 | // Stop the timer if it was already running
792 | this.stopSDConnectionTimer()
793 |
794 | // Create a reconnect timer to watch the socket. If disconnected try to connect
795 | this.log('debug', 'Starting SDConnectionTimer')
796 | this.reconSDTimer = setInterval(() => {
797 | if (this.sdsocket === undefined || this.sdsocket.readyState === 3 /*CLOSED*/) {
798 | // Not connected. Try to connect again.
799 | this.connectToProPresenterSD()
800 | } else {
801 | this.currentState.internal.wsSDConnected = true
802 | }
803 | }, 5000)
804 | }
805 |
806 | /**
807 | * Stops the stage display reconnection timer.
808 | */
809 | stopSDConnectionTimer() {
810 | this.log('debug', 'Stopping SDConnectionTimer')
811 | if (this.reconSDTimer !== undefined) {
812 | clearInterval(this.reconSDTimer)
813 | delete this.reconSDTimer
814 | }
815 | }
816 |
817 | /**
818 | * Create a timer to connect to Follower ProPresenter.
819 | */
820 | startFollowerConnectionTimer() {
821 | // Stop the timer if it was already running
822 | this.stopFollowerConnectionTimer()
823 |
824 | this.log('debug', 'Starting Follower ConnectionTimer')
825 | // Create a reconnect timer to watch the socket. If disconnected try to connect.
826 | this.reconFollowerTimer = setInterval(() => {
827 | if (
828 | this.followersocket === undefined ||
829 | this.followersocket.readyState === 3 /*CLOSED*/ ||
830 | this.followersocket.readyState === 2 /*CLOSING*/
831 | ) {
832 | // Not connected.
833 | this.currentState.internal.wsFollowerConnected = false
834 | // Try to connect again.
835 | this.connectToFollowerProPresenter()
836 | } else {
837 | if (this.followersocket.readyState === 1 /*OPEN*/) {
838 | this.currentState.internal.wsFollowerConnected = true
839 | }
840 | }
841 | }, 3000)
842 | }
843 |
844 | /**
845 | * Stops the follower reconnection timer.
846 | */
847 | stopFollowerConnectionTimer() {
848 | this.log('debug', 'Stopping Follower ConnectionTimer')
849 | if (this.reconFollowerTimer !== undefined) {
850 | clearInterval(this.reconFollowerTimer)
851 | delete this.reconFollowerTimer
852 | }
853 | }
854 |
855 | /**
856 | * Updates the connection status variable.
857 | */
858 | setConnectionVariable(status, updateLog) {
859 | this.updateVariable('connection_status', status)
860 |
861 | if (updateLog) {
862 | this.log('info', 'ProPresenter ' + status)
863 | }
864 | }
865 |
866 | /**
867 | * Updates the stage display connection status variable.
868 | */
869 | setSDConnectionVariable(status, updateLog) {
870 | this.updateVariable('sd_connection_status', status)
871 |
872 | if (updateLog) {
873 | this.log('info', 'ProPresenter Stage Display ' + status)
874 | }
875 | }
876 |
877 | /**
878 | * Disconnect the websocket from ProPresenter, if connected.
879 | */
880 | disconnectFromProPresenter() {
881 | if (this.socket !== undefined) {
882 | // Disconnect if already connected
883 | if (this.socket.readyState !== 3 /*CLOSED*/) {
884 | this.socket.terminate()
885 | }
886 | delete this.socket
887 | }
888 | this.currentState.internal.wsConnected = false
889 | this.setConnectionVariable('Disconnected', true)
890 | }
891 |
892 | /**
893 | * Disconnect the websocket from ProPresenter stage display, if connected.
894 | */
895 | disconnectFromProPresenterSD() {
896 | if (this.sdsocket !== undefined) {
897 | // Disconnect if already connected
898 | if (this.sdsocket.readyState !== 3 /*CLOSED*/) {
899 | this.sdsocket.terminate()
900 | }
901 | delete this.sdsocket
902 | }
903 | }
904 |
905 | /**
906 | * Disconnect the websocket from Follower ProPresenter, if connected.
907 | */
908 | disconnectFromFollowerProPresenter() {
909 | if (this.followersocket !== undefined) {
910 | // Disconnect if already connected
911 | if (this.followersocket.readyState !== 3 /*CLOSED*/) {
912 | this.followersocket.terminate()
913 | }
914 | delete this.followersocket
915 | }
916 |
917 | this.checkFeedbacks('propresenter_follower_connected')
918 | }
919 |
920 | /**
921 | * Attempts to open a websocket connection with ProPresenter.
922 | */
923 | connectToProPresenter() {
924 | // Check for undefined host or port. Also make sure port is [1-65535] and host is least 1 char long.
925 | if (
926 | !this.config.host ||
927 | this.config.host.length < 1 ||
928 | !this.config.port ||
929 | this.config.port < 1 ||
930 | this.config.port > 65535
931 | ) {
932 | // Do not try to connect with invalid host or port
933 | return
934 | }
935 |
936 | // Disconnect if already connected
937 | this.disconnectFromProPresenter()
938 |
939 | this.log('debug', 'OPENING: ' + this.config.host + ':' + this.config.port)
940 | // Connect to remote control websocket of ProPresenter
941 | this.socket = new WebSocket('ws://' + this.config.host + ':' + this.config.port + '/remote')
942 |
943 | this.socket.on('open', () => {
944 | this.log('info', 'Opened websocket to ProPresenter remote control: ' + this.config.host + ':' + this.config.port)
945 | this.currentState.internal.timeOfLastConnection = Date.now()
946 | this.updateVariable('connection_timer', 0)
947 | this.socket.send(
948 | JSON.stringify({
949 | password: this.config.pass,
950 | protocol: this.config.clientVersion ? this.config.clientVersion : '701', // This will connect to Pro6 and Pro7 (the version check is happy with higher versions - but versions too low will be refused)
951 | action: 'authenticate',
952 | })
953 | )
954 | })
955 |
956 | this.socket.on('error', (err) => {
957 | this.log('debug', 'Socket error: ' + err.message)
958 | this.updateStatus(InstanceStatus.UnknownError, err.message)
959 | })
960 |
961 | this.socket.on('connect', () => {
962 | this.log('debug', 'Connected to ProPresenter remote control')
963 | })
964 |
965 | this.socket.on('close', (code, reason) => {
966 | // Event is also triggered when a reconnect attempt fails.
967 | // Reset the current state then abort; don't flood logs with disconnected notices.
968 | var wasConnected = this.currentState.internal.wsConnected
969 |
970 | this.log('debug', 'socket closed')
971 |
972 | if (wasConnected === false) {
973 | return
974 | }
975 |
976 | this.emptyCurrentState() // This is also sets this.currentState.internal.wsConnected to false
977 |
978 | this.updateStatus(InstanceStatus.UnknownError, 'Not connected to ProPresenter')
979 | this.setConnectionVariable('Disconnected', true)
980 | })
981 |
982 | this.socket.on('message', (message) => {
983 | // Handle the message received from ProPresenter
984 | this.onWebSocketMessage(message)
985 | })
986 | }
987 |
988 | /**
989 | * Attempts to open a websocket connection with ProPresenter stage display.
990 | */
991 | connectToProPresenterSD() {
992 | // Check for undefined host or port. Also make sure port is [1-65535] and host is least 1 char long.
993 | if (
994 | !this.config.host ||
995 | this.config.host.length < 1 ||
996 | !this.config.port ||
997 | this.config.port < 1 ||
998 | this.config.port > 65535
999 | ) {
1000 | // Do not try to connect with invalid host or port
1001 | return
1002 | }
1003 |
1004 | // Disconnect if already connected
1005 | this.disconnectFromProPresenterSD()
1006 |
1007 | if (this.config.host === undefined) {
1008 | return
1009 | }
1010 |
1011 | // Check for undefined sdport. Also make sure sdport is [1-65535]. (Otherwise, use ProPresenter remote port)
1012 | if (!this.config.sdport || this.config.sdport < 1 || this.config.sdport > 65535) {
1013 | this.config.sdport = this.config.port
1014 | }
1015 |
1016 | // Connect to Stage Display websocket of ProPresenter
1017 | this.sdsocket = new WebSocket('ws://' + this.config.host + ':' + this.config.sdport + '/stagedisplay')
1018 |
1019 | this.sdsocket.on('open', () => {
1020 | this.log('info', 'Opened websocket to ProPresenter stage display: ' + this.config.host + ':' + this.config.sdport)
1021 | this.sdsocket.send(
1022 | JSON.stringify({
1023 | pwd: this.config.sdpass,
1024 | ptl: 610, //Pro7 still wants 610 ! (so this works for both Pro6 and Pro7)
1025 | acn: 'ath',
1026 | })
1027 | )
1028 | })
1029 |
1030 | // Since Stage Display connection is not required to function - we will only send a warning if it fails
1031 | this.sdsocket.on('error', (err) => {
1032 | // If stage display can't connect - it's not really a "code red" error - since *most* of the core functionally does not require it.
1033 | // Therefore, a failure to connect stage display is more of a warning state.
1034 | // However, if the module is already in error, then we should not lower that to warning!
1035 | if (this.currentStatus !== InstanceStatus.UnknownError && this.config.use_sd === 'yes') {
1036 | this.updateStatus(InstanceStatus.UnknownWarning, 'OK - Stage Display not connected')
1037 | }
1038 | this.log('debug', 'SD socket error: ' + err.message)
1039 | })
1040 |
1041 | this.sdsocket.on('connect', () => {
1042 | this.log('debug', 'Connected to ProPresenter stage display')
1043 | })
1044 |
1045 | this.sdsocket.on('close', (code, reason) => {
1046 | // Event is also triggered when a reconnect attempt fails.
1047 | // Reset the current state then return from this function and avoid flooding logs with disconnected notices.
1048 | if (this.currentState.internal.wsSDConnected === false) {
1049 | return
1050 | }
1051 | this.currentState.internal.wsSDConnected = false // Just set this var instead of emptyCurrentState (this is all SD connection is used for)
1052 |
1053 | if (this.config.use_sd === 'yes' && this.socket !== undefined && this.socket.readyState === 1 /* OPEN */) {
1054 | this.updateStatus(InstanceStatus.UnknownWarning, 'OK, But Stage Display closed')
1055 | }
1056 | this.log('debug', 'SD Disconnected')
1057 | this.setSDConnectionVariable('Disconnected', true)
1058 | })
1059 |
1060 | this.sdsocket.on('message', (message) => {
1061 | // Handle the stage display message received from ProPresenter
1062 | this.onSDWebSocketMessage(message)
1063 | })
1064 | }
1065 |
1066 | /**
1067 | * Attempts to open a websocket connection with Follower ProPresenter.
1068 | */
1069 | connectToFollowerProPresenter() {
1070 | // Check for undefined host or port. Also make sure port is [1-65535] and host is least 1 char long.
1071 | if (
1072 | !this.config.followerhost ||
1073 | this.config.followerhost.length < 1 ||
1074 | !this.config.followerport ||
1075 | this.config.followerport < 1 ||
1076 | this.config.followerport > 65535
1077 | ) {
1078 | // Do not try to connect with invalid host or port
1079 | return
1080 | }
1081 |
1082 | // Disconnect if already connected
1083 | this.disconnectFromFollowerProPresenter()
1084 |
1085 | // Connect to remote control websocket of ProPresenter
1086 | this.followersocket = new WebSocket('ws://' + this.config.followerhost + ':' + this.config.followerport + '/remote')
1087 |
1088 | this.followersocket.on('open', () => {
1089 | this.log(
1090 | 'info',
1091 | 'Opened websocket to Follower ProPresenter remote control: ' +
1092 | this.config.followerhost +
1093 | ':' +
1094 | this.config.followerport
1095 | )
1096 | this.followersocket.send(
1097 | JSON.stringify({
1098 | password: this.config.followerpass,
1099 | protocol: this.config.clientVersion ? this.config.clientVersion : '701', // This will connect to Pro6 and Pro7 (the version check is happy with higher versions)
1100 | action: 'authenticate',
1101 | })
1102 | )
1103 | })
1104 |
1105 | this.followersocket.on('error', (err) => {
1106 | if (this.config.control_follower === 'yes') {
1107 | this.log('warn', 'Follower Socket error: ' + err.message)
1108 | }
1109 | this.currentState.internal.wsFollowerConnected = false
1110 | })
1111 |
1112 | this.followersocket.on('close', (code, reason) => {
1113 | // Event is also triggered when a reconnect attempt fails.
1114 | // Reset the current state then abort; don't flood logs with disconnected notices.
1115 | var wasFollowerConnected = this.currentState.internal.wsFollowerConnected
1116 | this.currentState.internal.wsFollowerConnected = false
1117 |
1118 | if (wasFollowerConnected === false) {
1119 | return
1120 | }
1121 | this.log('info', 'Follower ProPresenter socket connection closed')
1122 | })
1123 |
1124 | this.followersocket.on('message', (message) => {
1125 | // Handle the message received from ProPresenter
1126 | this.onFollowerWebSocketMessage(message)
1127 | })
1128 | }
1129 |
1130 | init_feedbacks = () => {
1131 | /**
1132 | * @type{import('@companion-module/base').CompanionFeedbackDefinitions
1133 | */
1134 | var feedbacks = {}
1135 | feedbacks['stagedisplay_active'] = {
1136 | type: 'advanced',
1137 | name: 'Change colors based on active stage display',
1138 | description: 'If the specified stage display is active, change colors of the bank',
1139 | options: [
1140 | {
1141 | type: 'colorpicker',
1142 | label: 'Foreground color',
1143 | id: 'fg',
1144 | default: combineRgb(255, 255, 255),
1145 | },
1146 | {
1147 | type: 'colorpicker',
1148 | label: 'Background color',
1149 | id: 'bg',
1150 | default: combineRgb(0, 153, 51),
1151 | },
1152 | {
1153 | type: 'textinput',
1154 | label: 'Stage Display Index',
1155 | id: 'index',
1156 | default: '0',
1157 | regex: Regex.NUMBER,
1158 | },
1159 | ],
1160 | callback: (feedback) => {
1161 | if (this.currentState.internal.stageDisplayIndex == feedback.options.index) {
1162 | return { color: Number(feedback.options.fg), bgcolor: Number(feedback.options.bg) }
1163 | } else {
1164 | return {}
1165 | }
1166 | },
1167 | }
1168 |
1169 | feedbacks['pro7_stagelayout_active'] = {
1170 | type: 'advanced',
1171 | name: "Change colors based on the active layout for one of Pro7's stage screens",
1172 | description: 'If the specified stage layout is active on the specified stage screen, change colors of the bank',
1173 | options: [
1174 | {
1175 | type: 'colorpicker',
1176 | label: 'Foreground color',
1177 | id: 'fg',
1178 | default: combineRgb(255, 255, 255),
1179 | },
1180 | {
1181 | type: 'colorpicker',
1182 | label: 'Background color',
1183 | id: 'bg',
1184 | default: combineRgb(0, 153, 51),
1185 | },
1186 | {
1187 | type: 'dropdown',
1188 | label: 'Pro7 Stage Display Screen',
1189 | id: 'pro7StageScreenUUID',
1190 | tooltip: 'Choose which stage display screen you want to monitor',
1191 | choices: this.currentState.internal.pro7StageScreens,
1192 | default: this.currentState.internal.pro7StageScreens?.[0]?.id,
1193 | },
1194 | {
1195 | type: 'dropdown',
1196 | label: 'Pro7 Stage Display Layout',
1197 | id: 'pro7StageLayoutUUID',
1198 | tooltip: 'Choose the stage display layout to trigger above color change',
1199 | choices: this.currentState.internal.pro7StageLayouts,
1200 | default: this.currentState.internal.pro7StageLayouts?.[0]?.id,
1201 | },
1202 | ],
1203 | callback: (feedback) => {
1204 | // Get screen (includes current layout)
1205 | var stageScreen = this.currentState.internal.pro7StageScreens.find(
1206 | (pro7StageScreen) =>
1207 | pro7StageScreen.id ===
1208 | (feedback.options.pro7StageScreenUUID
1209 | ? feedback.options.pro7StageScreenUUID
1210 | : this.currentState.internal.pro7StageScreens[0].id)
1211 | )
1212 |
1213 | this.log('debug', 'feedback for ' + feedback.options.pro7StageScreenUUID)
1214 |
1215 | // Exit if we could not find matching screen
1216 | if (stageScreen === undefined) {
1217 | return {}
1218 | }
1219 |
1220 | // Check stage layout for screeen and return feedback color if matched
1221 | if (
1222 | stageScreen.layoutUUID ===
1223 | (feedback.options.pro7StageLayoutUUID
1224 | ? feedback.options.pro7StageLayoutUUID
1225 | : this.currentState.internal.pro7StageLayouts[0].id)
1226 | ) {
1227 | return { color: Number(feedback.options.fg), bgcolor: Number(feedback.options.bg) }
1228 | } else {
1229 | return {}
1230 | }
1231 | },
1232 | }
1233 |
1234 | feedbacks['active_look'] = {
1235 | type: 'advanced',
1236 | name: 'Change colors based on active look',
1237 | description: 'If the specified look display is active, change colors of the bank',
1238 | options: [
1239 | {
1240 | type: 'colorpicker',
1241 | label: 'Foreground color',
1242 | id: 'fg',
1243 | default: combineRgb(255, 255, 255),
1244 | },
1245 | {
1246 | type: 'colorpicker',
1247 | label: 'Background color',
1248 | id: 'bg',
1249 | default: combineRgb(0, 153, 51),
1250 | },
1251 | {
1252 | type: 'dropdown',
1253 | label: 'Look',
1254 | id: 'look',
1255 | tooltip: 'Choose the Look to trigger above color change',
1256 | choices: this.currentState.internal.pro7Looks,
1257 | default: this.currentState.internal.pro7Looks?.[0]?.id,
1258 | },
1259 | ],
1260 | callback: (feedback) => {
1261 | if (this.currentState.internal.current_pro7_look_id == feedback.options.look) {
1262 | return { color: Number(feedback.options.fg), bgcolor: Number(feedback.options.bg) }
1263 | } else {
1264 | return {}
1265 | }
1266 | },
1267 | }
1268 |
1269 | feedbacks['propresenter_module_connected'] = {
1270 | type: 'advanced',
1271 | name: 'Change colors based on Propresenter module being connected',
1272 | description: 'Propresenter module being connected, change colors of the bank',
1273 | options: [
1274 | {
1275 | type: 'colorpicker',
1276 | label: 'Connected Foreground color',
1277 | id: 'cfg',
1278 | default: combineRgb(255, 255, 255),
1279 | },
1280 | {
1281 | type: 'colorpicker',
1282 | label: 'Connected Background color',
1283 | id: 'cbg',
1284 | default: combineRgb(0, 153, 51),
1285 | },
1286 | {
1287 | type: 'colorpicker',
1288 | label: 'Disconnected Foreground color',
1289 | id: 'dfg',
1290 | default: combineRgb(255, 255, 255),
1291 | },
1292 | {
1293 | type: 'colorpicker',
1294 | label: 'Disconnected Background color',
1295 | id: 'dbg',
1296 | default: combineRgb(204, 0, 0),
1297 | },
1298 | ],
1299 | callback: (feedback) => {
1300 | if (this.currentState.internal.wsConnected) {
1301 | return { color: Number(feedback.options.cfg), bgcolor: Number(feedback.options.cbg) }
1302 | } else {
1303 | return { color: Number(feedback.options.dfg), bgcolor: Number(feedback.options.dbg) }
1304 | }
1305 | },
1306 | }
1307 |
1308 | feedbacks['propresenter_follower_connected'] = {
1309 | type: 'advanced',
1310 | name: 'Change colors based on Propresenter follower being connected',
1311 | description: 'Propresenter follower being connected, change colors of the bank',
1312 | options: [
1313 | {
1314 | type: 'colorpicker',
1315 | label: 'Connected & Controlled Foreground color',
1316 | id: 'fcfg',
1317 | default: combineRgb(255, 255, 255),
1318 | },
1319 | {
1320 | type: 'colorpicker',
1321 | label: 'Connected & Controlled Background color',
1322 | id: 'fcbg',
1323 | default: combineRgb(0, 153, 51),
1324 | },
1325 | {
1326 | type: 'colorpicker',
1327 | label: 'Connected & Control Disabled Foreground color',
1328 | id: 'fcdfg',
1329 | default: combineRgb(255, 255, 255),
1330 | },
1331 | {
1332 | type: 'colorpicker',
1333 | label: 'Connected & Control Disabled Background color',
1334 | id: 'fcdbg',
1335 | default: combineRgb(255, 102, 10),
1336 | },
1337 | {
1338 | type: 'colorpicker',
1339 | label: 'Disconnected Foreground color',
1340 | id: 'fdfg',
1341 | default: combineRgb(255, 255, 255),
1342 | },
1343 | {
1344 | type: 'colorpicker',
1345 | label: 'Disconnected Background color',
1346 | id: 'fdbg',
1347 | default: combineRgb(204, 0, 0),
1348 | },
1349 | ],
1350 | callback: (feedback) => {
1351 | if (this.currentState.internal.wsFollowerConnected) {
1352 | if (this.config.control_follower === 'yes') {
1353 | return { color: Number(feedback.options.fcfg), bgcolor: Number(feedback.options.fcbg) }
1354 | } else {
1355 | return { color: Number(feedback.options.fcdfg), bgcolor: Number(feedback.options.fcdbg) }
1356 | }
1357 | } else {
1358 | return { color: Number(feedback.options.fdfg), bgcolor: Number(feedback.options.fdbg) }
1359 | }
1360 | },
1361 | }
1362 |
1363 | this.setFeedbackDefinitions(feedbacks)
1364 | }
1365 |
1366 | /**
1367 | * Received a message from ProPresenter.
1368 | */
1369 | onWebSocketMessage = (message) => {
1370 | var objData
1371 |
1372 | // Try to parse websocket payload as JSON...
1373 | try {
1374 | objData = JSON.parse(message)
1375 | } catch (err) {
1376 | this.log('warn', err.message)
1377 | return
1378 | }
1379 |
1380 | switch (objData.action) {
1381 | case 'authenticate':
1382 | if (objData.authenticated === 1) {
1383 | // Autodetect if Major version of ProPresenter is version 7
1384 | // Only Pro7 includes .majorVersion and .minorVersion properties.
1385 | // .majorVersion will be set to = "7" from Pro7 (Pro6 does not include these at all)
1386 | if (objData.hasOwnProperty('majorVersion')) {
1387 | if (objData.majorVersion === 7) {
1388 | this.currentState.internal.proMajorVersion = 7
1389 | }
1390 | } else {
1391 | // Leave default
1392 | }
1393 |
1394 | this.log(
1395 | 'info',
1396 | 'Authenticated to ProPresenter (Version: ' + this.currentState.internal.proMajorVersion + ')'
1397 | )
1398 | this.updateStatus(InstanceStatus.Ok)
1399 | this.currentState.internal.wsConnected = true
1400 | // Successfully authenticated. Request current state.
1401 | this.setConnectionVariable('Connected', true)
1402 | this.getProPresenterState(true) // Force refresh with 'presentationCurrent' after first connection is authenticated (to ensure we alway have presentationPath)
1403 | this.init_feedbacks()
1404 | // Get current Stage Display (index and Name)
1405 | this.getStageDisplaysInfo()
1406 | // Get current Pro7 Macros & Looks List.
1407 | if (this.currentState.internal.proMajorVersion >= 7) {
1408 | this.getMacrosList()
1409 | this.getLooksList()
1410 | }
1411 |
1412 | // Ask ProPresenter to start sending clock updates (they are sent once per second)
1413 | this.socket.send(
1414 | JSON.stringify({
1415 | action: 'clockStartSendingCurrentTime',
1416 | })
1417 | )
1418 | } else {
1419 | this.updateStatus(InstanceStatus.UnknownError)
1420 | // Bad password
1421 | this.log('warn', 'Failed to authenticate to ProPresenter. ' + objData.error)
1422 | this.disconnectFromProPresenter()
1423 |
1424 | // No point in trying to connect again. The user must either re-enable this
1425 | // module or re-save the config changes to make another attempt.
1426 | this.stopConnectionTimer()
1427 | }
1428 | break
1429 |
1430 | case 'presentationTriggerIndex':
1431 | this.updateVariable('current_presentation_path', String(objData.presentationPath)) // this is included in presentationTriggerIndex - but not presentationTriggerIndex
1432 | // Do not break - processing there two mesages is basically the same (except presentationPath)
1433 | case 'presentationSlideIndex':
1434 | // Update the current slide index.
1435 | var slideIndex = parseInt(objData.slideIndex, 10)
1436 |
1437 | if (objData.hasOwnProperty('presentationDestination') && objData.presentationDestination == 1) {
1438 | // Track Announcement layer presentationPath and Slide Index
1439 | this.updateVariable('current_announcement_slide', slideIndex + 1)
1440 | this.updateVariable('current_announcement_presentation_path', String(objData.presentationPath))
1441 | } else {
1442 | // Track Presentation layer presentationPath, Slide Index )and optionally remaining slides)
1443 | this.currentState.internal.slideIndex = slideIndex
1444 | this.updateVariable('current_slide', slideIndex + 1)
1445 | if (objData.presentationPath == this.currentState.internal.presentationPath) {
1446 | // If the triggered slide is part of the current presentation (for which we have stored the total slides) then update the 'remaining_slides' dynamic variable
1447 | // Note that, if the triggered slide is NOT part of the current presentation, the 'remaining_slides' dynamic variable will be updated later when we call the presentationCurrent action to refresh current presentation info.
1448 | this.updateVariable('remaining_slides', this.currentState.dynamicVariables['total_slides'] - slideIndex - 1)
1449 | }
1450 | }
1451 |
1452 | // Workaround for bug that occurs when a presentation with automatically triggered slides (eg go-to-next timer), fires one of it's slides while *another* presentation is selected and before any slides within the newly selected presentation are fired. This will lead to total_slides being wrong (and staying wrong) even after the user fires slides within the newly selected presentation.
1453 | setTimeout(() => {
1454 | this.getProPresenterState()
1455 | }, 400)
1456 | this.log(
1457 | 'info',
1458 | 'Slide Triggered: ' +
1459 | String(objData.presentationPath) +
1460 | '.' +
1461 | String(objData.slideIndex) +
1462 | ' on layerid: ' +
1463 | String(objData.presentationDestination)
1464 | )
1465 |
1466 | // Trigger same slide in follower ProPresenter (If configured and connected)
1467 | if (this.config.control_follower === 'yes' && this.currentState.internal.wsFollowerConnected) {
1468 | const cmd = {
1469 | action: 'presentationTriggerIndex',
1470 | slideIndex: String(slideIndex),
1471 | // Pro 6 for Windows requires 'presentationPath' to be set.
1472 | presentationPath: objData.presentationPath,
1473 | }
1474 | this.log('debug', 'Forwarding command to Follower: ' + JSON.stringify(cmd))
1475 | try {
1476 | var cmdJSON = JSON.stringify(cmd)
1477 | this.followersocket.send(cmdJSON)
1478 | } catch (e) {
1479 | this.log('debug', 'Follower NETWORK ' + e)
1480 | }
1481 | }
1482 |
1483 | break
1484 |
1485 | case 'clearText':
1486 | // Forward command to follower (Only if clearText is recieved twice less than 300msec apart - Since Pro7.4.1 on Windows sends clearText for every slide and send it twice for real clearText action)
1487 | var timeOfThisClearMessage = new Date()
1488 | if (
1489 | this.config.control_follower === 'yes' &&
1490 | this.currentState.internal.wsFollowerConnected &&
1491 | this.currentState.internal.previousTimeOfLeaderClearMessage != null &&
1492 | timeOfThisClearMessage.getTime() - this.currentState.internal.previousTimeOfLeaderClearMessage.getTime() < 300
1493 | ) {
1494 | const cmd = {
1495 | action: 'clearText',
1496 | }
1497 | this.log('debug', 'Forwarding command to Follower: ' + JSON.stringify(cmd))
1498 | try {
1499 | var cmdJSON = JSON.stringify(cmd)
1500 | this.followersocket.send(cmdJSON)
1501 | } catch (e) {
1502 | this.log('debug', 'Follower NETWORK ' + e)
1503 | }
1504 | }
1505 | this.currentState.internal.previousTimeOfLeaderClearMessage = timeOfThisClearMessage
1506 | break
1507 |
1508 | case 'clearAll':
1509 | // Forward command to follower
1510 | if (this.config.control_follower === 'yes' && this.currentState.internal.wsFollowerConnected) {
1511 | const cmd = {
1512 | action: 'clearAll',
1513 | }
1514 | this.log('debug', 'Forwarding command to Follower: ' + JSON.stringify(cmd))
1515 | try {
1516 | var cmdJSON = JSON.stringify(cmd)
1517 | this.followersocket.send(cmdJSON)
1518 | } catch (e) {
1519 | this.log('debug', 'Follower NETWORK ' + e)
1520 | }
1521 | }
1522 | break
1523 |
1524 | case 'clearVideo':
1525 | // Forward command to follower
1526 | if (this.config.control_follower === 'yes' && this.currentState.internal.wsFollowerConnected) {
1527 | const cmd = {
1528 | action: 'clearVideo',
1529 | }
1530 | this.log('debug', 'Forwarding command to Follower: ' + JSON.stringify(cmd))
1531 | try {
1532 | var cmdJSON = JSON.stringify(cmd)
1533 | this.followersocket.send(cmdJSON)
1534 | } catch (e) {
1535 | this.log('debug', 'Follower NETWORK ' + e)
1536 | }
1537 | }
1538 | break
1539 |
1540 | case 'clearAudio':
1541 | // Forward command to follower
1542 | if (this.config.control_follower === 'yes' && this.currentState.internal.wsFollowerConnected) {
1543 | const cmd = {
1544 | action: 'clearAudio',
1545 | }
1546 | this.log('debug', 'Forwarding command to Follower: ' + JSON.stringify(cmd))
1547 | try {
1548 | var cmdJSON = JSON.stringify(cmd)
1549 | this.followersocket.send(cmdJSON)
1550 | } catch (e) {
1551 | this.log('debug', 'Follower NETWORK ' + e)
1552 | }
1553 | }
1554 | break
1555 |
1556 | case 'presentationCurrent':
1557 | var objPresentation = objData.presentation
1558 |
1559 | // Check for awaiting SlideByLabel request
1560 | // If found, we need to interate over the groups/slides nested array (linearly in order) - counting slides until it finds a match...
1561 | // ...then we will have slideIndex to use in the {"action":"presentationTriggerIndex","slideIndex":[SLIDE INDEX],"presentationPath":"[PRESENTATION PATH]"}
1562 | if (
1563 | this.currentState.internal.awaitingSlideByLabelRequest.hasOwnProperty('presentationPath') &&
1564 | this.currentState.internal.awaitingSlideByLabelRequest.presentationPath == objData.presentationPath
1565 | ) {
1566 | this.log(
1567 | 'debug',
1568 | 'Found matching awaitingSlideByLabelRequest: ' +
1569 | JSON.stringify(this.currentState.internal.awaitingSlideByLabelRequest)
1570 | )
1571 | var slideIndex = 0
1572 | var foundSlide = false
1573 | for (
1574 | var presentationSlideGroupsIndex = 0;
1575 | presentationSlideGroupsIndex < objPresentation.presentationSlideGroups.length;
1576 | presentationSlideGroupsIndex++
1577 | ) {
1578 | for (
1579 | var groupSlidesIndex = 0;
1580 | groupSlidesIndex <
1581 | objPresentation.presentationSlideGroups[presentationSlideGroupsIndex].groupSlides.length;
1582 | groupSlidesIndex++
1583 | ) {
1584 | if (
1585 | objPresentation.presentationSlideGroups[presentationSlideGroupsIndex].groupSlides[groupSlidesIndex]
1586 | .slideLabel == this.currentState.internal.awaitingSlideByLabelRequest.slideLabel
1587 | ) {
1588 | this.log(
1589 | 'debug',
1590 | 'Labels match: ' +
1591 | objPresentation.presentationSlideGroups[presentationSlideGroupsIndex].groupSlides[groupSlidesIndex]
1592 | .slideLabel +
1593 | '=' +
1594 | this.currentState.internal.awaitingSlideByLabelRequest.slideLabel +
1595 | ' at index: ' +
1596 | slideIndex
1597 | )
1598 | foundSlide = true
1599 | }
1600 | if (foundSlide) {
1601 | break
1602 | } else {
1603 | slideIndex++
1604 | }
1605 | }
1606 | if (foundSlide) {
1607 | break
1608 | }
1609 | }
1610 | if (foundSlide) {
1611 | // we have finally found the slide, within it's presentation & playlist - send presentationTriggerIndex to trigger it
1612 | const cmd = {
1613 | action: 'presentationTriggerIndex',
1614 | slideIndex: String(slideIndex),
1615 | presentationPath: this.currentState.internal.awaitingSlideByLabelRequest.presentationPath,
1616 | }
1617 | this.log('debug', 'cmd=' + JSON.stringify(cmd))
1618 | try {
1619 | if (this.socket.readyState == 1 /*OPEN*/) {
1620 | this.socket.send(JSON.stringify(cmd))
1621 | }
1622 | } catch (e) {
1623 | this.log('debug', 'Socket Send Error: ' + e.message)
1624 | }
1625 | } else {
1626 | this.log(
1627 | 'debug',
1628 | 'Could not find slide with label: ' + this.currentState.internal.awaitingSlideByLabelRequest.slideLabel
1629 | )
1630 | }
1631 | this.currentState.internal.awaitingSlideByLabelRequest = {} // All done, reset awaitingSlideByLabelRequest
1632 | }
1633 |
1634 | // Check for awaiting GroupSlide request
1635 | // If found, we need to interate over the groups/slides nested array (linearly in order) - to find specified slide in specified group
1636 | // ...then we will have slideIndex to use in the {"action":"presentationTriggerIndex","slideIndex":[SLIDE INDEX],"presentationPath":"[PRESENTATION PATH]"}
1637 | if (
1638 | this.currentState.internal.awaitingGroupSlideRequest.hasOwnProperty('presentationPath') &&
1639 | this.currentState.internal.awaitingGroupSlideRequest.presentationPath == objData.presentationPath
1640 | ) {
1641 | this.log(
1642 | 'debug',
1643 | 'Found matching awaitingGroupSlideRequest: ' +
1644 | JSON.stringify(this.currentState.internal.awaitingGroupSlideRequest)
1645 | )
1646 |
1647 | var groupNames = this.currentState.internal.awaitingGroupSlideRequest.groupName.split('|') // Search each group given (separated by |)
1648 | for (var groupNameIndex = 0; groupNameIndex < groupNames.length; groupNameIndex++) {
1649 | var slideIndex = 0
1650 | var foundSlide = false
1651 | for (
1652 | var presentationSlideGroupsIndex = 0;
1653 | presentationSlideGroupsIndex < objPresentation.presentationSlideGroups.length;
1654 | presentationSlideGroupsIndex++
1655 | ) {
1656 | for (
1657 | var groupSlidesIndex = 0;
1658 | groupSlidesIndex <
1659 | objPresentation.presentationSlideGroups[presentationSlideGroupsIndex].groupSlides.length;
1660 | groupSlidesIndex++
1661 | ) {
1662 | if (
1663 | objPresentation.presentationSlideGroups[presentationSlideGroupsIndex].groupName ==
1664 | groupNames[groupNameIndex] &&
1665 | groupSlidesIndex == this.currentState.internal.awaitingGroupSlideRequest.slideNumber - 1
1666 | ) {
1667 | this.log(
1668 | 'debug',
1669 | 'Found Group Slide: ' +
1670 | objPresentation.presentationSlideGroups[presentationSlideGroupsIndex].groupName +
1671 | '=' +
1672 | groupNames[groupNameIndex] +
1673 | ' at index: ' +
1674 | slideIndex
1675 | )
1676 | foundSlide = true
1677 | }
1678 | if (foundSlide) {
1679 | break
1680 | } else {
1681 | slideIndex++
1682 | }
1683 | }
1684 | if (foundSlide) {
1685 | break
1686 | }
1687 | }
1688 | if (foundSlide) {
1689 | break
1690 | }
1691 | }
1692 |
1693 | if (foundSlide) {
1694 | // we have finally found the slide, within it's presentation & playlist - send presentationTriggerIndex to trigger it
1695 | const cmd = {
1696 | action: 'presentationTriggerIndex',
1697 | slideIndex: String(slideIndex),
1698 | presentationPath: this.currentState.internal.awaitingGroupSlideRequest.presentationPath,
1699 | }
1700 | this.log('debug', 'cmd=' + JSON.stringify(cmd))
1701 | try {
1702 | if (this.socket.readyState == 1 /*OPEN*/) {
1703 | this.socket.send(JSON.stringify(cmd))
1704 | }
1705 | } catch (e) {
1706 | this.log('debug', 'Socket Send Error: ' + e.message)
1707 | }
1708 | } else {
1709 | this.log(
1710 | 'debug',
1711 | 'Could not find slide ' +
1712 | this.currentState.internal.awaitingGroupSlideRequest.slideNumber +
1713 | ' in group(s): ' +
1714 | this.currentState.internal.awaitingGroupSlideRequest.groupName
1715 | )
1716 | }
1717 | this.currentState.internal.awaitingGroupSlideRequest = {} // All done, reset awaitingGroupSlideRequest
1718 | }
1719 |
1720 | // If playing from the library on Mac, the presentationPath here will be the full
1721 | // path to the document on the user's computer ('/Users/JohnDoe/.../filename.pro6'),
1722 | // which differs from objData.presentationPath returned by an action like
1723 | // 'presentationTriggerIndex' or 'presentationSlideIndex' which only contains the
1724 | // filename.
1725 | // These two values need to match or we'll re-request 'presentationCurrent' on every
1726 | // slide change. Strip off everything before and including the final '/'.
1727 | // TODO: revisit this logic for Pro7 (consider updating to suit Pro7 instead of Pro6)
1728 | objData.presentationPath = objData.presentationPath.replace(/.*\//, '')
1729 |
1730 | // Remove file extension (.pro or .pro6) to make module var friendly.
1731 | var presentationName = objPresentation.presentationName.replace(/\.pro.?$/i, '')
1732 | this.updateVariable('presentation_name', presentationName)
1733 |
1734 | // '.presentationPath' and '.presentation.presentationCurrentLocation' look to be
1735 | // the same on Pro6 Mac, but '.presentation.presentationCurrentLocation' is the
1736 | // wrong value on Pro6 PC (tested 6.1.6.2). Use '.presentationPath' instead.
1737 | this.currentState.internal.presentationPath = objData.presentationPath
1738 | this.updateVariable('current_presentation_path', objData.presentationPath)
1739 |
1740 | // Get the total number of slides in this presentation
1741 | var totalSlides = 0
1742 | for (var i = 0; i < objPresentation.presentationSlideGroups.length; i++) {
1743 | totalSlides += objPresentation.presentationSlideGroups[i].groupSlides.length
1744 | }
1745 |
1746 | this.updateVariable('total_slides', totalSlides)
1747 |
1748 | // Update remaining_slides (as total_slides has probably just changed)
1749 | this.updateVariable(
1750 | 'remaining_slides',
1751 | this.currentState.dynamicVariables['total_slides'] - this.currentState.dynamicVariables['current_slide']
1752 | )
1753 |
1754 | this.log('debug', 'presentationCurrent: ' + presentationName)
1755 | break
1756 |
1757 | case 'clockRequest':
1758 | // Using clockRequest for a workaround when clockCurrentTimes action is never recieved from some versions of Pro7 on MacOS
1759 | // The workaround is to manually poll with clockRequests - when a clockRequest response is recieved, just pre-load objData.clockTimes with the times array from the clockRequest clockInfo and keep using the normal processing below that processes the clockTimes array!
1760 | objData.clockTimes = objData.clockInfo.map((x) => x.clockTime)
1761 | case 'clockCurrentTime':
1762 | case 'clockCurrentTimes':
1763 | var objClockTimes = objData.clockTimes
1764 |
1765 | this.currentState.internal.timeOfLastClockUpdate = Date.now() // Keep track since last 'clockCurrentTimes' message was received - there should be one every second.
1766 | this.updateVariable('time_since_last_clock_update', 0)
1767 |
1768 | // Update dyn var for watched clock/timer
1769 | if (this.config.indexOfClockToWatch >= 0 && this.config.indexOfClockToWatch < objData.clockTimes.length) {
1770 | this.updateVariable('watched_clock_current_time', objData.clockTimes[this.config.indexOfClockToWatch])
1771 | }
1772 |
1773 | // Update complete list of dyn vars for all clocks/timers (two for each clock - one with and one without hours)
1774 | var updateModuleVars = false
1775 | for (let clockIndex = 0; clockIndex < objClockTimes.length; clockIndex++) {
1776 | // Update (add) dynamic clock variable
1777 | this.currentState.dynamicVariables['pro7_clock_' + clockIndex] = this.formatClockTime(
1778 | objClockTimes[clockIndex]
1779 | )
1780 | this.updateVariable(
1781 | 'pro7_clock_' + clockIndex,
1782 | this.currentState.dynamicVariables['pro7_clock_' + clockIndex]
1783 | )
1784 | // If we don't already have this dynamic var defined then add a definition for it (we'll update Companion once loop is done)
1785 | var varDef = { name: 'Pro7 Clock ' + clockIndex, variableId: 'pro7_clock_' + clockIndex }
1786 | if (!this.currentState.dynamicVariablesDefs.some(({ variableId }) => variableId === varDef.variableId)) {
1787 | this.currentState.dynamicVariablesDefs.push(varDef)
1788 | updateModuleVars = true
1789 | }
1790 |
1791 | // Update (add) dynamic clock variable (hourless)
1792 | this.currentState.dynamicVariables['pro7_clock_' + clockIndex + '_hourless'] = this.formatClockTime(
1793 | objClockTimes[clockIndex],
1794 | false
1795 | )
1796 | this.updateVariable(
1797 | 'pro7_clock_' + clockIndex + '_hourless',
1798 | this.currentState.dynamicVariables['pro7_clock_' + clockIndex + '_hourless']
1799 | )
1800 | // If we don't already have this dynamic var defined then add a definition for it (we'll update Companion once loop is done)
1801 | var varDef = {
1802 | name: 'Pro7 Clock ' + clockIndex + ' Hourless',
1803 | variableId: 'pro7_clock_' + clockIndex + '_hourless',
1804 | }
1805 | if (!this.currentState.dynamicVariablesDefs.some(({ variableId }) => variableId === varDef.variableId)) {
1806 | this.currentState.dynamicVariablesDefs.push(varDef)
1807 | updateModuleVars = true
1808 | }
1809 |
1810 | // Update (add) dynamic clock variable (totalseconds)
1811 | this.currentState.dynamicVariables['pro7_clock_' + clockIndex + '_totalseconds'] = this.convertToTotalSeconds(
1812 | objClockTimes[clockIndex]
1813 | )
1814 | this.updateVariable(
1815 | 'pro7_clock_' + clockIndex + '_totalseconds',
1816 | this.currentState.dynamicVariables['pro7_clock_' + clockIndex + '_totalseconds']
1817 | )
1818 | // If we don't already have this dynamic var defined then add a definition for it (we'll update Companion once loop is done)
1819 | var varDef = {
1820 | name: 'Pro7 Clock ' + clockIndex + ' Total Seconds',
1821 | variableId: 'pro7_clock_' + clockIndex + '_totalseconds',
1822 | }
1823 | if (!this.currentState.dynamicVariablesDefs.some(({ variableId }) => variableId === varDef.variableId)) {
1824 | this.currentState.dynamicVariablesDefs.push(varDef)
1825 | updateModuleVars = true
1826 | }
1827 | }
1828 |
1829 | // Tell Companion about any new module vars for clocks that were added (so they become visible in WebUI etc)
1830 | if (updateModuleVars) {
1831 | this.setVariableDefinitions(this.currentState.dynamicVariablesDefs)
1832 | }
1833 |
1834 | break
1835 |
1836 | case 'stageDisplaySetIndex': // Companion User (or someone else) has set a new Stage Display Layout in Pro6 (Time to refresh stage display dynamic variables)
1837 | if (this.currentState.internal.proMajorVersion === 6) {
1838 | var stageDisplayIndex = objData.stageDisplayIndex
1839 | this.currentState.internal.stageDisplayIndex = parseInt(stageDisplayIndex, 10)
1840 | this.updateVariable('current_stage_display_index', stageDisplayIndex)
1841 | this.getStageDisplaysInfo()
1842 | this.checkFeedbacks('stagedisplay_active')
1843 | }
1844 | break
1845 |
1846 | case 'stageDisplaySets':
1847 | if (this.currentState.internal.proMajorVersion === 6) {
1848 | // ******* PRO6 *********
1849 | // Handle Pro6 Stage Display Info...
1850 | // The Pro6 response from sending stageDisplaySets is a reply that includes an array of stageDisplaySets, and an index "stageDisplayIndex" that is set to the index of the currently selected layout for the single stage display in Pro6
1851 | var stageDisplaySets = objData.stageDisplaySets
1852 | var stageDisplayIndex = objData.stageDisplayIndex
1853 | this.currentState.internal.stageDisplayIndex = parseInt(stageDisplayIndex, 10)
1854 | this.updateVariable('current_stage_display_index', stageDisplayIndex)
1855 | this.updateVariable('current_stage_display_name', stageDisplaySets[parseInt(stageDisplayIndex, 10)])
1856 | this.checkFeedbacks('stagedisplay_active')
1857 | } else if (this.currentState.internal.proMajorVersion === 7) {
1858 | // ******* PRO7 *********
1859 | // Handle Pro7 Stage Display Info...
1860 | // The Pro7 response from sending stageDisplaySets is a reply that includes TWO arrays/lists
1861 | // The list "stageLayouts" includes the name and id of each stagelayout defined in Pro7
1862 | // The list "stageScreens: includes name, id and id of the selected stageLayout for all stage output screens defined in Pro7
1863 | var watchScreen_StageLayoutSelectedLayoutUUID = ''
1864 |
1865 | // Refresh list of all stageLayouts (name and id)
1866 | if (objData.hasOwnProperty('stageLayouts')) {
1867 | // Empty old list of stageLayouts
1868 | this.currentState.internal.pro7StageLayouts = []
1869 |
1870 | // Refresh list from new data
1871 | objData.stageLayouts.forEach((stageLayout) => {
1872 | this.currentState.internal.pro7StageLayouts.push({
1873 | id: stageLayout['stageLayoutUUID'],
1874 | label: stageLayout['stageLayoutName'],
1875 | })
1876 | })
1877 | }
1878 |
1879 | // Refresh list of stage OUTPUT SCREENS
1880 | // Update the records of screen names (and selected layout UUID)
1881 | // Updates dynamic module vars for stage layouts
1882 | // Also record UUID of the current_pro7_stage_layout_name for selected watched screen
1883 | if (objData.hasOwnProperty('stageScreens')) {
1884 | // Empty old list of pro7StageScreens
1885 | this.currentState.internal.pro7StageScreens = []
1886 |
1887 | // Refresh list from new data
1888 | var updateModuleVars = false
1889 | objData.stageScreens.forEach((stageScreen) => {
1890 | var stageScreenName = stageScreen['stageScreenName']
1891 | var stageScreenUUID = stageScreen['stageScreenUUID']
1892 | var stageLayoutSelectedLayoutUUID = stageScreen['stageLayoutSelectedLayoutUUID']
1893 | this.currentState.internal.pro7StageScreens.push({
1894 | id: stageScreenUUID,
1895 | label: stageScreenName,
1896 | layoutUUID: stageLayoutSelectedLayoutUUID,
1897 | })
1898 |
1899 | // Update dynamic module var with current layout name for this pro7 stage screen
1900 | try {
1901 | this.currentState.dynamicVariables[stageScreenName + '_pro7_stagelayoutname'] =
1902 | this.currentState.internal.pro7StageLayouts.find(
1903 | (pro7StageLayout) => pro7StageLayout.id === stageLayoutSelectedLayoutUUID
1904 | ).label
1905 | this.updateVariable(
1906 | stageScreenName + '_pro7_stagelayoutname',
1907 | this.currentState.dynamicVariables[stageScreenName + '_pro7_stagelayoutname']
1908 | )
1909 | // If we don't already have this dynamic var defined then add a definition for it (we'll update Companion once loop is done)
1910 | var varDef = {
1911 | name: stageScreenName + '_pro7_stagelayoutname',
1912 | variableId: stageScreenName + '_pro7_stagelayoutname',
1913 | }
1914 | if (
1915 | !this.currentState.dynamicVariablesDefs.some(({ variableId }) => variableId === varDef.variableId)
1916 | ) {
1917 | this.currentState.dynamicVariablesDefs.push(varDef)
1918 | updateModuleVars = true
1919 | }
1920 | } catch (e) {
1921 | this.log(
1922 | 'warn',
1923 | 'Error finding/updating layout name for ' + stageScreenName + '_pro7_stagelayoutname. ' + e.message
1924 | )
1925 | }
1926 |
1927 | // Capture the UUID of the current_pro7_stage_layout_name for selected watched screen
1928 | if (stageScreenUUID === this.config.GUIDOfStageDisplayScreenToWatch) {
1929 | watchScreen_StageLayoutSelectedLayoutUUID = stageLayoutSelectedLayoutUUID
1930 | this.currentState.internal.stageDisplayIndex = this.currentState.internal.pro7StageLayouts
1931 | .map((x) => {
1932 | return x.id
1933 | })
1934 | .indexOf(watchScreen_StageLayoutSelectedLayoutUUID)
1935 | this.checkFeedbacks('stagedisplay_active')
1936 | }
1937 | })
1938 |
1939 | // Tell Companion about any new module vars for stage screens that were added (so they become visible in WebUI etc)
1940 | if (updateModuleVars) {
1941 | this.setVariableDefinitions(this.currentState.dynamicVariablesDefs)
1942 | }
1943 | }
1944 |
1945 | // Update current_pro7_stage_layout_name
1946 | if (objData.hasOwnProperty('stageLayouts')) {
1947 | objData.stageLayouts.forEach((stageLayout) => {
1948 | if (stageLayout['stageLayoutUUID'] === watchScreen_StageLayoutSelectedLayoutUUID) {
1949 | this.updateVariable('current_pro7_stage_layout_name', stageLayout['stageLayoutName'])
1950 | }
1951 | })
1952 | }
1953 |
1954 | this.checkFeedbacks('pro7_stagelayout_active')
1955 |
1956 | this.log('info', 'Got Pro7 Stage Display Sets')
1957 | this.setActionDefinitions(GetActions(this))
1958 |
1959 | this.init_feedbacks() // Update dropdown lists for pro7 stage layout feedback.
1960 | }
1961 | break
1962 |
1963 | case 'looksRequest': // Response from sending looksRequest
1964 | if (objData.hasOwnProperty('looks')) {
1965 | var currentLooks = []
1966 | objData.looks.forEach((look) => {
1967 | var lookName = look['lookName']
1968 | var lookID = look['lookID']
1969 | currentLooks.push({ id: lookID, label: lookName })
1970 | })
1971 |
1972 | // Update dyn var for current look name
1973 | this.updateVariable('current_pro7_look_name', objData.activeLook.lookName)
1974 | // Keep track of ID for current look
1975 | this.currentState.internal.current_pro7_look_id = objData.activeLook.lookID
1976 |
1977 | this.log('debug', 'Got Pro7 Looks List, Active Look = ' + objData.activeLook.lookName)
1978 |
1979 | // Compare currentLooks with this.currentState.internal.pro7Looks If it is different then update list and UI
1980 | var looksChanged = false
1981 | if (this.currentState.internal.pro7Looks.length == currentLooks.length) {
1982 | for (var index = 0; index < this.currentState.internal.pro7Looks.length; index++) {
1983 | var internalLook = this.currentState.internal.pro7Looks[index]
1984 | if (
1985 | internalLook.lookName != currentLooks[index].lookName ||
1986 | internalLook.lookID != currentLooks[index].lookID
1987 | ) {
1988 | looksChanged = true
1989 | break
1990 | }
1991 | }
1992 | } else {
1993 | looksChanged = true
1994 | }
1995 |
1996 | if (looksChanged) {
1997 | this.log('debug', 'Looks changed. Updated internal list ')
1998 | this.currentState.internal.pro7Looks = currentLooks.slice() // Update .internal.pro7Looks to same as currentLooks
1999 | this.setActionDefinitions(GetActions(this))
2000 | this.init_feedbacks() // Update dropdown lists for look feedback.
2001 | }
2002 |
2003 | this.checkFeedbacks('active_look')
2004 | }
2005 | break
2006 |
2007 | case 'macrosRequest': // Response from sending macrosRequest
2008 | if (objData.hasOwnProperty('macros')) {
2009 | this.currentState.internal.pro7Macros = []
2010 | objData.macros.forEach((look) => {
2011 | var macroName = look['macroName']
2012 | var macroID = look['macroID']
2013 | this.currentState.internal.pro7Macros.push({ id: macroID, label: macroName })
2014 | })
2015 |
2016 | this.log('info', 'Got Pro7 Macros List')
2017 | this.setActionDefinitions(GetActions(this))
2018 | }
2019 | break
2020 |
2021 | case 'playlistRequestAll':
2022 | this.log('debug', 'Received All PlayLists')
2023 | // Check if there is an awaiting SlideByLabelRequest...
2024 | // ..If so, cAll recursivelyScanPlaylistsObjToTriggerSlideByLabel() to find presentation path
2025 | // Update this.currentState.internal.awaitingSlideByLabelRequest with the matching path and then send a presentationRequest.
2026 | // presentationRequest will return a presetationCurrent response, and because there is an waiting SlideByLabelRequest, the response will be searched for matching slide so the request can finally be completed.
2027 | var awaitingSlideByLabelRequest = this.currentState.internal.awaitingSlideByLabelRequest
2028 | if (
2029 | awaitingSlideByLabelRequest.hasOwnProperty('playlistName') &&
2030 | awaitingSlideByLabelRequest.hasOwnProperty('presentationName') &&
2031 | awaitingSlideByLabelRequest.hasOwnProperty('slideLabel')
2032 | ) {
2033 | this.log(
2034 | 'debug',
2035 | 'Scanning playlists for: [' +
2036 | awaitingSlideByLabelRequest.playlistName +
2037 | ', ' +
2038 | awaitingSlideByLabelRequest.presentationName +
2039 | ', ' +
2040 | awaitingSlideByLabelRequest.slideLabel +
2041 | ']'
2042 | )
2043 |
2044 | // Prepare for recursive search (using this.currentState.internal.matchingPlaylistItemFound as a flag between recursive calls to recursivelyScanPlaylistsObjToTriggerSlideByLabel)
2045 | try {
2046 | this.currentState.internal.matchingPlaylistItemFound = false
2047 | this.recursivelyScanPlaylistsObjToTriggerSlideByLabel(
2048 | JSON.parse(message),
2049 | awaitingSlideByLabelRequest.playlistName,
2050 | awaitingSlideByLabelRequest.presentationName,
2051 | awaitingSlideByLabelRequest.slideLabel
2052 | )
2053 | } catch (err) {
2054 | this.log('debug', err.message)
2055 | }
2056 | }
2057 | break
2058 | }
2059 |
2060 | if (
2061 | objData.presentationPath !== undefined &&
2062 | objData.presentationPath !== this.currentState.internal.presentationPath
2063 | ) {
2064 | // The presentationPath has changed. Update the path and request the information.
2065 | this.getProPresenterState()
2066 | }
2067 | }
2068 |
2069 | /**
2070 | * Received a message from Follower ProPresenter.
2071 | */
2072 | onFollowerWebSocketMessage = (message) => {
2073 | var objData
2074 | // Try to parse websocket payload as JSON...
2075 | try {
2076 | objData = JSON.parse(message)
2077 | } catch (err) {
2078 | this.log('warn', err.message)
2079 | return
2080 | }
2081 |
2082 | switch (objData.action) {
2083 | case 'authenticate':
2084 | if (objData.authenticated === 1) {
2085 | // Autodetect if Major version of ProPresenter is version 7
2086 | // Only Pro7 includes .majorVersion and .minorVersion properties.
2087 | // .majorVersion will be set to = "7" from Pro7 (Pro6 does not include these at all)
2088 | if (objData.hasOwnProperty('majorVersion')) {
2089 | this.log('info', 'Authenticated to Follower ProPresenter (Version: ' + objData.majorVersion + ')')
2090 | }
2091 |
2092 | this.currentState.internal.wsFollowerConnected = true
2093 |
2094 | this.checkFeedbacks('propresenter_follower_connected')
2095 | } else {
2096 | this.updateStatus(InstanceStatus.UnknownWarning)
2097 | this.log('warn', 'Failed to authenticate to Follower ProPresenter' + objData.error)
2098 | this.disconnectFromFollowerProPresenter()
2099 |
2100 | // No point in trying to connect again. The user must either re-enable this
2101 | // module or re-save the config changes to make another attempt.
2102 | this.stopFollowerConnectionTimer()
2103 |
2104 | this.currentState.internal.wsFollowerConnected = false
2105 | }
2106 | break
2107 |
2108 | case 'presentationTriggerIndex':
2109 | case 'presentationSlideIndex':
2110 | // Update the current slide index.
2111 | var slideIndex = parseInt(objData.slideIndex, 10)
2112 | this.log('debug', 'Follower presentationSlideIndex: ' + slideIndex)
2113 | break
2114 |
2115 | case 'presentationCurrent':
2116 | var objPresentation = objData.presentation
2117 |
2118 | // Pro6 PC's 'presentationName' contains the raw file extension '.pro6'. Remove it.
2119 | var presentationName = objPresentation.presentationName.replace(/\.pro6$/i, '')
2120 | this.log('info', 'Follower presentationCurrent: ' + presentationName)
2121 | break
2122 | }
2123 | }
2124 |
2125 | /**
2126 | * Received a stage display message from ProPresenter.
2127 | */
2128 | onSDWebSocketMessage = (message) => {
2129 | var objData
2130 | // Try to parse websocket payload as JSON...
2131 | try {
2132 | objData = JSON.parse(message)
2133 | } catch (err) {
2134 | this.log('warn', err.message)
2135 | return
2136 | }
2137 |
2138 | switch (objData.acn) {
2139 | case 'ath':
2140 | if (objData.ath === true) {
2141 | this.currentState.internal.wsSDConnected = true
2142 | // Successfully authenticated.
2143 | this.setSDConnectionVariable('Connected', true)
2144 | this.updateStatus(InstanceStatus.Ok)
2145 | } else {
2146 | // Bad password
2147 | if (this.config.use_sd === 'yes') {
2148 | this.updateStatus(InstanceStatus.UnknownWarning, 'OK, But Stage Display failed auth')
2149 | this.log('warn', 'Stage Display auth error: ' + String(objData.err))
2150 | }
2151 | this.stopSDConnectionTimer()
2152 | }
2153 | break
2154 |
2155 | case 'vid':
2156 | if (objData.hasOwnProperty('txt')) {
2157 | // Record new video countdown timer value in dynamic var
2158 | this.updateVariable('video_countdown_timer', objData.txt)
2159 | // Convert video countdown timer to hourless
2160 | this.updateVariable('video_countdown_timer_hourless', this.formatClockTime(objData.txt, false))
2161 | // Convert video countdown timer to total seconds
2162 | this.updateVariable('video_countdown_timer_totalseconds', this.convertToTotalSeconds(objData.txt))
2163 | }
2164 | break
2165 | }
2166 | }
2167 |
2168 | /**
2169 | * Requests the current state from ProPresenter.
2170 | */
2171 | getProPresenterState = (refreshCurrentPresentation = false) => {
2172 | if (this.currentState.internal.wsConnected === false) {
2173 | return
2174 | }
2175 |
2176 | if (refreshCurrentPresentation) {
2177 | this.log('debug', 'presentationCurrent')
2178 | // Force send presentationCurrent with presentationSlideQuality = '0' (string) (25-Jan-2022 This was the default way "always". It Performs well for Pro7 and Pro6 on MacOS - very slow for Pro6/7 on Windows)
2179 | this.socket.send(
2180 | JSON.stringify({
2181 | action: 'presentationCurrent',
2182 | presentationSlideQuality: '0', // Setting to 0 stops Pro from including the slide preview image data (which is a lot of data) - no need to get slide preview images since we are not using them!
2183 | })
2184 | )
2185 | } else {
2186 | if (this.config.sendPresentationCurrentMsgs !== 'no') {
2187 | // User can optionally block sending these msgs to ProPresenter (as it can cause performance issues with ProPresenter on Windows)
2188 | if (this.config.typeOfPresentationRequest == 'auto') {
2189 | // Decide which type of request to get current presentation info
2190 | // Just send presentationCurrent with presentationSlideQuality = '0' (string) (25-Jan-2022 This was the default way "always". It Performs well for Pro7 and Pro6 on MacOS - very slow for Pro6/7 on Windows)
2191 | this.socket.send(
2192 | JSON.stringify({
2193 | action: 'presentationCurrent',
2194 | presentationSlideQuality: '0', // Setting to 0 stops Pro from including the slide preview image data (which is a lot of data) - no need to get slide preview images since we are not using them!
2195 | })
2196 | )
2197 | } else {
2198 | // Send presentationRequest with presentationSlideQuality = 0 (int) (At time of adding this option, this was only method that performs well for Pro7.8+ on Mac/Win and Pro6 on Mac)
2199 | this.socket.send(
2200 | JSON.stringify({
2201 | action: 'presentationRequest',
2202 | presentationSlideQuality: 0, // Setting to 0 stops Pro from including the slide preview image data (which is a lot of data) - no need to get slide preview images since we are not using them!
2203 | presentationPath: this.currentState.dynamicVariables['current_presentation_path'],
2204 | })
2205 | )
2206 | }
2207 | }
2208 | }
2209 |
2210 | if (this.currentState.dynamicVariables.current_slide === 'N/A') {
2211 | // The currentSlide will be empty when the module first loads. Request it.
2212 | this.socket.send(
2213 | JSON.stringify({
2214 | action: 'presentationSlideIndex',
2215 | })
2216 | )
2217 | }
2218 | }
2219 |
2220 | /*
2221 | * Requests the list of configured stage displays (includes names)
2222 | */
2223 | getStageDisplaysInfo = () => {
2224 | if (this.currentState.internal.wsConnected === false) {
2225 | return
2226 | }
2227 |
2228 | this.socket.send(
2229 | JSON.stringify({
2230 | action: 'stageDisplaySets',
2231 | })
2232 | )
2233 | }
2234 |
2235 | /*
2236 | * Request Looks List
2237 | */
2238 | getLooksList = () => {
2239 | if (this.currentState.internal.wsConnected === false) {
2240 | return
2241 | }
2242 |
2243 | this.socket.send(
2244 | JSON.stringify({
2245 | action: 'looksRequest',
2246 | })
2247 | )
2248 | }
2249 |
2250 | /*
2251 | * Request Macros List
2252 | */
2253 | getMacrosList = () => {
2254 | if (this.currentState.internal.wsConnected === false) {
2255 | return
2256 | }
2257 |
2258 | this.socket.send(
2259 | JSON.stringify({
2260 | action: 'macrosRequest',
2261 | })
2262 | )
2263 | }
2264 |
2265 | /*
2266 | * Format Time string
2267 | */
2268 | formatClockTime = (clockTimeString, includeHours = true) => {
2269 | // Record if time is negative
2270 | var timeIsNegative = false
2271 | if (clockTimeString.length > 0) {
2272 | timeIsNegative = clockTimeString.charAt(0) == '-'
2273 | }
2274 |
2275 | // Remove decimal (sub-seconds) and save in formattedClockTimeString
2276 | var formattedClockTimeString = ''
2277 | if (clockTimeString.indexOf('.') > 0) {
2278 | formattedClockTimeString = clockTimeString.slice(0, clockTimeString.indexOf('.'))
2279 | } else {
2280 | formattedClockTimeString = clockTimeString
2281 | }
2282 |
2283 | var hours = ''
2284 | var minutes = ''
2285 | var seconds = ''
2286 | var timeParts = formattedClockTimeString.split(':')
2287 | if (timeParts.length == 3) {
2288 | hours = timeParts.shift()
2289 | }
2290 | if (timeParts.length == 2) {
2291 | minutes = timeParts.shift()
2292 | }
2293 | if (timeParts.length == 1) {
2294 | seconds = timeParts.shift()
2295 | }
2296 |
2297 | if (includeHours) {
2298 | return hours + ':' + minutes + ':' + seconds
2299 | } else {
2300 | // If time was negative the negative sign will in the hours component that is not returned here. Add a negtive sign to the minutes component.
2301 | if (timeIsNegative) {
2302 | minutes = '-' + minutes
2303 | }
2304 | return minutes + ':' + seconds
2305 | }
2306 | }
2307 |
2308 | /*
2309 | * Conver Time string to total seconds
2310 | */
2311 | convertToTotalSeconds = (clockTimeString) => {
2312 | var totalSeconds = 0
2313 |
2314 | // Record if time is negative
2315 | var timeIsNegative = false
2316 | if (clockTimeString.length > 0) {
2317 | timeIsNegative = clockTimeString.charAt(0) == '-'
2318 | }
2319 |
2320 | // Remove any decimal (sub-seconds) and save in formattedClockTimeString
2321 | var formattedClockTimeString = ''
2322 | if (clockTimeString.indexOf('.') > 0) {
2323 | formattedClockTimeString = clockTimeString.slice(0, clockTimeString.indexOf('.'))
2324 | } else {
2325 | formattedClockTimeString = clockTimeString
2326 | }
2327 |
2328 | // If time is negative remove leading - prefix from string
2329 | if (timeIsNegative) {
2330 | formattedClockTimeString = formattedClockTimeString.slice(1)
2331 | }
2332 |
2333 | var hours = 0
2334 | var minutes = 0
2335 | var seconds = 0
2336 | var timeParts = formattedClockTimeString.split(':')
2337 | if (timeParts.length == 3) {
2338 | hours = parseInt(timeParts.shift())
2339 | if (!isNaN(hours)) {
2340 | totalSeconds = totalSeconds + 3600 * hours
2341 | }
2342 | }
2343 | if (timeParts.length == 2) {
2344 | minutes = parseInt(timeParts.shift())
2345 | if (!isNaN(minutes)) {
2346 | totalSeconds = totalSeconds + 60 * minutes
2347 | }
2348 | }
2349 | if (timeParts.length == 1) {
2350 | seconds = parseInt(timeParts.shift())
2351 | if (!isNaN(seconds)) {
2352 | totalSeconds = totalSeconds + seconds
2353 | }
2354 | }
2355 |
2356 | if (timeIsNegative) {
2357 | totalSeconds = totalSeconds * -1
2358 | }
2359 |
2360 | return totalSeconds
2361 | }
2362 |
2363 | /*
2364 | * Recursively Scan PlaylistsObject to find first presentation that matches given name, in the first playlist that matches given name.
2365 | * Calls presentationRequest (whose response handler will complete the request)
2366 | */
2367 |
2368 | recursivelyScanPlaylistsObjToTriggerSlideByLabel = (playlistObj, playlistName, presentationName, slideLabel) => {
2369 | Object.keys(playlistObj).forEach((key) => {
2370 | if (this.currentState.internal.matchingPlaylistItemFound) {
2371 | return
2372 | }
2373 | if (
2374 | playlistObj.hasOwnProperty('playlistName') &&
2375 | playlistObj.hasOwnProperty('playlist') &&
2376 | playlistObj.playlistName == playlistName
2377 | ) {
2378 | var matchingPlaylistItem = playlistObj.playlist.find(
2379 | (playlistItem) =>
2380 | playlistItem.hasOwnProperty('playlistItemName') &&
2381 | this.matchRuleShort(playlistItem.playlistItemName, presentationName)
2382 | ) // matchRuleShort allows use of wildcard * anywhere in presentationName parameter
2383 | if (matchingPlaylistItem !== undefined) {
2384 | this.log('debug', 'Found match: ' + JSON.stringify(matchingPlaylistItem))
2385 | this.currentState.internal.matchingPlaylistItemFound = true
2386 | // Update this.currentState.internal.awaitingSlideByLabelRequest with the matching path (so response to presentationRequest can check)
2387 | this.currentState.internal.awaitingSlideByLabelRequest.presentationPath =
2388 | matchingPlaylistItem.playlistItemLocation
2389 | // send presentationRequest
2390 | const cmd = {
2391 | action: 'presentationRequest',
2392 | presentationPath: this.currentState.internal.awaitingSlideByLabelRequest.presentationPath,
2393 | presentationSlideQuality: 0,
2394 | }
2395 | try {
2396 | if (this.socket.readyState == 1 /*OPEN*/) {
2397 | this.socket.send(JSON.stringify(cmd))
2398 | }
2399 | } catch (e) {
2400 | this.log('debug', 'Socket Send Error: ' + e.message)
2401 | }
2402 | }
2403 | }
2404 |
2405 | if (typeof playlistObj[key] === 'object' && playlistObj[key] !== null) {
2406 | this.recursivelyScanPlaylistsObjToTriggerSlideByLabel(
2407 | playlistObj[key],
2408 | playlistName,
2409 | presentationName,
2410 | slideLabel
2411 | )
2412 | }
2413 | })
2414 | }
2415 |
2416 | // Thanks to: https://stackoverflow.com/questions/26246601/wildcard-string-comparison-in-javascript/32402438#32402438
2417 | matchRuleShort = (str, rule) => {
2418 | var escapeRegex = (str) => str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1')
2419 | return new RegExp('^' + rule.split('*').map(escapeRegex).join('.*') + '$').test(str)
2420 | }
2421 | }
2422 | runEntrypoint(ProPresenterInstance, [])
2423 |
--------------------------------------------------------------------------------