├── README.md ├── audio-heartbeat.md ├── audio-heartbeat.vb ├── audio-sidechain.md ├── audio-sidechain.vb ├── auto-pre-mix.md ├── auto-pre-mix.vb ├── clone-input.md ├── clone-input.vb ├── event-reconfiguration.gtzip ├── event-reconfiguration.md ├── event-reconfiguration.vb ├── event-reconfiguration.xlsx ├── event-reconfiguration.xml ├── event-title-control.gtzip ├── event-title-control.md ├── event-title-control.vb ├── input-bridge.md ├── input-bridge.vb ├── input-mirror.md ├── input-mirror.vb ├── multiview-overlay.md ├── multiview-overlay.vb ├── ndi-studio-monitor.md ├── ndi-studio-monitor.vb ├── recording-log.md ├── recording-log.vb ├── remoteshowcontrol-loop.md ├── remoteshowcontrol-loop.vb ├── remoteshowcontrol-once.md ├── remoteshowcontrol-once.vb ├── smooth-pan-zoom.md ├── smooth-pan-zoom.vb ├── vmix-scripts.png └── vmix-scripts.psd /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | vMix Scripts 5 | ============ 6 | 7 | About 8 | ----- 9 | 10 | This is a collection of 11 | [VB.NET](https://en.wikipedia.org/wiki/Visual_Basic_.NET) 2.0 scripts 12 | for automating certain tasks and extending the functionality in the 13 | video/audio stream mixing software [vMix](https://www.vmix.com/) (4K and Pro editions only). 14 | All scripts were created in the professional context of a company 15 | filmstudio and its live event productions. The individual scripts are: 16 | 17 | - [audio-sidechain.vb](audio-sidechain.vb):
18 | [**Audio Sidechain Compression**](audio-sidechain.md)
19 | Allow audio output volumes to be automatically and temporarily 20 | reduced, based on audio input volumes (when temporarily above a 21 | threshold) -- similar to an audio side-chain compression. 22 |
23 | Use Cases: Microphone Ducking, Translator Voice-Over 24 | 25 | - [audio-heartbeat.vb](audio-heartbeat.vb):
26 | [**Detect Unexpected Silence**](audio-heartbeat.md)
27 | Notify operator in case unexpected silence, i.e., audio below a 28 | certain threshold on the master bus, is detected during streaming 29 | and/or recording. 30 |
31 | Use Cases: Playout Operator Hints, Playout Scene Switching 32 | 33 | - [auto-pre-mix.vb](auto-pre-mix.vb):
34 | [**Automatically Pre-Mixing Inputs**](auto-pre-mix.md)
35 | Allow one to auto-pre-mix (aka pre-render or flattening) source 36 | inputs with the help of two intermediate Mix-type input(s) in order to 37 | further embed the result onto a layer of a target input. 38 |
39 | Use Cases: Layer Re-Position/Re-Cropping 40 | 41 | - [input-bridge.vb](input-bridge.vb):
42 | [**Bridge Inputs between vMix instances**](input-bridge.md)
43 | Allow one to bridge/tunnel an arbitrary number of inputs between two 44 | vMix instances with the help of two NDI streams in 45 | order to perform load offloading between two vMix instances. 46 | (See the corresponding [demonstration video](https://youtu.be/Y6MHAtpMYG8) for details) 47 |
48 | Use Cases: Separated Ingest/Mixing 49 | 50 | - [input-mirror.vb](input-mirror.vb):
51 | [**Mirror Input Selection on vMix Slave Instance**](input-mirror.md)
52 | Allow one to mirror the current input preview/program selection 53 | on vMix slave instances in order to closely follow the vMix master instance. 54 |
55 | Use Cases: Separated Playouts 56 | 57 | - [multiview-overlay.vb](multiview-overlay.vb):
58 | [**Update Custom Multiview Overlays**](multiview-overlay.md)
59 | Allow one to update the preview/program overlays of a custom multiview 60 | by selecting corresponding images in a "List" type input. 61 |
62 | Use Cases: Virtual PTZ Overview 63 | 64 | - [smooth-pan-zoom.vb](smooth-pan-zoom.vb):
65 | [**Smooth Virtual Pan/Zoom in Virtual Sets**](smooth-pan-zoom.md)
66 | Smoothly adjust the pan/zoom of an input, for a rough emulation of the 67 | vMix Virtual PTZ feature, which cannot be used on layered inputs like 68 | Virtual Sets. 69 |
70 | Use Cases: Virtual PTZ Adjustment 71 | 72 | - [event-reconfiguration.vb](event-reconfiguration.vb):
73 | [**Reconfiguration of Event NDI Inputs (and Lower-Third Titles)**](event-reconfiguration.md)
74 | Allow one to step forward/backward through (or to a particular row of) 75 | an Excel-based conference event configuration by re-configuring four 76 | reusable NDI input sources (for shared content, one moderator and 77 | two presenters). 78 |
79 | Use Cases: Conference Guest Ingest 80 | 81 | - [event-title-control.vb](event-title-control.vb):
82 | [**Control Layer-Embedded Titles**](event-title-control.md)
83 | Control the in/out transitioning of lower-third titles which are 84 | embedded layers of scene inputs (where vMix only performs `TransitionIn` 85 | and never a `TransitionOut`). 86 |
87 | Use Cases: Conference Guest Title Mangement 88 | 89 | - [clone-input.vb](clone-input.vb):
90 | [**Really Cloning an Arbitrary Input**](clone-input.md)
91 | Allow an arbitrary input (which has to be in the preview) to be 92 | really cloned/duplicated. 93 |
94 | Use Cases: Event Configuration 95 | 96 | - [recording-log.vb](recording-log.vb):
97 | [**Logging Recording States**](recording-log.md)
98 | Logs the start and stop states of Recording and MultiCorder and can 99 | add a special marking log entry for bookkeeping special points of 100 | interest during a recording. 101 |
102 | Use Cases: Point of Interest Tracking 103 | 104 | - [remoteshowcontrol-loop.vb](remoteshowcontrol-loop.vb):
105 | [**Continuously Control Irisdown RemoteShowControl**](remoteshowcontrol-loop.md)
106 | Automatically and continuously control a remote 107 | PowerPoint slide-deck with the help of its 108 | [Irisdown RemoteShowControl](https://www.irisdown.co.uk/rsc.html) plugin, 109 | based on vMix input name information. 110 |
111 | Use Cases: Automatic PowerPoint Slide Control 112 | 113 | - [remoteshowcontrol-once.vb](remoteshowcontrol-once.vb):
114 | [**Once Control Irisdown RemoteShowControl**](remoteshowcontrol-once.md)
115 | Once control a remote PowerPoint slide-deck with the help of its 116 | [Irisdown RemoteShowControl](https://www.irisdown.co.uk/rsc.html) 117 | plugin, based on vMix triggers or shortcuts. 118 |
119 | Use Cases: Manual PowerPoint Slide Control 120 | 121 | - [ndi-studio-monitor.vb](ndi-studio-monitor.vb):
122 | [**Reconfigure NewTek NDI Studio Monitor**](ndi-studio-monitor.md)
123 | Allow vMix to reconfigure the NDI source displayed in a (remote) NewTek 124 | NDI Studio Monitor instance. 125 |
126 | Use Cases: Manual NDI Tools Studio Monitor Source Control 127 | 128 | Installation 129 | ------------ 130 | 131 | 1. Clone this [repository](https://github.com/rse/vmix-scripts) 132 | or [download a ZIP archive](https://github.com/rse/vmix-scripts/archive/refs/heads/master.zip):
133 | `git clone https://github.com/rse/vmix-scripts`
134 | 135 | 2. Add the individual scripts to vMix with
136 | **Settings** → **Scripting** → **Add** → **Import** 137 | 138 | 3. For some of the scripts, do not forget to adjust their configuration section! 139 | 140 | Background 141 | ---------- 142 | 143 | All these vMix scripts where created in the professional context of a 144 | company filmstudio, where multiple vMix instances (connected through 145 | NDI) are used to drive the live event productions. The particularly used 146 | vMix instances and their job (partially driven by the scripts and manual 147 | Bitfocus Companion control informations) are: 148 | 149 | - **Ingest 1**: 2160p30 mode, 5x physical camera ingest, 5x chroma-keying, 5x game-engine based 150 | background overlaying (indirectly using content 1+2 from **Ingest 151 | 2**), 5x8 physical PTZ management, 5x8x7 virtual PTZ management, 5x7 152 | virtual PTZ emit (to **Mixing 1**). 153 |
154 | Rationale: 4K cameras have to be still chroma-keyed in 4K, flexible virtual PTZ management. 155 |
156 | Scripts: multiview-overlay, smooth-pan-zoom 157 | 158 | - **Ingest 2**: 1080p30 mode, content 1+2 ingest (from **Mixing 1**), 159 | content 1+2 emit (via virtual camera into the game engines inside **Ingest 1**). 160 |
161 | Rationale: content from Mixing has to be ingested back into game-engine instances. 162 |
163 | Scripts: none 164 | 165 | - **Mixing 1**: 1080p30 mode, 5x7 virtual PTZ ingest (from **Ingest 1**), 8x remote guests ingest, 1x slide ingest, 166 | 12x microphone ingest, 4x4 title overlays, content 1+2 emit (to **Ingest 2**), primary programm emit (to **Playout 1**). 167 |
168 | Rationale: primary scene mixing (usually with german audio) 169 |
170 | Scripts: input-bridge, audio-sidechain, event-reconfiguration, event-title-control, recording-log, 171 | remoteshowcontrol-once, remoteshowcontrol-loop, ndi-studio-monitor, clone-input, auto-pre-mix 172 | 173 | - **Mixing 2**: 1080p30 mode, primary program ingest (from **Mixing 1**), 2x real-time translator ingest, 174 | translator audio export/re-import, secondary programm emit (to **Playout 2**), studio multiview emit. 175 |
176 | Rationale: real-time translation (usually german to englisch audio), studio multiview production. 177 |
178 | Scripts: audio-sidechain 179 | 180 | - **Playout 1**: 1080p30 mode, primary program ingest (from **Mixing 1**), event program management, 181 | primary program broadcasting. 182 |
183 | Rationale: event program mangement of first stream (usually german audio) 184 |
185 | Scripts: input-mirror, audio-heartbeat 186 | 187 | - **Playout 2**: 1080p30 mode, secondary program ingest (from **Mixing 2**), event program management, 188 | secondary program broadcasting. 189 |
190 | Rationale: event program mangement of second stream (usually english audio) 191 |
192 | Scripts: none 193 | 194 | License 195 | ------- 196 | 197 | Copyright © 2022-2023 Dr. Ralf S. Engelschall (http://engelschall.com/) 198 | 199 | Permission is hereby granted, free of charge, to any person obtaining 200 | a copy of this software and associated documentation files (the 201 | "Software"), to deal in the Software without restriction, including 202 | without limitation the rights to use, copy, modify, merge, publish, 203 | distribute, sublicense, and/or sell copies of the Software, and to 204 | permit persons to whom the Software is furnished to do so, subject to 205 | the following conditions: 206 | 207 | The above copyright notice and this permission notice shall be included 208 | in all copies or substantial portions of the Software. 209 | 210 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 211 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 212 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 213 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 214 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 215 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 216 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 217 | 218 | -------------------------------------------------------------------------------- /audio-heartbeat.md: -------------------------------------------------------------------------------- 1 | 2 | [Audio-Heartbeat](audio-heartbeat.vb) 3 | ===================================== 4 | 5 | **Detect Unexpected Silence** 6 | 7 | Notify operator (and optionally switch inputs) in case unexpected 8 | silence, i.e., audio below a certain threshold on the master bus, is 9 | detected during streaming and/or recording. 10 | 11 | Problem 12 | ------- 13 | 14 | Sometimes microphones were forgotten to be enabled, the wrong 15 | microphones were enabled or the wrong microphones were given to a particular talent. 16 | 17 | Solution 18 | -------- 19 | 20 | In all problem cases, during streaming/recording the master bus audio 21 | volume drops below a certain threshold (e.g. -32 dB FS) for longer than 22 | a certain time (e.g. 5000 ms). If this situation is detected, the vMix 23 | operator is notified through regular system sounds (e.g. every 2000 ms) 24 | and optionally the program is temporarily switched to a special input. 25 | 26 | Usage 27 | ----- 28 | 29 | Just run the script `audio-heartbeat` in the background. 30 | 31 | -------------------------------------------------------------------------------- /audio-heartbeat.vb: -------------------------------------------------------------------------------- 1 | '-- 2 | '-- audio-heartbeat.vb -- vMix VB.NET script for detecting unexpected silence 3 | '-- Copyright (c) 2022-2023 Dr. Ralf S. Engelschall 4 | '-- Distributed under MIT license 5 | '-- 6 | '-- Language: VB.NET 2.0 (vMix 4K/Pro flavor) 7 | '-- Version: 0.9.2 (2023-06-30) 8 | '-- 9 | 10 | '-- ==== CONFIGURATION (please adjust) ==== 11 | 12 | dim heartbeatMonitorBus as string = "master" 'id of audio bus to monitor volume 13 | dim heartbeatMonitorInput as string = "" 'id of input to monitor volume 14 | dim heartbeatThresholdVolume as integer = -60 'threshold below which volume to react (dB FS) 15 | dim heartbeatThresholdTime as integer = 5000 'threshold after which time to react (ms) 16 | dim heartbeatWarningEvery as integer = 2000 'time between warning indicators (ms) 17 | dim heartbeatTimeSlice as integer = 10 'time interval between the script iterations (ms) 18 | dim inputOK as string = "STREAM1" 'optional input to switch to for OK situation 19 | dim inputWARNING as string = "SCREEN-FAILURE" 'optional input to switch to for warning situaton 20 | dim debug as boolean = false 'whether to output debug information to the console 21 | 22 | '-- ==== INTERNAL STATE ==== 23 | 24 | '-- internal state 25 | dim mode as string = "OK" 'current iteration mode 26 | dim timeAwaitBelowCount as integer = 0 'counter for time over the threshold 27 | dim timeAwaitOverCount as integer = 0 'counter for time below the threshold 28 | dim timeWarningCount as integer = 0 'counter for time of warning 29 | 30 | '-- pre-convert decibel to amplitude 31 | dim heartbeatThresholdAmp as double = 10 ^ (heartbeatThresholdVolume / 20) 32 | 33 | '-- prepare XML DOM tree 34 | dim cfg as new System.Xml.XmlDocument 35 | 36 | '-- use a fixed locale for parsing floating point numbers 37 | dim cultureInfo as System.Globalization.CultureInfo = System.Globalization.CultureInfo.CreateSpecificCulture("en-US") 38 | 39 | '-- enter endless iteration loop 40 | do while true 41 | '-- fetch current vMix API status 42 | dim xml as string = API.XML() 43 | cfg.LoadXml(xml) 44 | 45 | '-- determine whether we should operate at all (indicated by streaming/recording enabled) 46 | dim isStreaming as boolean = Convert.ToBoolean(cfg.SelectSingleNode("/vmix/streaming").InnerText) 47 | dim isRecording as boolean = Convert.ToBoolean(cfg.SelectSingleNode("/vmix/recording").InnerText) 48 | dim isMultiCording as boolean = Convert.ToBoolean(cfg.SelectSingleNode("/vmix/multiCorder").InnerText) 49 | if not (isStreaming or isRecording or isMultiCording) then 50 | continue do 51 | end if 52 | 53 | '-- determine input volume (in linear volume scale) 54 | dim meter1 as double = 0.0 55 | dim meter2 as double = 0.0 56 | if heartbeatMonitorBus <> "" and heartbeatMonitorInput = "" then 57 | meter1 = Convert.ToDouble(cfg.SelectSingleNode("/vmix/audio/bus" & heartbeatMonitorBus & "/@meterF1").Value, cultureInfo) 58 | meter2 = Convert.ToDouble(cfg.SelectSingleNode("/vmix/audio/bus" & heartbeatMonitorBus & "/@meterF2").Value, cultureInfo) 59 | elseif heartbeatMonitorBus = "" and heartbeatMonitorInput <> "" then 60 | meter1 = Convert.ToDouble(cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & heartbeatMonitorInput & "']/@meterF1").Value, cultureInfo) 61 | meter2 = Convert.ToDouble(cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & heartbeatMonitorInput & "']/@meterF2").Value, cultureInfo) 62 | end if 63 | if meter1 < meter2 then 64 | meter1 = meter2 65 | end if 66 | 67 | '-- track whether input volume is continuously over or below volume threshold 68 | if meter1 > heartbeatThresholdAmp then 69 | timeAwaitOverCount += 1 70 | timeAwaitBelowCount = 0 71 | else 72 | timeAwaitBelowCount += 1 73 | timeAwaitOverCount = 0 74 | end if 75 | 76 | '-- decide current operation mode 77 | dim modeNew as String = "OK" 78 | if timeAwaitBelowCount >= cint(heartbeatThresholdTime / heartbeatTimeSlice) then 79 | modeNew = "WARNING" 80 | end if 81 | if modeNew = "WARNING" and mode <> "WARNING" then 82 | '-- enforce an initial warning immediately 83 | timeWarningCount = cint(heartbeatWarningEvery / heartBeatTimeSlice) 84 | end if 85 | if mode <> modeNew then 86 | if debug then 87 | Console.WriteLine("audio-heartbeat: INFO: switching to mode: " & modeNew) 88 | end if 89 | dim inputInProgramNum as String = cfg.SelectSingleNode("/vmix/active").InnerText 90 | dim inputInProgram as String = cfg.SelectSingleNode("/vmix/inputs/input[@number = '" & inputInProgramNum & "']/@title").Value 91 | if mode = "OK" and modeNew = "WARNING" then 92 | '-- enter WARNING mode: optionally switch to WARNING input 93 | if inputOK <> "" and inputWARNING <> "" and inputInProgram = inputOK then 94 | API.Function("CutDirect", Input := inputWARNING) 95 | end if 96 | elseif mode = "WARNING" and modeNew = "OK" then 97 | '-- leave WARNING mode: optionally switch to regular input 98 | if inputOK <> "" and inputWARNING <> "" and inputInProgram <> inputOK then 99 | API.Function("CutDirect", Input := inputOK) 100 | end if 101 | end if 102 | mode = modeNew 103 | end if 104 | 105 | '-- warn operator 106 | if mode = "WARNING" then 107 | timeWarningCount += 1 108 | if timeWarningCount >= cint(heartbeatWarningEvery / heartBeatTimeSlice) then 109 | timeWarningCount = 0 110 | Console.WriteLine("audio-heartbeat: DEBUG: notifying operator about warning situation") 111 | My.Computer.Audio.PlaySystemSound(Media.SystemSounds.Exclamation) 112 | end if 113 | end if 114 | 115 | '-- wait until next iteration 116 | sleep(heartbeatTimeSlice) 117 | loop 118 | 119 | -------------------------------------------------------------------------------- /audio-sidechain.md: -------------------------------------------------------------------------------- 1 | 2 | [Audio-Sidechain](audio-sidechain.vb) 3 | ===================================== 4 | 5 | **Audio Sidechain Compression** 6 | 7 | Allow audio output volumes to be automatically and temporarily 8 | reduced, based on audio input volumes (when temporarily above a 9 | threshold) -- similar to an audio side-chain compression. 10 | 11 | Problem 12 | ------- 13 | 14 | In practice there are two challenges in vMix when it comes to audio: 15 | 16 | 1. MICROPHONE DUCKING: 17 | Allow input devices (microphones, attached to the Master bus plus the 18 | "marker" bus Bus-A, but individually controlled) to be temporarily 19 | "ducked" (volume reduced) as long as output devices (callers and 20 | media, monitored on Bus-B) are active. This prevents nasty echos or 21 | even full loops. 22 | 23 | 2. TRANSLATOR VOICE-OVER: 24 | Allow one or more people (usually remote translators, sitting on 25 | vMix Call inputs, receiving the program in Bus-A, and mixed onto the 26 | Master audio bus and additionally monitored on Bus-C) to speak over 27 | the program (usually received via NDI and mixed on the Master audio 28 | bus after being "dimmed" on Bus-B). 29 | 30 | Solution 31 | -------- 32 | 33 | The solution to both challenges is to place those audio inputs onto a 34 | monitoring audio bus which should be observed (similar to the side-chain 35 | of a compressor) and the audio inputs which should be adjusted onto 36 | another audio bus (similar to the regular input of a compressor). 37 | 38 | Usage 39 | ----- 40 | 41 | The recommended configurations are: 42 | 43 | 1. MICROPHONE DUCKING: 44 | 45 | busMonitor = "B" (Notice: callers and media) 46 | busAdjust = "A" (Notice: microphones) 47 | busAdjustInputs = true (Notice: adjust the inputs) 48 | busAdjustInputsExcl = "" 49 | busAdjustUnmutedOnly = false 50 | volumeThreshold = -42 (Notice: -42 dB FS) 51 | volumeFull = 100 (Notice: 100%) 52 | volumeReduced = 30 (Notice: 30% = reduce by -42 dB) 53 | timeSlice = 10 54 | timeAwaitOver = 20 55 | timeAwaitBelow = 150 56 | timeFadeDown = 50 57 | timeFadeUp = 50 58 | 59 | 2. TRANSLATOR VOICE-OVER: 60 | 61 | busMonitor = "C" (Notice: translators) 62 | busAdjust = "B" (Notice: original program) 63 | busAdjustInputs = false (Notice: adjust the bus) 64 | busAdjustInputsExcl = "" 65 | busAdjustUnmutedOnly = false 66 | volumeThreshold = -42 (Notice: -42 dB FS) 67 | volumeFull = 100 (Notice: 100%) 68 | volumeReduced = 60 (Notice: 60% = reduce by -18 dB) 69 | timeSlice = 10 70 | timeAwaitOver = 10 (Notice: allow translators to have priority) 71 | timeAwaitBelow = 1500 (Notice: allow translators to breathe) 72 | timeFadeDown = 10 (Notice: allow translators to have priority) 73 | timeFadeUp = 400 (Notice: fade in program slowly) 74 | 75 | Background 76 | ---------- 77 | 78 | The audio volume science is a little bit hard to understand and vMix 79 | in addition also makes it even more complicated by using different 80 | scales. Here is some background on the Decibel (dB) based scales (like 81 | "volumeThreshold"), the formulas how the scales can be converted, and 82 | some examples: 83 | 84 | Scales: 85 | Volume: 0 to 100 (used for UI volume bars, SetVolumeFade) 86 | Amplitude: 0 to 1 (used for API audio bus meter, @meterF1) 87 | Amplitude2: 0 to 100 (used for API input volume input, @volume) 88 | Decibels: -oo to 0 (used in audio science) 89 | 90 | Formulas: 91 | Amplitude = Amplitude2 / 100 92 | Amplitude2 = Amplitude * 100 93 | Volume = (Amplitude ^ 0.25) * 100 94 | Amplitude = (Volume / 100) ^ 4 95 | Decibels = 20 * Math.Log10(Amplitude) 96 | Amplitude = 10 ^ (Decibels / 20) 97 | 98 | Examples: 99 | ==== METER (measured) =============== ==== CONTROL ============= 100 | Volume (%) Amplitude Decibel (dB FS) Volume (%) Decibel (dB FS) * 101 | ---------- ---------- --------------- ---------- --------------- 102 | 100 1,0000 0,00 100 - 18,00 103 | 95 0,8145 - 1,78 95 - 19,78 104 | 90 0,6561 - 3,66 90 - 21,66 105 | 85 0,5220 - 5,65 85 - 23,65 106 | 80 0,4096 - 7,75 80 - 25,75 107 | 75 0,3164 - 10,00 75 - 28,00 108 | 70 0,2401 - 12,39 70 - 30,39 109 | 65 0,1785 - 14,97 65 - 32,97 110 | 60 0,1296 - 17,75 60 - 35,75 111 | 55 0,0915 - 20,77 55 - 38,77 112 | 50 0,0625 - 24,08 50 - 42,08 113 | 45 0,0410 - 27,74 45 - 45,74 114 | 40 0,0256 - 31,84 40 - 49,84 115 | 35 0,0150 - 36,47 35 - 54,47 116 | 30 0,0081 - 41,83 30 - 59,83 117 | 25 0,0039 - 48,16 25 - 66,16 118 | 20 0,0016 - 55,92 20 - 73,92 119 | 15 0,0005 - 65,91 15 - 83,91 120 | 10 0,0001 - 80,00 10 - 98,00 121 | 5 0,0000 -104,08 5 -122,08 122 | 1 0,0000 -160,00 1 -178,08 123 | 0 0,0000 - oo 0 - oo 124 | 125 | (*) for a usual incoming voice signal of -18 dB FS 126 | 127 | Attention: Keep in mind that 100% of a Volume Meter corresponds to 0 128 | dB FS, while 100% of the Volume Bar corresponds to the full incoming 129 | signal (usually about the target loudness of -18 dB FS for voice). So, 130 | if you attenuate by targeting a Volume Bar of 50%, you are reducing your 131 | incoming signal BY(!) -24 dB relative, and NOT TO(!) -24 dB absolute. 132 | 133 | -------------------------------------------------------------------------------- /audio-sidechain.vb: -------------------------------------------------------------------------------- 1 | '-- 2 | '-- audio-sidechain.vb -- vMix VB.NET script for audio side-chain compression 3 | '-- Copyright (c) 2022-2023 Dr. Ralf S. Engelschall 4 | '-- Distributed under MIT license 5 | '-- 6 | '-- Language: VB.NET 2.0 (vMix 4K/Pro flavor) 7 | '-- Version: 1.5.0 (2024-04-24) 8 | '-- 9 | 10 | '-- ==== CONFIGURATION (please adjust) ==== 11 | 12 | '-- input-based configuration (alternative 1) 13 | dim inputMonitor as string = "" 'id of input to input/monitor volume 14 | dim inputAdjust as string = "" 'id of input to output/adjust volume 15 | dim inputAdjustUnmutedOnly as boolean = false 'whether only unmuted input should be adjusted 16 | 17 | '-- bus-based configuration (alternative 2) 18 | dim busMonitor1 as string = "B" 'id of audio bus to input/monitor volume 19 | dim busMonitor2 as string = "" 'id of audio bus to input/monitor volume (additional bus) 20 | dim busAdjust as string = "A" 'id of audio bus to output/adjust volume 21 | dim busAdjustInputs as boolean = true 'whether inputs attached to bus are adjusted instead of just bus itself 22 | dim busAdjustInputsExcl as string = "" 'comma-separated list of inputs to exclude from adjustment 23 | dim busAdjustUnmutedOnly as boolean = false 'whether only unmuted bus/inputs should be adjusted 24 | 25 | '-- volume configuration 26 | dim volumeThreshold as integer = -36 'threshold of input in dB FS (-oo to 0) 27 | dim volumeFull as integer = 100 'full output in percent (0 to 100) 28 | dim volumeReduced as integer = 20 'reduced output in percent (0 to 100) 29 | 30 | '-- time configuration 31 | dim timeSlice as integer = 10 'time interval between the script iterations (ms) 32 | dim timeAwaitOver as integer = 20 'time over the threshold before triggering fade down (ms) 33 | dim timeAwaitBelow as integer = 350 'time below the threshold before triggering fade up (ms) 34 | dim timeFadeDown as integer = 50 'time for fading down (ms) 35 | dim timeFadeUp as integer = 50 'time for fading up (ms) 36 | 37 | '-- debug configuration 38 | dim debug as boolean = true 'whether to output debug information to the console 39 | 40 | '-- ==== INTERNAL STATE ==== 41 | 42 | '-- internal state 43 | dim mode as string = "wait" 'current iteration mode 44 | dim volumeCurrent as double = -1 'current volume of output in percent (from 0 to 100) 45 | dim timeAwaitBelowCount as integer = 0 'counter for time over the threshold 46 | dim timeAwaitOverCount as integer = 0 'counter for time below the threshold 47 | 48 | '-- pre-convert values 49 | dim volumeThresholdAmp as double = 10 ^ (volumeThreshold / 20) 50 | 51 | '-- prepare XML DOM tree 52 | dim cfg as new System.Xml.XmlDocument 53 | 54 | '-- prepare list of excluded inputs 55 | dim busAdjustInputsExclA() as string = busAdjustInputsExcl.Split(",") 56 | 57 | '-- use a fixed locale for parsing floating point numbers 58 | dim cultureInfo as System.Globalization.CultureInfo = System.Globalization.CultureInfo.CreateSpecificCulture("en-US") 59 | 60 | '-- enter endless iteration loop 61 | do while true 62 | '-- fetch current vMix API status 63 | dim xml as string = API.XML() 64 | cfg.LoadXml(xml) 65 | 66 | '-- determine whether we should operate at all (indicated by muted/unmuted input bus) 67 | dim muted as boolean = true 68 | if busMonitor1 <> "" and inputMonitor = "" then 69 | dim muted1 as boolean = Convert.ToBoolean(cfg.SelectSingleNode("/vmix/audio/bus" & busMonitor1 & "/@muted").Value) 70 | dim muted2 as boolean = true 71 | if busMonitor2 <> "" then 72 | muted2 = Convert.ToBoolean(cfg.SelectSingleNode("/vmix/audio/bus" & busMonitor2 & "/@muted").Value) 73 | end if 74 | muted = muted1 and muted2 75 | elseif busMonitor1 = "" and inputMonitor <> "" then 76 | muted = Convert.ToBoolean(cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & inputMonitor & "']/@muted").Value) 77 | end if 78 | if muted then 79 | '-- ensure we reset current volume knowledge once we become unmuted again 80 | if volumeCurrent >= 0 then 81 | volumeCurrent = -1 82 | end if 83 | continue do 84 | end if 85 | 86 | '-- initialize output volume 87 | if volumeCurrent < 0 then 88 | volumeCurrent = volumeFull 89 | if busAdjust <> "" and inputAdjust = "" then 90 | if not busAdjustInputs then 91 | '-- adjust the audio bus directly 92 | dim isMuted as boolean = Convert.ToBoolean(cfg.SelectSingleNode("/vmix/audio/bus" & busAdjust & "/@muted").Value) 93 | if not isMuted or not busAdjustUnmutedOnly then 94 | API.Function("SetBus" & busAdjust & "Volume", Value := cint(volumeCurrent).ToString()) 95 | end if 96 | else 97 | '-- adjust the inputs attached to the audio bus 98 | dim busInputs as XmlNodeList = cfg.SelectNodes("/vmix/inputs/input[@audiobusses]") 99 | for each busInput as XmlNode in busInputs 100 | dim onBusses() as string = busInput.Attributes("audiobusses").Value.Split(",") 101 | dim title as string = busInput.Attributes("title").Value 102 | if Array.IndexOf(onBusses, busAdjust) >= 0 and Array.IndexOf(busAdjustInputsExclA, title) < 0 then 103 | dim isMuted as boolean = Convert.ToBoolean(busInput.Attributes("muted").Value) 104 | if not isMuted or not busAdjustUnmutedOnly then 105 | dim num as integer = Convert.ToInt32(busInput.Attributes("number").Value) 106 | Input.Find(num).Function("SetVolume", Value := cint(volumeCurrent).ToString()) 107 | end if 108 | end if 109 | next 110 | end if 111 | elseif busAdjust = "" and inputAdjust <> "" then 112 | '-- adjust the input directly 113 | dim isMuted as boolean = Convert.ToBoolean(cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & inputAdjust & "']/@muted").Value) 114 | if not isMuted or not inputAdjustUnmutedOnly then 115 | API.Function("SetVolume", Input := inputAdjust, Value := cint(volumeCurrent).ToString()) 116 | end if 117 | end if 118 | end if 119 | 120 | '-- determine input volume (in linear volume scale) 121 | dim meter1 as double = 0.0 122 | dim meter2 as double = 0.0 123 | if busMonitor1 <> "" and inputMonitor = "" then 124 | dim meter11 = Convert.ToDouble(cfg.SelectSingleNode("/vmix/audio/bus" & busMonitor1 & "/@meterF1").Value, cultureInfo) 125 | dim meter12 = Convert.ToDouble(cfg.SelectSingleNode("/vmix/audio/bus" & busMonitor1 & "/@meterF2").Value, cultureInfo) 126 | dim meter21 = Convert.ToDouble(cfg.SelectSingleNode("/vmix/audio/bus" & busMonitor2 & "/@meterF1").Value, cultureInfo) 127 | dim meter22 = Convert.ToDouble(cfg.SelectSingleNode("/vmix/audio/bus" & busMonitor2 & "/@meterF2").Value, cultureInfo) 128 | meter1 = Math.min(meter11, meter12) 129 | meter2 = Math.min(meter21, meter22) 130 | elseif busMonitor1 = "" and inputMonitor <> "" then 131 | meter1 = Convert.ToDouble(cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & inputMonitor & "']/@meterF1").Value, cultureInfo) 132 | meter2 = Convert.ToDouble(cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & inputMonitor & "']/@meterF2").Value, cultureInfo) 133 | end if 134 | if meter1 < meter2 then 135 | meter1 = meter2 136 | end if 137 | 138 | '-- track whether input volume is continuously over or below volume threshold 139 | if meter1 > volumeThresholdAmp then 140 | timeAwaitOverCount += 1 141 | timeAwaitBelowCount = 0 142 | else 143 | timeAwaitBelowCount += 1 144 | timeAwaitOverCount = 0 145 | end if 146 | 147 | '-- decide current operation mode 148 | dim modeNew as String = "" 149 | if timeAwaitBelowCount >= cint(timeAwaitBelow / timeSlice) and volumeCurrent < volumeFull then 150 | modeNew = "fade-up" 151 | elseif timeAwaitOverCount >= cint(timeAwaitOver / timeSlice) and volumeCurrent > volumeReduced then 152 | modeNew = "fade-down" 153 | else 154 | modeNew = "wait" 155 | end if 156 | if mode <> modeNew then 157 | if debug then 158 | Console.WriteLine("audio-sidechain: INFO: switching to mode: " & modeNew) 159 | end if 160 | mode = modeNew 161 | end if 162 | 163 | '-- fade output volume down/up 164 | if mode = "fade-down" or mode = "fade-up" then 165 | if mode = "fade-down" then 166 | volumeCurrent -= ((volumeFull - volumeReduced) / timeFadeDown) * timeSlice 167 | elseif mode = "fade-up" then 168 | volumeCurrent += ((volumeFull - volumeReduced) / timeFadeUp ) * timeSlice 169 | end if 170 | if busAdjust <> "" and inputAdjust = "" then 171 | if not busAdjustInputs then 172 | '-- adjust the audio bus directly 173 | dim isMuted as boolean = Convert.ToBoolean(cfg.SelectSingleNode("/vmix/audio/bus" & busAdjust & "/@muted").Value) 174 | if not isMuted or not busAdjustUnmutedOnly then 175 | API.Function("SetBus" & busAdjust & "Volume", Value := cint(volumeCurrent).ToString()) 176 | end if 177 | else 178 | '-- adjust the inputs attached to the audio bus 179 | dim busInputs as XmlNodeList = cfg.SelectNodes("/vmix/inputs/input[@audiobusses]") 180 | for each busInput as XmlNode in busInputs 181 | dim onBusses() as string = busInput.Attributes("audiobusses").Value.Split(",") 182 | dim title as string = busInput.Attributes("title").Value 183 | if Array.IndexOf(onBusses, busAdjust) >= 0 and Array.IndexOf(busAdjustInputsExclA, title) < 0 then 184 | dim isMuted as boolean = Convert.ToBoolean(busInput.Attributes("muted").Value) 185 | if not isMuted or not busAdjustUnmutedOnly then 186 | dim num as integer = Convert.ToInt32(busInput.Attributes("number").Value) 187 | Input.Find(num).Function("SetVolume", Value := cint(volumeCurrent).ToString()) 188 | end if 189 | end if 190 | next 191 | end if 192 | elseif busAdjust = "" and inputAdjust <> "" then 193 | '-- adjust the input directly 194 | dim isMuted as boolean = Convert.ToBoolean(cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & inputAdjust & "']/@muted").Value) 195 | if not isMuted or not inputAdjustUnmutedOnly then 196 | API.Function("SetVolume", Input := inputAdjust, Value := cint(volumeCurrent).ToString()) 197 | end if 198 | end if 199 | end if 200 | 201 | '-- wait until next iteration 202 | sleep(timeSlice) 203 | loop 204 | 205 | -------------------------------------------------------------------------------- /auto-pre-mix.md: -------------------------------------------------------------------------------- 1 | 2 | [Auto-Pre-Mix](auto-pre-mix.vb) 3 | =============================== 4 | 5 | **Automatically Pre-Mixing Inputs** 6 | 7 | Allow one to auto-pre-mix (aka pre-render or flattening) source 8 | inputs with the help of two intermediate Mix-type input(s) in order to 9 | further embed the result onto a layer of a target input. 10 | 11 | Problem 12 | ------- 13 | 14 | vMix is an awesome video mixing software, but it is still rather limited 15 | (especially in contrast to OBS Studio) when it comes to its input 16 | layering: when creating complex input trees the maximum layering depth 17 | is 2, and when applying effects like positioning there is only the 18 | single, inherited, original positioning. The reason is the design 19 | weakness of vMix that no pre-rendering/flattening of layers actually 20 | happens. 21 | 22 | In practice the following scenario of a greenscreen-based and 23 | virtual-PTZ-based scenario illustrates the problem: The *N* physical 24 | cameras are on inputs `PCAM-`*N*. For applying a chroma-key filter 25 | (without changing the original input), *N* virtual inputs `VCAM-`*N* 26 | are created from the `PCAM-`*N* inputs. For replacing the greenscreen 27 | with *N* pre-rendered background images (inputs `BG-`*N*) and performing 28 | *K* virtual pan/tilt/zoom (PTZ), *N*x*K* `VPTZ-`*N*`-`*xxx* inputs (of 29 | type Virtual Set / Blank) are created with each having the background 30 | image `BG-`*N* on layer 1, the `VCAM-`*N* on layer 2 and an individual 31 | Position for the PTZ. *X* scene inputs (*X* <= *N*x*K*) `SCENE-`*X* 32 | can now be created with each having `VPTZ-`*N*`-`*xxx* on layer 1, 33 | and e.g. slides and titles on additional layers. 34 | 35 | Unfortunately, when a slide is too unreadable and you want to exchange 36 | the virtual camera `VPTZ-`*N*`-`*xxx* and the slide by creating 37 | a special scene `SCENE-PIP` with the slide on layer 1 and the virtual camera 38 | `VPTZ-`*N*`-`*xxx* on layer 2 (as a PIP) you cannot achieve the PIP 39 | effect because the Position of layer 2 interferes with the Position 40 | already in the Virtual Set `VPTZ-`*N*`-`*xxx*. Even a virtual input from 41 | `VPTZ-`*N*`-`*xxx* doesn't help here. 42 | 43 | Solution 44 | -------- 45 | 46 | Fortunately, vMix 4K/Pro have Mix input types which actually perform 47 | pre-rendering/flattening of inputs -- even if they are primarily 48 | intended for switching between inputs. Unfortunately, vMix provides only 49 | a maximum of just 3(!) of those Mix input types. But we can re-use a Mix 50 | input type by dynamically re-configuring it to show (and pre-render) a 51 | particular input when it a scene containing it comes into PREVIEW. In 52 | order to not change the PROGRAM, two Mix inputs are used in total. 53 | 54 | For our practical scenario, you create two Mix-type inputs `PRERENDER1` 55 | and `PRERENDER2` and in the PIP-scene `SCENE-PIP` you now place the 56 | slide on layer 1, `PRERENDER1` on layer 2 and the virtual camera 57 | `VPTZ-`*N*`-`*xxx* on layer 3, but disabled. The Position for the 58 | PIP is now done on layer 2 instead of 3. When the `SCENE-PIP` comes 59 | into PREVIEW, the `auto-pre-mix` script detects the scenario and 60 | automatically switches `PRERENDER1` to `VPTZ-`*N*`-`*xxx*. In case 61 | PROGRAM already uses `PRERENDER1`, the `SCENE-PIP` is re-configured to 62 | use `PRERENDER2` and then this Mix is switched. 63 | 64 | Usage 65 | ----- 66 | 67 | Create two *Mix*-type inputs `PRERENDER1` and `PRERENDER2` and on any 68 | target input, place any *Mix* input onto layer *N* and any source input 69 | on layer *N*+1. 70 | 71 | Alternative 72 | ----------- 73 | 74 | An alternative for pre-mixing is to use a VirtualSet with a single 75 | layer based on a full-screen UV Map (1:1 mapping). See [vMix-VirtualSet-Flatten](https://github.com/rse/vmix-assets/tree/master/vMix-VirtualSet-Flatten) 76 | for a realization. 77 | 78 | -------------------------------------------------------------------------------- /auto-pre-mix.vb: -------------------------------------------------------------------------------- 1 | '-- 2 | '-- auto-pre-mix.vb -- vMix script for Automatically Pre-Mixing Inputs 3 | '-- Copyright (c) 2022-2023 Dr. Ralf S. Engelschall 4 | '-- Distributed under MIT license 5 | '-- 6 | '-- Language: VB.NET 2.0 (vMix 4K/Pro flavor) 7 | '-- Version: 0.9.1 (2022-07-27) 8 | '-- 9 | 10 | '-- CONFIGURATION 11 | dim mix1MixNumber as String = "1" 12 | dim mix1InputName as String = "PRERENDER1" 13 | dim mix2MixNumber as String = "2" 14 | dim mix2InputName as String = "PRERENDER2" 15 | dim timeSlice as Integer = 250 16 | dim debug as Boolean = true 17 | 18 | '-- prepare XML DOM tree and load the current API state 19 | dim cfg as new System.Xml.XmlDocument 20 | dim xml as String = API.XML() 21 | cfg.LoadXml(xml) 22 | 23 | '-- determine UUIDs of mix inputs 24 | dim mix1InputNum as String = cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & mix1InputName & "']/@number").Value 25 | dim mix1InputKey as String = cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & mix1InputName & "']/@key").Value 26 | dim mix2InputNum as String = cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & mix2InputName & "']/@number").Value 27 | dim mix2InputKey as String = cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & mix2InputName & "']/@key").Value 28 | 29 | '-- keep internal state 30 | dim lastInPreview as String = "" 31 | dim lastInProgram as String = "" 32 | 33 | '-- endless loop 34 | do while true 35 | '-- re-load the current API state 36 | xml = API.XML() 37 | cfg.LoadXml(xml) 38 | 39 | '-- determine what is currently in preview 40 | dim nowInPreview as String = cfg.SelectSingleNode("/vmix/preview").InnerText 41 | 42 | '-- only react if a new input was placed into preview 43 | if nowInPreview <> lastInPreview then 44 | lastInPreview = nowInPreview 45 | dim inputName as String = "" 46 | if debug then 47 | inputName = cfg.SelectSingleNode("/vmix/inputs/input[@number = '" & nowInPreview & "']/@title").Value 48 | Console.WriteLine("auto-pre-mix: INFO: PREVIEW change detected: input=" & inputName) 49 | end if 50 | 51 | '-- determine what is currently in program 52 | dim nowInProgram as String = cfg.SelectSingleNode("/vmix/active").InnerText 53 | 54 | '-- transitively iterate through the program input tree 55 | '-- and find out whether a pre-rendering Mix is used at all 56 | dim mix1Found as Boolean = false 57 | dim mix2Found as Boolean = false 58 | dim inputKey as String = cfg.SelectSingleNode("/vmix/inputs/input[@number = '" & nowInProgram & "']/@key").Value 59 | dim stack as System.Collections.Stack = new System.Collections.Stack() 60 | stack.Push(inputKey) 61 | do while stack.Count > 0 and (not mix1Found or not mix2Found) 62 | inputKey = stack.Pop() 63 | if debug then 64 | inputName = cfg.SelectSingleNode("/vmix/inputs/input[@key = '" & inputKey & "']/@title").Value 65 | Console.WriteLine("auto-pre-mix: DEBUG: crawl PROGRAM input tree: input=" & inputName) 66 | end if 67 | if inputKey = mix1InputKey then 68 | mix1Found = true 69 | exit do 70 | elseif inputKey = mix2InputKey then 71 | mix2Found = true 72 | exit do 73 | else 74 | dim layers as XmlNodeList = cfg.SelectNodes("/vmix/inputs/input[@key = '" & inputKey & "']/overlay") 75 | for each layer as XmlNode in layers 76 | dim layerKey as String = layer.Attributes("key").InnerText 77 | stack.Push(layerKey) 78 | next 79 | end if 80 | loop 81 | if debug then 82 | if mix1Found then 83 | Console.WriteLine("auto-pre-mix: INFO: found mix 1 usage in PROGRAM input tree: input=" & mix1InputName) 84 | end if 85 | if mix2Found then 86 | Console.WriteLine("auto-pre-mix: INFO: found mix 2 usage in PROGRAM input tree: input=" & mix2InputName) 87 | end if 88 | end if 89 | 90 | '-- transitively iterate through the Preview input tree 91 | '-- in order to re-configure the pre-rendering Mix which should be used 92 | '-- (i.e. the one which is not used in Program) 93 | inputKey = cfg.SelectSingleNode("/vmix/inputs/input[@number = '" & nowInPreview & "']/@key").Value 94 | stack = new System.Collections.Stack() 95 | stack.Push(inputKey) 96 | do while stack.Count > 0 97 | inputKey = stack.Pop() 98 | if debug then 99 | inputName = cfg.SelectSingleNode("/vmix/inputs/input[@key = '" & inputKey & "']/@title").Value 100 | Console.WriteLine("auto-pre-mix: DEBUG: crawl PREVIEW input tree: input=" & inputName) 101 | end if 102 | 103 | '-- determine input details 104 | dim targetNum as String = cfg.SelectSingleNode("/vmix/inputs/input[@key = '" & inputKey & "']/@number").Value 105 | dim targetOverlays as XmlNodeList = cfg.SelectNodes("/vmix/inputs/input[@key = '" & inputKey & "']/overlay") 106 | 107 | '-- check for the special input configuration we act on 108 | dim mixNumber as String = "" '-- the mix number to use 109 | dim mixChange as Boolean = False '-- whether to change the mix 110 | dim overlay1Number as Integer = -1 '-- the overlay of potential mix input (layer number) 111 | dim overlay2Number as Integer = -1 '-- the overlay of potential source input (layer number) 112 | for i as Integer = 0 to targetOverlays.Count - 1 113 | dim ovKey as String = targetOverlays.Item(i).Attributes("key").InnerText 114 | if ovKey = mix1InputKey then 115 | overlay1Number = i 116 | if not mix1Found then 117 | mixNumber = mix1MixNumber 118 | else 119 | mixNumber = mix2MixNumber 120 | mixChange = true 121 | end if 122 | elseif ovKey = mix2InputKey then 123 | overlay1Number = i 124 | if not mix2Found then 125 | mixNumber = mix2MixNumber 126 | else 127 | mixNumber = mix1MixNumber 128 | mixChange = true 129 | end if 130 | elseif overlay1Number <> -1 and overlay2Number = -1 then 131 | overlay2Number = i 132 | end if 133 | stack.Push(ovKey) 134 | next 135 | 136 | '-- react on special input configuration, if found 137 | if mixNumber <> "" and overlay1Number <> -1 and overlay2Number <> -1 then 138 | if debug then 139 | dim overlay1Name as String = cfg.SelectSingleNode("/vmix/inputs/input[@key = '" & targetOverlays.Item(overlay1Number).Attributes("key").InnerText & "']/@title").Value 140 | dim overlay2Name as String = cfg.SelectSingleNode("/vmix/inputs/input[@key = '" & targetOverlays.Item(overlay2Number).Attributes("key").InnerText & "']/@title").Value 141 | Console.WriteLine("auto-pre-mix: INFO: target input " & inputName & ": found setup: layer-" & (overlay1Number + 1) & "=" & overlay1Name & " layer-" & (overlay2Number + 1) & "=" & overlay2Name) 142 | end if 143 | 144 | '-- reconfigure the pre-rendering Mix input 145 | dim overlayKey as String = targetOverlays.Item(overlay2Number).Attributes("key").InnerText 146 | dim overlayName as String = cfg.SelectSingleNode("/vmix/inputs/input[@key = '" & targetOverlays.Item(overlay2Number).Attributes("key").InnerText & "']/@title").Value 147 | dim overlayNum as String = cfg.SelectSingleNode("/vmix/inputs/input[@key = '" & targetOverlays.Item(overlay2Number).Attributes("key").InnerText & "']/@number").Value 148 | if debug then 149 | if mixNumber = mix1MixNumber then 150 | Console.WriteLine("auto-pre-mix: INFO: target input " & inputName & ": switch: mix=" & mix1InputName & " input=" & overlayName) 151 | else 152 | Console.WriteLine("auto-pre-mix: INFO: target input " & inputName & ": switch: mix=" & mix2InputName & " input=" & overlayName) 153 | end if 154 | end if 155 | API.Function("PreviewInput", Input := overlayNum, Mix := mixNumber) 156 | API.Function("ActiveInput", Input := overlayNum, Mix := mixNumber) 157 | 158 | '-- optionally re-configure mix layer 159 | if mixChange then 160 | if mixNumber = mix1MixNumber then 161 | if debug then 162 | Console.WriteLine("auto-pre-mix: INFO: target input " & inputName & ": reconfigure: layer-" & (overlay1Number + 1) & "=" & mix1InputName) 163 | end if 164 | API.Function("SetMultiViewOverlay", Input := targetNum, Value := (overlay1Number + 1).toString() & "," & mix1InputNum) 165 | else 166 | if debug then 167 | Console.WriteLine("auto-pre-mix: INFO: target input " & inputName & ": reconfigure: layer-" & (overlay1Number + 1) & "=" & mix2InputName) 168 | end if 169 | API.Function("SetMultiViewOverlay", Input := targetNum, Value := (overlay1Number + 1).toString() & "," & mix2InputNum) 170 | end if 171 | end if 172 | 173 | '-- disable the marker overlay of the source input on the target input 174 | if debug then 175 | Console.WriteLine("auto-pre-mix: INFO: target input " & inputName & ": reconfigure: layer-" & (overlay2Number + 1) & "=" & overlayName & " (disabled)") 176 | end if 177 | API.Function("MultiViewOverlayOff", Input := targetNum, Value := (overlay2Number + 1).toString()) 178 | end if 179 | loop 180 | end if 181 | 182 | '-- wait a little bit before next iteration 183 | sleep(timeSlice) 184 | loop 185 | 186 | -------------------------------------------------------------------------------- /clone-input.md: -------------------------------------------------------------------------------- 1 | 2 | [Clone-Input](clone-input.vb) 3 | ============================= 4 | 5 | **Really Cloning an Arbitrary Input** 6 | 7 | Allow an arbitrary input (which has to be in the preview) to be 8 | really cloned/duplicated. 9 | 10 | Problem 11 | ------- 12 | 13 | vMix has to real input cloning functionality. It only has the "Settings" 14 | / "Copy from..." functionality of an input, but this copies not 15 | everything. Also, the "Settings" / "General" / "Create Virtual Input" 16 | functionality of an input still attaches the resulting input to the 17 | original one. But when creating dozens of event scenes, one usually 18 | wants to clone existing scenes. 19 | 20 | Solution 21 | -------- 22 | 23 | This script performs a real clone of an input by directly operating 24 | on the underlying vMix preset XML file. For this to work correctly, 25 | ensure that you are running on an already saved vMix preset (which is 26 | usually always the case in production, except when you are trying out 27 | this script on a freshly started vMix). 28 | 29 | Usage 30 | ----- 31 | 32 | Configure a vMix Shortcut with:
33 | *key* `ScriptStart` `clone-input` 34 | 35 | -------------------------------------------------------------------------------- /clone-input.vb: -------------------------------------------------------------------------------- 1 | '-- 2 | '-- clone-input.vb -- vMix script for really cloning an arbitrary input 3 | '-- Copyright (c) 2022-2023 Dr. Ralf S. Engelschall 4 | '-- Distributed under MIT license 5 | '-- 6 | '-- Language: VB.NET 2.0 (vMix 4K/Pro flavor) 7 | '-- Version: 0.9.0 (2022-03-04) 8 | '-- 9 | 10 | '-- load the current API state 11 | dim xml as string = API.XML() 12 | dim cfg as new System.Xml.XmlDocument 13 | cfg.LoadXml(xml) 14 | 15 | '-- determine input currently in preview 16 | dim inputNum as String = cfg.SelectSingleNode("/vmix/preview").InnerText 17 | dim inputKey as String = cfg.SelectSingleNode("/vmix/inputs/input[@number = '" & inputNum & "']/@key").InnerText 18 | Console.WriteLine("clone-input: INFO: Cloning input #" & inputNum & " (" & inputKey & ")") 19 | 20 | '-- determine current preset 21 | dim presetNode as System.Xml.XmlNode = cfg.SelectSingleNode("/vmix/preset") 22 | if presetNode is Nothing then 23 | Console.WriteLine("clone-input: ERROR: You are running on a still UNSAVED vMix preset!") 24 | Console.WriteLine("clone-input: ERROR: Save your preset at least once, please.") 25 | return 26 | end if 27 | dim presetFile as String = presetNode.InnerText 28 | 29 | '-- save current preset (to ensure there are no modifications lost) 30 | API.Function("SavePreset", Value := presetFile) 31 | 32 | '-- read preset file and parse as XML 33 | dim utf8WithoutBOM as new System.Text.UTF8Encoding(false) 34 | xml = System.IO.File.ReadAllText(presetFile, utf8WithoutBOM) 35 | dim preset as new System.Xml.XmlDocument 36 | preset.PreserveWhitespace = true 37 | preset.LoadXml(xml) 38 | 39 | '-- find input 40 | dim inputNode as System.Xml.XmlNode = preset.SelectSingleNode("/XML/Input[@Key = '" & inputKey & "']") 41 | if inputNode is Nothing then 42 | Console.WriteLine("clone-input: ERROR: Unexpected inconsistency problem detected: Failed to locate vMix") 43 | Console.WriteLine("clone-input: ERROR: input #" & inputNum & " (" & inputKey & ") in the underlying vMix preset file!") 44 | return 45 | end if 46 | 47 | '-- clone input 48 | dim cloneNode as System.Xml.XmlNode = inputNode.Clone() 49 | dim GUID As String = System.Guid.NewGuid.ToString() 50 | cloneNode.Attributes("Key").Value = GUID 51 | if cloneNode.Attributes("Title") is Nothing then 52 | dim attr as System.Xml.XmlAttribute = preset.CreateAttribute("Title") 53 | attr.Value = cloneNode.Attributes("OriginalTitle").Value & " (CLONED)" 54 | cloneNode.Attributes.SetNamedItem(attr) 55 | else 56 | cloneNode.Attributes("Title").Value = cloneNode.Attributes("Title").Value & " (CLONED)" 57 | end if 58 | Console.WriteLine("clone-input: INFO: Cloned input under new GUID " & GUID) 59 | 60 | '-- insert cloned input 61 | inputNode.ParentNode.insertAfter(cloneNode, inputNode) 62 | 63 | '-- make backup of preset file 64 | System.IO.File.Copy(presetFile, presetFile & ".bak", true) 65 | 66 | '-- serialize XML and write preset file 67 | System.IO.File.WriteAllText(presetFile, preset.OuterXml, utf8WithoutBOM) 68 | 69 | '-- reopen the preset file (to activate our cloned input) 70 | API.Function("OpenPreset", Value := presetFile) 71 | 72 | -------------------------------------------------------------------------------- /event-reconfiguration.gtzip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rse/vmix-scripts/842c01912f266bc711143cfc329336d03157d421/event-reconfiguration.gtzip -------------------------------------------------------------------------------- /event-reconfiguration.md: -------------------------------------------------------------------------------- 1 | 2 | [Event-Reconfiguration](event-reconfiguration.vb) 3 | ================================================= 4 | 5 | **Reconfiguration of Event NDI Inputs (and Lower-Third Titles)** 6 | 7 | Allow one to step forward/backward through (or to a particular row of) 8 | an Excel-based conference event configuration by re-configuring four 9 | reusable NDI input sources (for shared content, one moderator and two 10 | presenters). 11 | 12 | See also the [Event-Title-Control](event-title-control.md) for the 13 | companion script to control the Lower-Third Titles of the people. 14 | 15 | Problem 16 | ------- 17 | 18 | For recorded, remote-only, online-only conference events, you want to 19 | chronologically step through the event phases, where in each phase you 20 | have to ingest a moderator camera, up to two presenters cameras and a 21 | shared screen of one of the presenters. 22 | 23 | Unfortunately, although vMix Call is very convenient for ingesting 24 | people, it is too limited. It supports only up to 720p (which can be 25 | acceptable) and especially does not directly and easily support that 26 | a presenter camera and screen can be shared in parallel (which is not 27 | acceptable for presenters). 28 | 29 | Additionally, even if presenters would accept having to fiddle around 30 | with two independent vMix Call browser tabs (one for the camera, one for 31 | the screen sharing), this also effectively limits the solution to just 32 | 5 parallel people being online (as vMix supports only up to 8 vMix Call 33 | instances, even in vMix Pro, and a moderator and its screen, plus two 34 | presenters and their screen, AND at least the next two presenters and 35 | their screen, already requires all the available 8 vMix Call instances). 36 | 37 | Solution 38 | -------- 39 | 40 | The alternative [VDON Call](https://github.com/rse/vdon-call/) is 41 | a WebRTC/NDI-based remote caller ingest solution for live video 42 | productions, based on the two swiss army knifes in their field: 43 | the awesome, low-latency, P2P-based video streaming facility 44 | [VDO.Ninja](https://vdo.ninja), and the awesome, ultra-flexible video 45 | mixing software [OBS Studio](https://obsproject.org). 46 | 47 | VDON Call allows you to ingest up to 16 streams which means 8 cameras 48 | plus up to 8 screen sharings -- all in parallel and in advance. This 49 | especially means that usually all people and their screens can be online 50 | in prepared in advance to the event. When ingesting the VDO.Ninja-based 51 | VDON Call streams via OBS Studio, you get 16 NDI A/V-streams on your 52 | network. 53 | 54 | In an [Excel-based event configuration](event-reconfiguration.xlsx) you 55 | pre-configure your conference event by splitting it into phases 1-N and 56 | in each phase you configure the moderator, the up to two presenters and 57 | their screen sharing (plus the names and titles of the people). 58 | 59 | The **Event-Reconfiguration** facility then allows you to step 60 | forward/backward through (or to a particular row of) this event 61 | configuration and re-configures four reusable NDI input sources (for 62 | shared content, one moderator P1 and two presenters P2 and P3). The crux 63 | is the use of a [full-screen GT title](event-reconfiguration.gtzip) 64 | input which holds the four cell information of the underlying [Excel 65 | data source](event-reconfiguration.xlsx) inside vMix between calls to 66 | this script. 67 | 68 | Usage 69 | ----- 70 | 71 | Create four NDI inputs and map their input name to the Excel 72 | field names in the [XML mapping file](event-reconfiguration.xml). 73 | Also create a input based on the information holding [GT 74 | title](event-reconfiguration.gtzip). Finally, configure the following 75 | vMix Shortcuts for reconfiguring the NDI inputs during the event: 76 | 77 | # switch to previous event configuration row 78 | SetDynamicValue1 PREV 79 | SetDynamicValue2 80 | ScriptStart event-reconfiguration 81 | 82 | # switch to next event configuration row 83 | SetDynamicValue1 NEXT 84 | SetDynamicValue2 85 | ScriptStart event-reconfiguration 86 | 87 | # switch to particular event configuration row) 88 | SetDynamicValue1 42 89 | SetDynamicValue2 90 | ScriptStart event-reconfiguration 91 | 92 | -------------------------------------------------------------------------------- /event-reconfiguration.vb: -------------------------------------------------------------------------------- 1 | '-- 2 | '-- event-reconfiguration.vb -- vMix script for updating event configuration 3 | '-- Copyright (c) 2022-2023 Dr. Ralf S. Engelschall 4 | '-- Distributed under MIT license 5 | '-- 6 | '-- Language: VB.NET 2.0 (vMix 4K/Pro flavor) 7 | '-- Version: 1.0.1 (2022-04-01) 8 | '-- 9 | 10 | '-- load the current API state 11 | dim xml as string = API.XML() 12 | dim cfg as new System.Xml.XmlDocument 13 | cfg.LoadXml(xml) 14 | 15 | '-- determine parameter 16 | dim opName as string = cfg.SelectSingleNode("//dynamic/value1").InnerText 17 | dim settingFile as string = cfg.SelectSingleNode("//dynamic/value2").InnerText 18 | 19 | '-- read configuration file 20 | dim utf8WithoutBOM as new System.Text.UTF8Encoding(false) 21 | xml = System.IO.File.ReadAllText(settingFile, utf8WithoutBOM) 22 | dim setting as new System.Xml.XmlDocument 23 | setting.LoadXml(xml) 24 | 25 | '-- parse configuration information 26 | dim dataSource as String = setting.SelectSingleNode("/event/data-source/@name").Value 27 | dim titleSource as String = setting.SelectSingleNode("/event/title-source/@name").Value 28 | dim ndiMappings as XmlNodeList = setting.SelectNodes("/event/ndi-mapping") 29 | 30 | '-- change row in data source 31 | if opName = "PREV" then 32 | API.Function("DataSourcePreviousRow", Value := dataSource) 33 | elseif opName = "NEXT" then 34 | API.Function("DataSourceNextRow", Value := dataSource) 35 | else 36 | API.Function("DataSourceSelectRow", Value := dataSource & "," & opName) 37 | end if 38 | 39 | '-- reset the title control states 40 | dim titleNodes as XmlNodeList = cfg.SelectNodes("//inputs/input[@key and @type = 'GT' and text[@name = 'LastTransition.Text']]") 41 | for each titleNode as XmlNode in titleNodes 42 | dim title as String = titleNode.Attributes("title").Value 43 | Input.Find(title).Function("SetText", SelectedName := "LastTransition.Text", Value := "0") 44 | next 45 | 46 | '-- give vMix some time to update the title input 47 | sleep(100) 48 | 49 | '-- update the NDI inputs 50 | for each ndiMapping as XmlNode in ndiMappings 51 | dim fieldName as String = ndiMapping.Attributes("field-name").Value 52 | dim inputName as String = ndiMapping.Attributes("input-name").Value 53 | dim ndiStream as string = Input.Find(titleSource).Text(fieldName & ".Text") 54 | API.Function("NDISelectSourceByName", Input := inputName, Value := ndiStream) 55 | next 56 | 57 | -------------------------------------------------------------------------------- /event-reconfiguration.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rse/vmix-scripts/842c01912f266bc711143cfc329336d03157d421/event-reconfiguration.xlsx -------------------------------------------------------------------------------- /event-reconfiguration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /event-title-control.gtzip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rse/vmix-scripts/842c01912f266bc711143cfc329336d03157d421/event-title-control.gtzip -------------------------------------------------------------------------------- /event-title-control.md: -------------------------------------------------------------------------------- 1 | 2 | [Event-Title-Control](event-title-control.vb) 3 | ============================================= 4 | 5 | **Control Layer-Embedded Titles** 6 | 7 | Control the in/out transitioning of lower-third titles which are 8 | embedded layers of scene inputs (where vMix only performs `TransitionIn` 9 | and never a `TransitionOut`). 10 | 11 | Problem 12 | ------- 13 | 14 | Titles in vMix are usually controlled via the Overlay 1-4 facility. This 15 | can be done manually by pressing the overlay buttons by the operator, or 16 | automatically with three triggers on a scene input: 17 | 18 | Trigger Function Input Delay 19 | TransitionIn OverlayInput1In 2000 20 | TransitionIn OverlayInput1Out 8000 21 | TransitionOut OverlayInput1Off 22 | 23 | Alternatively, you can also embed the title input as a layer on the 24 | scene input. But then vMix triggers only a `TransitionIn` on the title 25 | and never a `TransitionOut`). Finally, when reusing scenes, the title of 26 | a person should be raised only once within a certain time range. 27 | 28 | Solution 29 | -------- 30 | 31 | A [special reusable title](event-title-control.gtzip) is used for 32 | embedding on layers of scene inputs. The title contains an invisible 33 | `LastTransition` field for tracking time and makes all its elements 34 | `Hidden` on `TransitionIn` and `TransitionOut`, and performs the in/out 35 | transitioning on `Page1`/`Page2` instead. 36 | 37 | Additionally, this script ensures that independent of arbitrary scene 38 | input changes, the titles are shown just for `durationVisible` seconds 39 | and at maximum every `durationLocked` seconds (see configuration section 40 | at the top of the script). 41 | 42 | Usage 43 | ----- 44 | 45 | Place the title input(s) onto layers of the scene input and configure 46 | triggers on the scene input: 47 | 48 | OnTransitionIn SetDynamicValue1 <title-input-name>,... 49 | OnTransitionIn ScriptStart event-title-control 50 | 51 | -------------------------------------------------------------------------------- /event-title-control.vb: -------------------------------------------------------------------------------- 1 | '-- 2 | '-- event-title-control.vb -- vMix script for animating title 3 | '-- Copyright (c) 2022-2023 Dr. Ralf S. Engelschall <rse@engelschall.com> 4 | '-- Distributed under MIT license <https://spdx.org/licenses/MIT.html> 5 | '-- 6 | '-- Language: VB.NET 2.0 (vMix 4K/Pro flavor) 7 | '-- Version: 1.0.0 (2022-03-23) 8 | '-- 9 | 10 | '-- CONFIGURATION 11 | dim durationVisible as Integer = 10 '-- seconds to wait between title in and out 12 | dim durationLocked as Integer = 900 '-- seconds to wait until title can be shown again 13 | 14 | '-- load the current API state 15 | dim cfg as new System.Xml.XmlDocument 16 | dim xml as String = API.XML() 17 | cfg.LoadXml(xml) 18 | 19 | '-- determine parameter 20 | dim titleNames() as String = cfg.SelectSingleNode("//dynamic/value1").InnerText.Split(",") 21 | 22 | '-- determine current time 23 | dim now as Integer = (DateTime.Now - #1/1/1970#).TotalSeconds 24 | 25 | '-- iterate over all titles 26 | dim titleTransitioned as String = "" 27 | for each titleName as String in titleNames 28 | '-- determine currently selected transition 29 | dim selectedIndex as Integer = Convert.ToInt32(cfg.SelectSingleNode("//inputs/input[@title = '" & titleName & "']/@selectedIndex").Value) 30 | if selectedIndex <> 1 then 31 | '-- if transition IN was still not done, we might do it... 32 | dim state as String = Input.Find(titleName).Text("LastTransition.Text") 33 | dim time as Integer = 0 34 | if state <> "" then 35 | time = Convert.ToInt32(state) 36 | end if 37 | if (time + (durationVisible + durationLocked)) <= now then 38 | '-- ...but only at maximum every durationLocked seconds 39 | Input.Find(titleName).Function("SetText", SelectedName := "LastTransition.Text", Value := now.ToString()) 40 | Input.Find(titleName).Function("SelectIndex", Value := 1) 41 | 42 | '-- remember to transition OUT the title again afterwards 43 | if titleTransitioned <> "" then 44 | titleTransitioned = titleTransitioned & "," 45 | end if 46 | titleTransitioned = titleTransitioned & titleName 47 | end if 48 | end if 49 | next 50 | 51 | '-- in case of any transitions IN, wait and transition OUT again 52 | if titleTransitioned <> "" then 53 | '-- first let the title be visible for the configured amount of time 54 | Sleep(durationVisible * 1000) 55 | 56 | '-- reload the current API state 57 | xml = API.XML() 58 | cfg.LoadXml(xml) 59 | 60 | '-- iterate over all remembered titles again 61 | dim titleNames2() as String = titleTransitioned.Split(",") 62 | for each titleName2 as String in titleNames2 63 | '-- determine currently selected transition once again 64 | dim selectedIndex2 as Integer = Convert.ToInt32(cfg.SelectSingleNode("//inputs/input[@title = '" & titleName2 & "']/@selectedIndex").Value) 65 | if selectedIndex2 = 1 then 66 | '-- if transition IN was really done (and not something changed 67 | '-- between our operations as a side-effect), we transition OUT again 68 | Input.Find(titleName2).Function("SelectIndex", Value := 2) 69 | end if 70 | next 71 | end if 72 | 73 | -------------------------------------------------------------------------------- /input-bridge.md: -------------------------------------------------------------------------------- 1 | 2 | [Input-Bridge](input-bridge.vb) 3 | =============================== 4 | 5 | **Bridge Inputs between vMix instances** 6 | 7 | Allow one to bridge/tunnel an arbitrary number of inputs between two 8 | vMix instances with the help of two NDI streams in order to perform load 9 | offloading between two vMix instances. 10 | 11 | See the corresponding [demonstration video](https://youtu.be/Y6MHAtpMYG8) for details. 12 | 13 | Problem 14 | ------- 15 | 16 | Sometimes a single vMix instance is overloaded, even on a very powerful 17 | computer. One solution is to offload a bunch of inputs, e.g., camera 18 | inputs and their chroma-key filtering and virtual PTZ positioning, to 19 | a second vMix instance. For instance, when you have 4 cameras and 7 20 | virtual PTZ, you would want to offload the corresponding 28 inputs. 21 | Unfortunately, on a 1 Gbps network link you can transmit just a maximum 22 | of eight 1080p30, or six 1080p60, or four 2160p30, or two 2160p60, NDI 23 | streams between the two vMix instances. You would need at least a 10 24 | Gbps network link and use not more than 2160p30 if all the 28 inputs 25 | should be used on both vMix instances in parallel. 26 | 27 | Solution 28 | -------- 29 | 30 | Usually, even if you could bridge all 28 inputs, the primary vMix 31 | instance uses just a single one in its output/program and potentially 32 | another single one in its preview. So, in practice it is usually 33 | sufficient to bridge the arbitrary number of inputs over just two NDI 34 | streams, as long as all bridged inputs are singletons and do not occur 35 | as a pair in any scene and at any time. 36 | 37 | Usage 38 | ----- 39 | 40 | Suppose you you want to offload inputs named `VPTZ - XXX` from the vMix 41 | instance on COMPUTER2 (10.0.0.12) to a pre-processing vMix instance on 42 | COMPUTER1 (10.0.0.11). Then setup COMPUTER1 as following: 43 | 44 | - add the `VPTZ - XXX` inputs as usual (perhaps as the leaves of 45 | a complex input hierarchy like Camera / Virtual Input / VirtualSet). 46 | 47 | - add two inputs of type *Mix* named `BRIDGE1` and `BRIDGE2` and 48 | under their *Settings / Outputs* enable NDI for the outputs #3 and #4 and 49 | set these two inputs. 50 | 51 | Then setup COMPUTER2 as following: 52 | 53 | - add the `input-bridge` script to COMPUTER2 and adjust its configuration to: 54 | 55 | ``` 56 | dim peerAPI as String = "http://10.0.0.11:8088/API/" 57 | dim bridge1MixNum as String = "1" 58 | dim bridge2MixNum as String = "2" 59 | dim bridge1InputName as String = "BRIDGE1" 60 | dim bridge2InputName as String = "BRIDGE2" 61 | dim timeSlice as Integer = 50 62 | dim debug as Boolean = true 63 | ``` 64 | 65 | - add two inputs of type *NDI* named `BRIDGE1` and `BRIDGE2`. Their NDI 66 | streams should be set to `COMPUTER1 (vMix - Output 3)` and `COMPUTER1 67 | (vMix - Output 4)`. 68 | 69 | - for all the to be bridged inputs `VPTZ - XXX` (from COMPUTER1) add inputs of type Blank 70 | and set their layer 1 initially to just `BRIDGE1`. The actually used 71 | bridge input (`BRIDGE1` or `BRIDGE2`) is automatically selected 72 | afterwards and hence you can use any bridge input initially. 73 | 74 | Finally, on COMPUTER2, start the script `input-bridge` and bring any of 75 | the `VPTZ - XXX` inputs into preview (and then potentially cut it to 76 | output/program) and observe that the script automatically adjusts the 77 | layer 1 on the `VPTZ - XXX` inputs (it switches between `BRIDGE1` and 78 | `BRIDGE2` when necessary) and it automatically adjusts the Mix inputs 79 | `BRIDGE1` and `BRIDGE2` on COMPUTER1 to have the particular `VPTZ - XXX` 80 | inputs as their active input. 81 | 82 | -------------------------------------------------------------------------------- /input-bridge.vb: -------------------------------------------------------------------------------- 1 | '-- 2 | '-- input-bridge.vb -- vMix script for Bridging Inputs between vMix instances 3 | '-- Copyright (c) 2022-2023 Dr. Ralf S. Engelschall <rse@engelschall.com> 4 | '-- Distributed under MIT license <https://spdx.org/licenses/MIT.html> 5 | '-- 6 | '-- Language: VB.NET 2.0 (vMix 4K/Pro flavor) 7 | '-- Version: 1.1.0 (2023-06-11) 8 | '-- 9 | 10 | '-- ==== CONFIGURATION ==== 11 | 12 | dim peerAPI as String = "http://10.0.0.11:8088/API/" '-- peer vMix HTTP API endpoint (remote only) 13 | dim bridge1MixNum as String = "1" '-- mix number of bridge #1 (remote only) 14 | dim bridge2MixNum as String = "2" '-- mix number of bridge #2 (remote only) 15 | dim bridge1InputName as String = "BRIDGE1" '-- input name of bridge #1 (local and remote) 16 | dim bridge2InputName as String = "BRIDGE2" '-- input name of bridge #2 (local and remote) 17 | dim blankInputName as String = "BLANK" '-- input name of bkank content (remote only) 18 | dim timeSlice as Integer = 50 '-- time slice of processing interval (local only) 19 | dim debug as Boolean = false '-- wether to output debug messages (local only) 20 | 21 | '-- ==== STATE ==== 22 | 23 | '-- prepare XML DOM tree and load the current API state 24 | dim cfg as System.Xml.XmlDocument = new System.Xml.XmlDocument() 25 | dim xml as String = API.XML() 26 | cfg.LoadXml(xml) 27 | 28 | '-- pre-determine information of bridge inputs (locally) 29 | dim bridge1InputNum as String = cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & bridge1InputName & "']/@number").Value 30 | dim bridge1InputKey as String = cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & bridge1InputName & "']/@key").Value 31 | dim bridge2InputNum as String = cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & bridge2InputName & "']/@number").Value 32 | dim bridge2InputKey as String = cfg.SelectSingleNode("/vmix/inputs/input[@title = '" & bridge2InputName & "']/@key").Value 33 | 34 | '-- track which source input is in the bridges (remotely) 35 | dim bridge1SourceInputName as String = "" 36 | dim bridge2SourceInputName as String = "" 37 | 38 | '-- track which bridge is currently used (directly or indirectly) in preview and program (locally) 39 | dim bridgeInPreview as Integer = 0 40 | dim bridgeInProgram as Integer = 0 41 | 42 | '-- track last preview/program state (locally) 43 | dim inputInPreviewLast as String = "" 44 | dim inputInProgramLast as String = "" 45 | 46 | '-- track last preview/program state (remotely) 47 | dim inputInPreviewRemoteLast as String = "" 48 | dim inputInProgramRemoteLast as String = "" 49 | 50 | '-- ==== PROCESSING ==== 51 | 52 | '-- endless processing loop 53 | do while true 54 | '-- re-load the current API state 55 | xml = API.XML() 56 | cfg.LoadXml(xml) 57 | 58 | '-- determine what input is currently in preview and in program 59 | dim inputInPreviewNow as String = cfg.SelectSingleNode("/vmix/preview").InnerText 60 | dim inputInProgramNow as String = cfg.SelectSingleNode("/vmix/active").InnerText 61 | 62 | '-- react if a new input was placed into program 63 | if inputInProgramNow <> inputInProgramLast then 64 | '-- print detected change 65 | if debug then 66 | dim inputName as String = cfg.SelectSingleNode("/vmix/inputs/input[@number = '" & inputInProgramNow & "']/@title").Value 67 | Console.WriteLine("input-bridge: INFO: PROGRAM change detected: input=" & inputName) 68 | end if 69 | 70 | '-- transitively iterate through the program input tree 71 | '-- and find out whether and what bridge input is used 72 | bridgeInProgram = 0 73 | dim inputKey as String = cfg.SelectSingleNode("/vmix/inputs/input[@number = '" & inputInProgramNow & "']/@key").Value 74 | dim stack as System.Collections.Stack = new System.Collections.Stack() 75 | stack.Push(inputKey) 76 | do while stack.Count > 0 77 | inputKey = stack.Pop() 78 | if inputKey = bridge1InputKey then 79 | bridgeInProgram = 1 80 | exit do 81 | elseif inputKey = bridge2InputKey then 82 | bridgeInProgram = 2 83 | exit do 84 | else 85 | dim layers as XmlNodeList = cfg.SelectNodes("/vmix/inputs/input[@key = '" & inputKey & "']/overlay") 86 | for each layer as XmlNode in layers 87 | dim layerKey as String = layer.Attributes("key").Value 88 | stack.Push(layerKey) 89 | next 90 | end if 91 | loop 92 | if bridgeInProgram > 0 and debug then 93 | Console.WriteLine("input-bridge: INFO: found bridge #" & bridgeInProgram & " usage in PROGRAM input tree") 94 | end if 95 | end if 96 | 97 | '-- react if a new input was placed into preview 98 | if inputInPreviewNow <> inputInPreviewLast then 99 | '-- print detected change 100 | if debug then 101 | dim inputName as String = cfg.SelectSingleNode("/vmix/inputs/input[@number = '" & inputInPreviewNow & "']/@title").Value 102 | Console.WriteLine("input-bridge: INFO: PREVIEW change detected: input=" & inputName) 103 | end if 104 | 105 | '-- transitively iterate through the Preview input tree 106 | '-- in order to re-configure the bridge input which should be used 107 | '-- (i.e. the one which is not already used in the Program input tree) 108 | bridgeInPreview = 0 109 | dim inputKey as String = cfg.SelectSingleNode("/vmix/inputs/input[@number = '" & inputInPreviewNow & "']/@key").Value 110 | dim stack as System.Collections.Stack = new System.Collections.Stack() 111 | stack.Push(inputKey) 112 | do while stack.Count > 0 113 | inputKey = stack.Pop() 114 | 115 | '-- consistency check to ensure input (still or at all) exists 116 | '-- (notice: vMix sometimes has inconsistent states) 117 | dim inputs as XmlNodeList = cfg.SelectNodes("/vmix/inputs/input[@key = '" & inputKey & "']") 118 | if inputs.Count <> 1 then 119 | Console.WriteLine("input-bridge: WARNING: found inconsistent vMix API state: input key '" & inputKey & "' not existing (skipping)") 120 | continue do 121 | end if 122 | 123 | '-- determine input details 124 | dim inputName as String = cfg.SelectSingleNode("/vmix/inputs/input[@key = '" & inputKey & "']/@title").Value 125 | dim inputNum as String = cfg.SelectSingleNode("/vmix/inputs/input[@key = '" & inputKey & "']/@number").Value 126 | dim inputOverlays as XmlNodeList = cfg.SelectNodes("/vmix/inputs/input[@key = '" & inputKey & "']/overlay") 127 | 128 | '-- check for the special input configuration we act on 129 | dim inputChange as Boolean = False '-- whether to change the bridge input on remote side 130 | dim inputOverlay as Integer = -1 '-- the overlay of bridge input (layer number) 131 | for i as Integer = 0 to inputOverlays.Count - 1 132 | dim ovKey as String = inputOverlays.Item(i).Attributes("key").Value 133 | if ovKey = bridge1InputKey then 134 | inputOverlay = i 135 | if bridgeInProgram <> 1 or (bridgeInProgram = 1 and bridge1SourceInputName = inputName) then 136 | bridgeInPreview = 1 137 | else 138 | bridgeInPreview = 2 139 | inputChange = true 140 | end if 141 | elseif ovKey = bridge2InputKey then 142 | inputOverlay = i 143 | if bridgeInProgram <> 2 or (bridgeInProgram = 2 and bridge2SourceInputName = inputName) then 144 | bridgeInPreview = 2 145 | else 146 | bridgeInPreview = 1 147 | inputChange = true 148 | end if 149 | end if 150 | stack.Push(ovKey) 151 | next 152 | 153 | '-- react on special input configuration, if found 154 | if bridgeInPreview <> 0 and inputOverlay <> -1 then 155 | if debug then 156 | dim overlayName as String = cfg.SelectSingleNode("/vmix/inputs/input[@key = '" & inputOverlays.Item(inputOverlay).Attributes("key").Value & "']/@title").Value 157 | Console.WriteLine("input-bridge: INFO: target input '" & inputName & "': found setup: layer-" & (inputOverlay + 1).toString() & "=" & overlayName) 158 | end if 159 | 160 | '-- reconfigure the remote vMix instance to send input 161 | '-- (but do not re-configure if not necessary to not let program flash) 162 | if (bridgeInPreview = 1 and bridge1SourceInputName <> inputName) or (bridgeInPreview = 2 and bridge2SourceInputName <> inputName) then 163 | dim url as String = peerAPI & "?Function=ActiveInput&Mix=" 164 | if bridgeInPreview = 1 then 165 | if debug then 166 | Console.WriteLine("input-bridge: INFO: target input '" & inputName & "': route: remote-mix=" & bridge1MixNum & " local-bridge=" & bridge1InputName) 167 | end if 168 | url = url & bridge1MixNum & "&Input=" & inputName 169 | bridge1SourceInputName = inputName 170 | else 171 | if debug then 172 | Console.WriteLine("input-bridge: INFO: target input '" & inputName & "': route: remote-mix=" & bridge2MixNum & " local-bridge=" & bridge2InputName) 173 | end if 174 | url = url & bridge2MixNum & "&Input=" & inputName 175 | bridge2SourceInputName = inputName 176 | end if 177 | dim webClient as System.Net.WebClient = new System.Net.WebClient() 178 | webClient.DownloadString(url) 179 | end if 180 | 181 | '-- optionally re-configure local input layer to receive input 182 | if inputChange then 183 | if bridgeInPreview = 1 then 184 | if debug then 185 | Console.WriteLine("input-bridge: INFO: target input '" & inputName & "': reconfigure: layer-" & (inputOverlay + 1).toString() & "=" & bridge1InputName) 186 | end if 187 | API.Function("SetMultiViewOverlay", Input := inputNum, Value := (inputOverlay + 1).toString() & "," & bridge1InputNum) 188 | else 189 | if debug then 190 | Console.WriteLine("input-bridge: INFO: target input '" & inputName & "': reconfigure: layer-" & (inputOverlay + 1).toString() & "=" & bridge2InputName) 191 | end if 192 | API.Function("SetMultiViewOverlay", Input := inputNum, Value := (inputOverlay + 1).toString() & "," & bridge2InputNum) 193 | end if 194 | end if 195 | end if 196 | loop 197 | end if 198 | 199 | '-- determine whether to update remote program 200 | dim changeProgramToInput as String = "" 201 | if inputInProgramNow <> inputInProgramLast then 202 | if bridgeInProgram <> 0 then 203 | '-- input locally in program with a bridge, so reflect it remotely 204 | if bridgeInProgram = 1 and inputInProgramRemoteLast <> bridge1SourceInputName then 205 | changeProgramToInput = bridge1SourceInputName 206 | elseif bridgeInProgram = 2 and inputInProgramRemoteLast <> bridge2SourceInputName then 207 | changeProgramToInput = bridge2SourceInputName 208 | end if 209 | else 210 | '-- input locally in program without a bridge, so use empty input remotely 211 | if inputInProgramRemoteLast <> blankInputName then 212 | changeProgramToInput = blankInputName 213 | end if 214 | end if 215 | end if 216 | 217 | '-- determine whether to update remote preview 218 | dim changePreviewToInput as String = "" 219 | if inputInPreviewNow <> inputInPreviewLast then 220 | if bridgeInPreview <> 0 then 221 | '-- input locally in preview with a bridge, so reflect it remotely 222 | if bridgeInPreview = 1 and inputInPreviewRemoteLast <> bridge1SourceInputName then 223 | changePreviewToInput = bridge1SourceInputName 224 | elseif bridgeInPreview = 2 and inputInPreviewRemoteLast <> bridge2SourceInputName then 225 | changePreviewToInput = bridge2SourceInputName 226 | end if 227 | else 228 | '-- input locally in preview without a bridge, so use empty input remotely 229 | if inputInPreviewRemoteLast <> blankInputName then 230 | changePreviewToInput = blankInputName 231 | end if 232 | end if 233 | end if 234 | 235 | '-- update remote preview and program if a local change was done 236 | '-- (especially to keep tally lights in sync) 237 | if inputInPreviewRemoteLast = changeProgramToInput and inputInProgramRemoteLast = changePreviewToInput then 238 | '-- optimized all-in-one special case operation 239 | if debug then 240 | Console.WriteLine("input-bridge: INFO: remote: swapping preview '" & inputInPreviewRemoteLast & "' with program '" & inputInProgramRemoteLast & "'") 241 | end if 242 | dim url as String = peerAPI & "?Function=Cut" 243 | dim webClient as System.Net.WebClient = new System.Net.WebClient() 244 | webClient.DownloadString(url) 245 | inputInPreviewRemoteLast = changePreviewToInput 246 | inputInProgramRemoteLast = changeProgramToInput 247 | else 248 | if changeProgramToInput <> "" then 249 | '-- individual update operation 250 | if debug then 251 | Console.WriteLine("input-bridge: INFO: remote: switching program to '" & changeProgramToInput & "'") 252 | end if 253 | dim url as String = peerAPI & "?Function=CutDirect&Input=" & changeProgramToInput 254 | dim webClient as System.Net.WebClient = new System.Net.WebClient() 255 | webClient.DownloadString(url) 256 | inputInProgramRemoteLast = changeProgramToInput 257 | end if 258 | if changePreviewToInput <> "" then 259 | '-- individual update operation 260 | if debug then 261 | Console.WriteLine("input-bridge: INFO: remote: switching preview to '" & changePreviewToInput & "'") 262 | end if 263 | dim url as String = peerAPI & "?Function=PreviewInput&Input=" & changePreviewToInput 264 | dim webClient as System.Net.WebClient = new System.Net.WebClient() 265 | webClient.DownloadString(url) 266 | inputInPreviewRemoteLast = changePreviewToInput 267 | end if 268 | end if 269 | 270 | '-- finally remember new states 271 | if inputInProgramNow <> inputInProgramLast then 272 | inputInProgramLast = inputInProgramNow 273 | end if 274 | if inputInPreviewNow <> inputInPreviewLast then 275 | inputInPreviewLast = inputInPreviewNow 276 | end if 277 | 278 | '-- wait a little bit before next iteration 279 | sleep(timeSlice) 280 | loop 281 | 282 | -------------------------------------------------------------------------------- /input-mirror.md: -------------------------------------------------------------------------------- 1 | 2 | [Input-Mirror](input-mirror.vb) 3 | =============================== 4 | 5 | **Mirror Input Selection on vMix Slave** 6 | 7 | Allow one to mirror the current input preview/program selection 8 | on vMix slave instances in order to closely follow the vMix master instance. 9 | 10 | Problem 11 | ------- 12 | 13 | Sometimes a single vMix instance is not enough. For instance, when on a 14 | "playout" machine two incoming SRT streams should each be forwarded to 15 | multiple outgoing RTMP endpoints. The solution is to run vMix twice on 16 | the same machine: once for the first SRT stream and its RTMP endpoints 17 | and a once again for the second SRT stream and its RTMP endpoints. 18 | 19 | Solution 20 | -------- 21 | 22 | For a "playout" machine, one has to be able to switch between the 23 | incoming SRT streams and various "placeholder" inputs ("before-event", 24 | "after-event", "failure", "emergency", etc). As both vMix instances 25 | have the same set of inputs, one can simply mirror the inputs in 26 | preview/program in the vMix master instance 1:1 on the vMix slave 27 | instance. 28 | 29 | Usage 30 | ----- 31 | 32 | Suppose you you want to mirror inputs from the vMix 33 | master instance to a vMix slave instance whose 34 | HTTP API is under port 8188 (instead of the regular 35 | port 8088 of the vMix master instance). 36 | Then setup the vMix master instance as following: 37 | 38 | - add the `input-mirror` script and adjust its configuration to: 39 | 40 | ``` 41 | dim peerAPI as String = "http://127.0.0.1:8188/API/" 42 | dim timeSlice as Integer = 50 43 | dim debug as Boolean = true 44 | ``` 45 | 46 | Finally, start the script `input-mirror` and bring any of the inputs 47 | into preview/program. 48 | 49 | -------------------------------------------------------------------------------- /input-mirror.vb: -------------------------------------------------------------------------------- 1 | '-- 2 | '-- input-mirror.vb -- vMix script for Mirroring Input Selection on vMix Slave Instance 3 | '-- Copyright (c) 2023-2023 Dr. Ralf S. Engelschall <rse@engelschall.com> 4 | '-- Distributed under MIT license <https://spdx.org/licenses/MIT.html> 5 | '-- 6 | '-- Language: VB.NET 2.0 (vMix 4K/Pro flavor) 7 | '-- Version: 0.9.1 (2023-06-30) 8 | '-- 9 | 10 | '-- ==== CONFIGURATION ==== 11 | 12 | dim peerAPI as String = "http://127.0.0.1:8188/API/" '-- peer vMix HTTP API endpoint 13 | dim timeSlice as Integer = 50 '-- time slice of processing interval 14 | dim debug as Boolean = false '-- whether to output debug messages 15 | 16 | '-- ==== STATE ==== 17 | 18 | '-- prepare XML DOM tree and load the current API state 19 | dim cfg as System.Xml.XmlDocument = new System.Xml.XmlDocument() 20 | dim xml as String = API.XML() 21 | cfg.LoadXml(xml) 22 | 23 | '-- track last preview/program state (locally) 24 | dim inputInPreviewLast as String = "" 25 | dim inputInProgramLast as String = "" 26 | 27 | '-- track last preview/program state (remotely) 28 | dim inputInPreviewRemoteLast as String = "" 29 | dim inputInProgramRemoteLast as String = "" 30 | 31 | '-- ==== PROCESSING ==== 32 | 33 | '-- endless processing loop 34 | do while true 35 | '-- re-load the current API state 36 | xml = API.XML() 37 | cfg.LoadXml(xml) 38 | 39 | '-- determine what input is currently in preview and in program 40 | dim inputInPreviewNowNum as String = cfg.SelectSingleNode("/vmix/preview").InnerText 41 | dim inputInProgramNowNum as String = cfg.SelectSingleNode("/vmix/active").InnerText 42 | dim inputInPreviewNow as String = cfg.SelectSingleNode("/vmix/inputs/input[@number = '" & inputInPreviewNowNum & "']/@title").Value 43 | dim inputInProgramNow as String = cfg.SelectSingleNode("/vmix/inputs/input[@number = '" & inputInProgramNowNum & "']/@title").Value 44 | 45 | '-- detect if a new input was placed into program 46 | dim changeProgramToInput as String = "" 47 | if inputInProgramNow <> inputInProgramLast then 48 | '-- print detected change 49 | if debug then 50 | Console.WriteLine("input-mirror: INFO: PROGRAM change detected: input=" & inputInProgramNow) 51 | end if 52 | changeProgramToInput = inputInProgramNow 53 | end if 54 | 55 | '-- detect if a new input was placed into preview 56 | dim changePreviewToInput as String = "" 57 | if inputInPreviewNow <> inputInPreviewLast then 58 | '-- print detected change 59 | if debug then 60 | Console.WriteLine("input-mirror: INFO: PREVIEW change detected: input=" & inputInPreviewNow) 61 | end if 62 | changePreviewToInput = inputInPreviewNow 63 | end if 64 | 65 | '-- update remote preview and program if a local change was done 66 | if inputInPreviewRemoteLast = changeProgramToInput and inputInProgramRemoteLast = changePreviewToInput then 67 | '-- optimized all-in-one special case operation 68 | if debug then 69 | Console.WriteLine("input-mirror: INFO: remote: swapping preview '" & inputInPreviewRemoteLast & "' with program '" & inputInProgramRemoteLast & "'") 70 | end if 71 | dim url as String = peerAPI & "?Function=Cut" 72 | dim webClient as System.Net.WebClient = new System.Net.WebClient() 73 | webClient.DownloadString(url) 74 | inputInPreviewRemoteLast = changePreviewToInput 75 | inputInProgramRemoteLast = changeProgramToInput 76 | else 77 | if changeProgramToInput <> "" then 78 | '-- individual update operation 79 | if debug then 80 | Console.WriteLine("input-mirror: INFO: remote: switching program to '" & changeProgramToInput & "'") 81 | end if 82 | dim url as String = peerAPI & "?Function=CutDirect&Input=" & changeProgramToInput 83 | dim webClient as System.Net.WebClient = new System.Net.WebClient() 84 | webClient.DownloadString(url) 85 | inputInProgramRemoteLast = changeProgramToInput 86 | end if 87 | if changePreviewToInput <> "" then 88 | '-- individual update operation 89 | if debug then 90 | Console.WriteLine("input-mirror: INFO: remote: switching preview to '" & changePreviewToInput & "'") 91 | end if 92 | dim url as String = peerAPI & "?Function=PreviewInput&Input=" & changePreviewToInput 93 | dim webClient as System.Net.WebClient = new System.Net.WebClient() 94 | webClient.DownloadString(url) 95 | inputInPreviewRemoteLast = changePreviewToInput 96 | end if 97 | end if 98 | 99 | '-- finally remember new states 100 | if inputInProgramNow <> inputInProgramLast then 101 | inputInProgramLast = inputInProgramNow 102 | end if 103 | if inputInPreviewNow <> inputInPreviewLast then 104 | inputInPreviewLast = inputInPreviewNow 105 | end if 106 | 107 | '-- wait a little bit before next iteration 108 | sleep(timeSlice) 109 | loop 110 | 111 | -------------------------------------------------------------------------------- /multiview-overlay.md: -------------------------------------------------------------------------------- 1 | 2 | [Multiview-Overlay](multiview-overlay.vb) 3 | ========================================= 4 | 5 | **Update Custom Multiview Overlays** 6 | 7 | Allow one to select the corresponding page of a multi-camera multiview 8 | and update its preview/program overlays. 9 | 10 | Problem 11 | ------- 12 | 13 | The vMix Multiview facility is rather limited, as it neither allows 14 | multiple multiviews nor fully custom multiview layouts. For a usual 15 | greenscreen-based setup with N physical cameras and M virtual 16 | views/angles, one usually want N multiviews and each one should show M 17 | views/angles. 18 | 19 | Solution 20 | -------- 21 | 22 | As as long the number of views/angles M is lower than 8, create N 23 | multiview-like inputs, each with M views/angles on layers the layers 24 | 1-M and on two additional layers place two `Title` type inputs. Each 25 | Nx2 `Title` type inputs then should have M transitions "PageX", 26 | corresponding to the view/angle input to be on preview/program. Then 27 | use this script to track changes on preview/program and select the 28 | corresponding transition "Pagex" on the `Title` inputs. 29 | 30 | Usage 31 | ----- 32 | 33 | Suppose we have the following setup: 34 | 35 | - 4 cameras and 7 views/angles and the view/angle inputs 36 | are named `VPTZ - CAMX-Y` where X is 1-4 and Y is `C-L` (closeup-left), 37 | `C-C` (closeup-center), `C-R` (closeup-right), `F-L` (figure-left), 38 | `F-C` (figure-center), `F-R` (figure-right), and `W-C` (wide-center). 39 | 40 | - `Title` inputs named `MULTIVIEW-OV-PREVIEW - CAMX` 41 | and `MULTIVIEW-OV-PROGRAM - CAMX` per camera, each holding 42 | M overlays for preview/program. 43 | 44 | - 4 custom multiview inputs named `MULTIVIEW - CAMX` corresponding 45 | to the 4 cameras and each showing the 7 views/angles. 46 | 47 | - 1 `Mix` type input #3 (1-3 for the custom mixers, 0 is the 48 | main mixer) which is assigned for the fullscreen output under 49 | `Settings`/`Outputs ...`/`Fullscreen` which receives the 50 | 4 custom multiview inputs. 51 | 52 | Then configure the `multiview-overlay` script with: 53 | 54 | `` ` 55 | dim numberOfCams as Integer = 4 56 | dim angleInputPrefix as String = "VPTZ - CAM" 57 | dim angleInputPostfixes as String() = { "C-L", "C-C", "C-R", "F-L", "F-C", "F-R", "W-C" } 58 | dim titlePreviewInputPrefix as String = "MULTIVIEW-OV-PREVIEW - CAM" 59 | dim titleProgramInputPrefix as String = "MULTIVIEW-OV-PROGRAM - CAM" 60 | dim multiviewInputPrefix as String = "MULTIVIEW - CAM" 61 | dim multiviewOutputId as String = "3" 62 | dim timeSlice as Integer = 50 63 | dim debug as Boolean = true 64 | ``` 65 | 66 | -------------------------------------------------------------------------------- /multiview-overlay.vb: -------------------------------------------------------------------------------- 1 | '-- 2 | '-- multiview-overlay.vb -- vMix script for Updating Custom Multiview Overlays 3 | '-- Copyright (c) 2022-2023 Dr. Ralf S. Engelschall <rse@engelschall.com> 4 | '-- Distributed under MIT license <https://spdx.org/licenses/MIT.html> 5 | '-- 6 | '-- Language: VB.NET 2.0 (vMix 4K/Pro flavor) 7 | '-- Version: 0.9.4 (2025-02-17) 8 | '-- 9 | 10 | '-- CONFIGURATION 11 | dim numberOfCams as Integer = 4 12 | dim angleInputPrefix as String = "VPTZ - CAM" 13 | dim angleInputPrefixPHYS as String = "PTZ - CAM" 14 | dim angleInputPostfixes as String() = { "C-L", "C-C", "C-R", "F-L", "F-C", "F-R", "W-C" } 15 | dim titlePreviewInputPrefix as String = "MULTIVIEW-OV-PREVIEW - CAM" 16 | dim titleProgramInputPrefix as String = "MULTIVIEW-OV-PROGRAM - CAM" 17 | dim multiviewInputPrefix as String = "MULTIVIEW - CAM" 18 | dim multiviewInputPHYS as String = "MULTIVIEW - CAMx" 19 | dim multiviewInputNOCAM as String = "MULTIVIEW - NOCAM" 20 | dim multiviewOutputId as String = "3" 21 | dim timeSlice as Integer = 50 22 | dim debug as Boolean = false 23 | 24 | '-- prepare XML DOM tree and load the current API state 25 | dim cfg as new System.Xml.XmlDocument 26 | dim xml as String = API.XML() 27 | cfg.LoadXml(xml) 28 | 29 | '-- keep internal state of active preview/program 30 | dim lastInPreview as String = "" 31 | dim lastInProgram as String = "" 32 | 33 | '-- keep internal state of still cleared preview/program overlay 34 | dim clearedPreview as Boolean() = new Boolean(numberOfCams) {} 35 | dim clearedProgram as Boolean() = new Boolean(numberOfCams) {} 36 | for i as Integer = 0 to numberOfCams - 1 37 | clearedPreview(i) = false 38 | clearedProgram(i) = false 39 | next 40 | 41 | '-- keep internal state of current preview camera 42 | dim lastPreviewCam as Integer = 0 43 | 44 | '-- keep internal state of current mode 45 | dim lastMode as String = "" 46 | 47 | '-- endless loop 48 | do while true 49 | '-- re-load the current API state 50 | xml = API.XML() 51 | cfg.LoadXml(xml) 52 | 53 | '-- determine what is currently in preview 54 | dim nowInPreview as String = cfg.SelectSingleNode("/vmix/preview").InnerText 55 | dim nowInProgram as String = cfg.SelectSingleNode("/vmix/active").InnerText 56 | 57 | '-- react if a new input was placed into preview 58 | if nowInPreview <> lastInPreview then 59 | lastInPreview = nowInPreview 60 | dim inputName as String = cfg.SelectSingleNode("/vmix/inputs/input[@number = '" & nowInPreview & "']/@title").Value 61 | if debug then 62 | Console.WriteLine("multiview-update: INFO: PREVIEW change detected: input=" & inputName) 63 | end if 64 | dim changed as Boolean = false 65 | if inputName.length > angleInputPrefixPHYS.length then 66 | if inputName.substring(0, angleInputPrefixPHYS.length) = angleInputPrefixPHYS then 67 | if lastMode <> "PHYS" then 68 | API.Function("PreviewInput", Input := multiviewInputPHYS, Mix := multiviewOutputId) 69 | API.Function("ActiveInput", Input := multiviewInputPHYS, Mix := multiviewOutputId) 70 | lastPreviewCam = 0 71 | lastMode = "PHYS" 72 | end if 73 | end if 74 | end if 75 | if inputName.length > angleInputPrefix.length then 76 | if inputName.substring(0, angleInputPrefix.length) = angleInputPrefix then 77 | lastMode = "VIRT" 78 | dim cam as Integer = Convert.ToInt32(inputName.substring(angleInputPrefix.length, 1)) 79 | dim angle as String = inputName.substring(angleInputPrefix.length + 2) 80 | dim idx as Integer = Array.IndexOf(angleInputPostfixes, angle) 81 | if idx >= 0 then 82 | if debug then 83 | Console.WriteLine("multiview-update: INFO: updating multiview PREVIEW overlay of CAM" & cam) 84 | end if 85 | API.Function("TitleBeginAnimation", Input := titlePreviewInputPrefix & cam, Value := "Page" & (idx + 1).toString()) 86 | clearedPreview(cam - 1) = false 87 | for i as Integer = 1 to numberOfCams 88 | if i <> cam then 89 | if clearedPreview(i - 1) = false then 90 | if debug then 91 | Console.WriteLine("multiview-update: INFO: clearing multiview PREVIEW overlay of CAM" & i.toString()) 92 | end if 93 | API.Function("TitleBeginAnimation", Input := titlePreviewInputPrefix & i.toString(), Value := "TransitionOut") 94 | clearedPreview(i - 1) = true 95 | end if 96 | end if 97 | next 98 | if lastPreviewCam <> cam then 99 | lastPreviewCam = cam 100 | if debug then 101 | Console.WriteLine("multiview-update: INFO: switching MULTIVIEW to CAM" & cam.toString()) 102 | end if 103 | API.Function("PreviewInput", Input := multiviewInputPrefix & cam.toString(), Mix := multiviewOutputId) 104 | API.Function("ActiveInput", Input := multiviewInputPrefix & cam.toString(), Mix := multiviewOutputId) 105 | end if 106 | changed = true 107 | end if 108 | end if 109 | end if 110 | if not changed then 111 | for i as Integer = 1 to numberOfCams 112 | if clearedPreview(i - 1) = false then 113 | if debug then 114 | Console.WriteLine("multiview-update: INFO: clearing multiview PREVIEW overlay of CAM" & i.toString()) 115 | end if 116 | API.Function("TitleBeginAnimation", Input := titlePreviewInputPrefix & i.toString(), Value := "TransitionOut") 117 | clearedPreview(i - 1) = true 118 | end if 119 | next 120 | API.Function("PreviewInput", Input := multiviewInputNOCAM, Mix := multiviewOutputId) 121 | API.Function("ActiveInput", Input := multiviewInputNOCAM, Mix := multiviewOutputId) 122 | lastPreviewCam = 0 123 | end if 124 | end if 125 | 126 | '-- react if a new input was placed into program 127 | if nowInProgram <> lastInProgram then 128 | lastInProgram = nowInProgram 129 | dim inputName as String = cfg.SelectSingleNode("/vmix/inputs/input[@number = '" & nowInProgram & "']/@title").Value 130 | if debug then 131 | Console.WriteLine("multiview-update: INFO: PROGRAM change detected: input=" & inputName) 132 | end if 133 | dim changed as Boolean = false 134 | if inputName.length > angleInputPrefix.length then 135 | if inputName.substring(0, angleInputPrefix.length) = angleInputPrefix then 136 | dim cam as Integer = Convert.ToInt32(inputName.substring(angleInputPrefix.length, 1)) 137 | dim angle as String = inputName.substring(angleInputPrefix.length + 2) 138 | dim idx as Integer = Array.IndexOf(angleInputPostfixes, angle) 139 | if idx >= 0 then 140 | if debug then 141 | Console.WriteLine("multiview-update: INFO: updating multiview PROGRAM overlay of CAM" & cam) 142 | end if 143 | API.Function("TitleBeginAnimation", Input := titleProgramInputPrefix & cam, Value := "Page" & (idx + 1).toString()) 144 | clearedProgram(cam - 1) = false 145 | for i as Integer = 1 to numberOfCams 146 | if i <> cam then 147 | if clearedProgram(i - 1) = false then 148 | if debug then 149 | Console.WriteLine("multiview-update: INFO: clearing multiview PROGRAM overlay of CAM" & i.toString()) 150 | end if 151 | API.Function("TitleBeginAnimation", Input := titleProgramInputPrefix & i.toString(), Value := "TransitionOut") 152 | clearedProgram(i - 1) = true 153 | end if 154 | end if 155 | next 156 | changed = true 157 | end if 158 | end if 159 | end if 160 | if not changed then 161 | for i as Integer = 1 to numberOfCams 162 | if clearedProgram(i - 1) = false then 163 | if debug then 164 | Console.WriteLine("multiview-update: INFO: clearing multiview PREVIEW overlay of CAM" & i.toString()) 165 | end if 166 | API.Function("TitleBeginAnimation", Input := titleProgramInputPrefix & i.toString(), Value := "TransitionOut") 167 | clearedProgram(i - 1) = true 168 | end if 169 | next 170 | end if 171 | end if 172 | 173 | '-- wait a little bit before next iteration 174 | sleep(timeSlice) 175 | loop 176 | 177 | -------------------------------------------------------------------------------- /ndi-studio-monitor.md: -------------------------------------------------------------------------------- 1 | 2 | [NDI-Studio-Monitor](ndi-studio-monitor.vb) 3 | =========================================== 4 | 5 | **Reconfigure NewTek NDI Studio Monitor** 6 | 7 | Allow vMix to reconfigure the NDI source displayed in a (remote) NewTek 8 | NDI Studio Monitor instance. 9 | 10 | Problem 11 | ------- 12 | 13 | The NewTek NDI Studio Monitor is a great way to display an NDI stream. 14 | It even supports Picture-in-Picture (PiP). Unfortunately, it usually 15 | runs on a different computer (usually a mini-PC attached to a TV) than 16 | vMix and during a production might have to switch its NDI stream (in 17 | concert with vMix scene inputs or a shortcut used by the operator). 18 | 19 | Solution 20 | -------- 21 | 22 | The NewTek NDI Studio Monitor can be 23 | [remote controlled](https://github.com/bitfocus/companion-module-newtek-ndistudiomonitor/blob/master/HELP.md) 24 | via a small HTTP interface. We call this interface directly from within vMix in 25 | order to reconfigure the shown NDI stream. 26 | 27 | Usage 28 | ----- 29 | 30 | Configure a vMix Shortcut with: 31 | 32 | <key> SetDynamicValue1 <ip-address>:<port> 33 | <key> SetDynamicValue1 <ndi-source-name> 34 | <key> ScriptStart ndi-studio-monitor 35 | 36 | -------------------------------------------------------------------------------- /ndi-studio-monitor.vb: -------------------------------------------------------------------------------- 1 | '-- 2 | '-- ndi-studio-monitor.vb -- vMix script for NDI Studio Monitor stream configuration 3 | '-- Copyright (c) 2022-2023 Dr. Ralf S. Engelschall <rse@engelschall.com> 4 | '-- Distributed under MIT license <https://spdx.org/licenses/MIT.html> 5 | '-- 6 | '-- Language: VB.NET 2.0 (vMix 4K/Pro flavor) 7 | '-- Version: 0.9.1 (2022-09-11) 8 | '-- 9 | 10 | '-- fetch current vMix API status 11 | dim xml as string = API.XML() 12 | dim cfg as new System.Xml.XmlDocument 13 | cfg.LoadXml(xml) 14 | 15 | '-- determine parameters 16 | dim monitorIP as String = cfg.selectSingleNode("//dynamic/value1").InnerText 17 | dim monitorSource as String = cfg.selectSingleNode("//dynamic/value2").InnerText 18 | 19 | '-- prepare re-configuration URL 20 | dim monitorURL as String = "http://" & monitorIP & "/v1/configuration" 21 | 22 | '-- prepare re-configuration JSON payload 23 | dim utf8WithoutBOM as new System.Text.UTF8Encoding(false) 24 | dim payloadJSON as String = "{""version"":1,""NDI_source"":""" & monitorSource & """}" 25 | dim payloadBytes as Byte() = utf8WithoutBOM.GetBytes(payloadJSON) 26 | 27 | '-- initiate the HTTP POST request to NDI Studio Monitor 28 | dim client as System.Net.WebClient = new System.Net.WebClient() 29 | client.Encoding = System.Text.Encoding.UTF8 30 | client.Headers.Add("Content-Type", "application/x-www-form-urlencoded") 31 | client.Headers.Add("Content-Length", CStr(payloadBytes.Length)) 32 | client.UploadString(monitorURL, payloadBytes) 33 | 34 | -------------------------------------------------------------------------------- /recording-log.md: -------------------------------------------------------------------------------- 1 | 2 | [Recording-Log](recording-log.vb) 3 | ================================= 4 | 5 | **Logging Recording States** 6 | 7 | Logs the start and stop states of Recording and MultiCorder and can add 8 | a special marking log entry for bookkeeping special points of interest 9 | during a recording. 10 | 11 | Problem 12 | ------- 13 | 14 | vMix has the `WriteDurationToRecordingLog` which can be used for adding 15 | a marker to the regular vMix logfile with the help of a shortcut. 16 | Unfortunately, this works only for regular Recording and not for 17 | MultiCorder and one cannot interactively enter a particular marker 18 | message. 19 | 20 | Solution 21 | -------- 22 | 23 | An own logging facility detects the start and stop states of Recording 24 | and MultiCorder in parallel and in addition provides a way to add a 25 | special marking log entry (through a shortcut) for bookkeeping special 26 | points of interest during a recording. The marker log entry is extended 27 | with a custom message which is interactively given by the vMix operator. 28 | 29 | The following log is the result of starting Recording, pressing the 30 | marker shortcut two times in sequence, then starting MultiCorder, 31 | pressing the marker shortcut two times in sequence once again, then 32 | stopping MultiCorder, pressing the marker shortcut two times in sequence 33 | once again, and finally also stopping Recording: 34 | 35 | ```txt 36 | [2022-07-04 23:59:39.184] RECORDING started 37 | [2022-07-04 23:59:41.130] RECORDING marked (position: 00:00:01.946): slip of the tongue 38 | [2022-07-04 23:59:42.555] RECORDING marked (position: 00:00:03.370): slip of the tongue 39 | [2022-07-04 23:59:44.514] MULTICORDER started 40 | [2022-07-04 23:59:47.101] RECORDING marked (position: 00:00:07.916): slip of the tongue 41 | [2022-07-04 23:59:47.101] MULTICORDER marked (position: 00:00:02.586): slip of the tongue 42 | [2022-07-04 23:59:48.558] RECORDING marked (position: 00:00:09.373): problem with slides 43 | [2022-07-04 23:59:48.558] MULTICORDER marked (position: 00:00:04.043): slip of the tongue 44 | [2022-07-04 23:59:52.456] MULTICORDER ended (duration: 00:00:07.942) 45 | [2022-07-04 23:59:53.518] RECORDING marked (position: 00:00:14.333): slip of the tongue 46 | [2022-07-04 23:59:54.282] RECORDING marked (position: 00:00:15.098): slip of the tongue 47 | [2022-07-04 23:59:56.820] RECORDING ended (duration: 00:00:17.636) 48 | ``` 49 | 50 | Usage 51 | ----- 52 | 53 | Configure vMix Shortcuts for marking with: 54 | 55 | <keyX> SetDynamicValue3 recording-marker-simple 56 | <keyY> SetDynamicValue3 recording-marker-custom 57 | 58 | -------------------------------------------------------------------------------- /recording-log.vb: -------------------------------------------------------------------------------- 1 | '-- 2 | '-- recording-log.vb -- vMix script for logging recording states 3 | '-- Copyright (c) 2022-2023 Dr. Ralf S. Engelschall <rse@engelschall.com> 4 | '-- Distributed under MIT license <https://spdx.org/licenses/MIT.html> 5 | '-- 6 | '-- Language: VB.NET 2.0 (vMix 4K/Pro flavor) 7 | '-- Version: 1.0.2 (2022-07-05) 8 | '-- 9 | 10 | '-- CONFIGURATION 11 | dim markerDynamicVariable as String = "3" 12 | dim markerStandardText as String = "slip of the tongue" 13 | dim timeSlice as Integer = 33 ' = 1000ms / 30fps 14 | 15 | '-- prepare XML DOM tree and load the current API state 16 | dim cfg as new System.Xml.XmlDocument 17 | dim xml as String = API.XML() 18 | cfg.LoadXml(xml) 19 | 20 | '-- determine logfile location 21 | dim presetNode as System.Xml.XmlNode = cfg.SelectSingleNode("/vmix/preset") 22 | if presetNode is Nothing then 23 | Console.WriteLine("recording-log: ERROR: You are running on a still UNSAVED vMix preset!") 24 | Console.WriteLine("recording-log: ERROR: Save your preset at least once, please.") 25 | return 26 | end if 27 | dim logFile as String = presetNode.InnerText.Replace(".vmix", ".log") 28 | 29 | '-- keep internal state 30 | dim recordingSince as DateTime = nothing 31 | dim multicordingSince as DateTime = nothing 32 | 33 | '-- endless loop 34 | do while true 35 | '-- re-load the current API state 36 | xml = API.XML() 37 | cfg.LoadXml(xml) 38 | 39 | '-- start fresh log 40 | dim log as new System.Collections.Generic.List(of String) 41 | 42 | '-- log recording state changes 43 | dim isRecording as Boolean = Boolean.parse(cfg.SelectSingleNode("/vmix/recording").InnerText) 44 | if recordingSince = nothing and isRecording then 45 | log.Add("RECORDING started") 46 | recordingSince = DateTime.Now 47 | elseif recordingSince <> nothing and not isRecording then 48 | dim diff as System.TimeSpan = DateTime.Now.Subtract(recordingSince) 49 | dim duration as DateTime = (new DateTime(0)).Add(diff) 50 | log.Add("RECORDING ended (duration: " & duration.ToString("HH:mm:ss.fff") & ")") 51 | recordingSince = nothing 52 | end if 53 | 54 | '-- log multicorder state changes 55 | dim isMulticording as Boolean = Boolean.parse(cfg.SelectSingleNode("/vmix/multiCorder").InnerText) 56 | if multicordingSince = nothing and isMulticording then 57 | log.Add("MULTICORDER started") 58 | multicordingSince = DateTime.Now 59 | elseif multicordingSince <> nothing and not isMulticording then 60 | dim diff as System.TimeSpan = DateTime.Now.Subtract(multicordingSince) 61 | dim duration as DateTime = (new DateTime(0)).Add(diff) 62 | log.Add("MULTICORDER ended (duration: " & duration.ToString("HH:mm:ss.fff") & ")") 63 | multicordingSince = nothing 64 | end if 65 | 66 | '-- log marker trigger 67 | dim nowVariableState as String = cfg.selectSingleNode("/vmix/dynamic/value" & markerDynamicVariable).InnerText 68 | if nowVariableState = "recording-marker-simple" or nowVariableState = "recording-marker-custom" then 69 | API.Function("SetDynamicValue" & markerDynamicVariable, Value := "") 70 | 71 | '-- determine duration(s) (in advance because of potentially interactive dialog) 72 | dim durationRecording as String = "" 73 | dim durationMulticording as String = "" 74 | if recordingSince <> nothing and isRecording then 75 | dim diff as System.TimeSpan = DateTime.Now.Subtract(recordingSince) 76 | dim duration as DateTime = (new DateTime(0)).Add(diff) 77 | durationRecording = duration.ToString("HH:mm:ss.fff") 78 | end if 79 | if multicordingSince <> nothing and isMulticording then 80 | dim diff as System.TimeSpan = DateTime.Now.Subtract(multicordingSince) 81 | dim duration as DateTime = (new DateTime(0)).Add(diff) 82 | durationMulticording = duration.ToString("HH:mm:ss.fff") 83 | end if 84 | 85 | '-- create log entry(s) 86 | if durationRecording <> "" or durationMulticording <> "" then 87 | dim msg as String = markerStandardText 88 | 89 | '-- if a custom marker message is requestd, interactively ask the user for it 90 | '-- (NOTICE: we have to use WSH, as we cannot open an input dialog directly from within vMix VB.Net) 91 | if nowVariableState = "recording-marker-custom" then 92 | 93 | '-- determine two temporary file paths 94 | dim tempfile1 as String = System.IO.Path.GetTempFileName() 95 | dim tempfile2 as String = System.IO.Path.GetTempFileName() 96 | 97 | '-- create companion WSH script 98 | dim crlf as String = Environment.NewLine 99 | dim script as String = "" 100 | script = script & "file = WScript.Arguments.Item(0)" & crlf 101 | script = script & "text = WScript.Arguments.Item(1)" & crlf 102 | script = script & "text = InputBox(""What is your textual annotation message to use for the recording marker?"", ""vMix: Recording-Log: Marker"", text)" & crlf 103 | script = script & "if text <> """" then" & crlf 104 | script = script & " set fso = CreateObject(""Scripting.FileSystemObject"")" & crlf 105 | script = script & " set out = fso.OpenTextFile(file, 8, true, -1)" & crlf 106 | script = script & " out.Write(text)" & crlf 107 | script = script & " out.Close()" & crlf 108 | script = script & "end if" & crlf 109 | System.IO.File.WriteAllText(tempfile1, script) 110 | 111 | '-- execute WSH in own process 112 | dim app as new ProcessStartInfo() 113 | app.FileName = "wscript.exe" 114 | app.Arguments = "/e:vbscript """ & tempfile1 & """ """ & tempfile2 & """ """ & msg & """" 115 | app.UseShellExecute = true 116 | app.CreateNoWindow = true 117 | app.WindowStyle = ProcessWindowStyle.Normal 118 | dim proc as Process = Process.Start(app) 119 | proc.WaitForExit() 120 | 121 | '-- read results form process 122 | '-- (NOTICE: we cannot use stdout here as WScript doesn't support stdout and CScript always opens a Terminal) 123 | dim utf8 as System.Text.Encoding = new System.Text.UTF8Encoding(true) 124 | msg = System.IO.File.ReadAllText(tempfile2, utf8) 125 | 126 | '-- cleanup temporary files 127 | System.IO.File.Delete(tempfile1) 128 | System.IO.File.Delete(tempfile2) 129 | end if 130 | 131 | '-- create log entries 132 | if durationRecording <> "" then 133 | log.Add("RECORDING marked (position: " & durationRecording & "): " & msg) 134 | end if 135 | if multicordingSince <> nothing and isMulticording then 136 | log.Add("MULTICORDER marked (position: " & durationMulticording & "): " & msg) 137 | end if 138 | end if 139 | end if 140 | 141 | '-- write log entries 142 | if log.Count > 0 then 143 | dim timestamp as String = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") 144 | dim utf8WithoutBOM as System.Text.Encoding = new System.Text.UTF8Encoding(false) 145 | for each entry as String in log 146 | dim msg as String = "[" & timestamp & "] " & entry & Environment.NewLine 147 | System.IO.File.AppendAllText(logFile, msg, utf8WithoutBOM) 148 | next 149 | 150 | '-- signal new log entries with a sound 151 | My.Computer.Audio.PlaySystemSound(Media.SystemSounds.Exclamation) 152 | end if 153 | 154 | '-- wait a little bit before next iteration 155 | sleep(timeSlice) 156 | loop 157 | 158 | -------------------------------------------------------------------------------- /remoteshowcontrol-loop.md: -------------------------------------------------------------------------------- 1 | 2 | [RemoteShowControl-Loop](remoteshowcontrol-loop.vb) 3 | =================================================== 4 | 5 | **Continuously Control Irisdown RemoteShowControl** 6 | 7 | Automatically and continuously control a remote 8 | PowerPoint slide-deck with the help of its [Irisdown 9 | RemoteShowControl](https://www.irisdown.co.uk/rsc.html) plugin, 10 | based on vMix input name information. 11 | 12 | Problem 13 | ------- 14 | 15 | When ingesting the screen or HDMI captured output of a PowerPoint 16 | slide-deck into vMix scene inputs one has to ensure that the correct 17 | slide is selected within PowerPoint. Often, PowerPoint is running on a 18 | different computer than vMix, too. 19 | 20 | Solution 21 | -------- 22 | 23 | The PowerPoint plugin [Irisdown 24 | RemoteShowControl](https://www.irisdown.co.uk/rsc.html) is installed 25 | into PowerPoint and listens on a TCP port for control commands. 26 | 27 | This script continuously observes which vMix input is in PREVIEW and if 28 | its name contains the string `[rsc:N]`, this script remotely instructs 29 | PowerPoint, through its installed Irisdown Remote Show Control plugin, 30 | to go to the particular slide number `N`. 31 | 32 | Usage 33 | ----- 34 | 35 | On a scene input embed the screen or HDMI captured slide content as a 36 | layer and add to the end of the input name e.g. `[rsc:42]`. Whenever 37 | this scene input comes into PREVIEW, PowerPoint switches to slide number 38 | 42 and as a result, the input scene in vMix shows the correct slide 39 | before being cut into PROGRAM. 40 | 41 | -------------------------------------------------------------------------------- /remoteshowcontrol-loop.vb: -------------------------------------------------------------------------------- 1 | '-- 2 | '-- remoteshowcontrol-loop.vb -- vMix script for RemoteShowControl following inputs 3 | '-- Copyright (c) 2022-2023 Dr. Ralf S. Engelschall <rse@engelschall.com> 4 | '-- Distributed under MIT license <https://spdx.org/licenses/MIT.html> 5 | '-- 6 | '-- Language: VB.NET 2.0 (vMix 4K/Pro flavor) 7 | '-- Version: 1.0.0 (2022-03-13) 8 | '-- 9 | 10 | '-- CONFIGURATION 11 | dim clientIP as String = "127.0.0.1" 12 | dim clientPort as String = "61001" 13 | 14 | '-- keep internal state 15 | dim lastInPreview as String = "" 16 | dim lastSlideSelect as String = "" 17 | 18 | '-- prepare XML DOM tree 19 | dim cfg as new System.Xml.XmlDocument 20 | 21 | '-- endless loop 22 | do while true 23 | '-- load the current API state 24 | dim xml as string = API.XML() 25 | cfg.LoadXml(xml) 26 | 27 | '-- only react if a new input was placed into the preview 28 | dim nowInPreview as String = cfg.SelectSingleNode("//preview").InnerText 29 | if nowInPreview <> lastInPreview then 30 | lastInPreview = nowInPreview 31 | 32 | '-- only react if input title contains "[rsc:N]" 33 | dim title as string = (cfg.SelectSingleNode("//inputs/input[@number = '" & nowInPreview & "']/@title").Value) 34 | dim titleMatches as Boolean = (title like "*[rsc:#]*") orElse (title like "*[rsc:##]*") 35 | if titleMatches then 36 | '-- only react if slide number has to change 37 | dim t as String = title.substring(title.indexOf("[rsc:") + 5) 38 | dim nowSlideSelect as String = t.substring(0, t.indexOf("]")) 39 | if nowSlideSelect <> lastSlideSelect then 40 | lastSlideSelect = nowSlideSelect 41 | 42 | '-- connect to RemoteShowControl and select particular slide number 43 | Console.WriteLine("remoteshowcontrol-loop: INFO: select slide #" & nowSlideSelect & " for input #" & nowInPreview) 44 | dim client as new System.Net.Sockets.TcpClient(clientIP, clientPort) 45 | client.SendTimeout = 1000 46 | client.ReceiveTimeout = 1000 47 | dim data as [Byte]() = System.Text.Encoding.ASCII.GetBytes("GO " & nowSlideSelect) 48 | dim stream as System.Net.Sockets.NetworkStream = client.GetStream() 49 | stream.Write(data, 0, data.Length) 50 | stream.Close() 51 | client.Close() 52 | end if 53 | end if 54 | end if 55 | 56 | '-- wait a little bit before next iteration 57 | sleep(200) 58 | loop 59 | 60 | -------------------------------------------------------------------------------- /remoteshowcontrol-once.md: -------------------------------------------------------------------------------- 1 | 2 | [RemoteShowControl-Once](remoteshowcontrol-once.vb) 3 | =================================================== 4 | 5 | **Once Control Irisdown RemoteShowControl** 6 | 7 | Once control a remote PowerPoint slide-deck with the help of its 8 | [Irisdown RemoteShowControl](https://www.irisdown.co.uk/rsc.html) 9 | plugin, based on vMix triggers or shortcuts. 10 | 11 | Problem 12 | ------- 13 | 14 | When ingesting the screen or HDMI captured output of a PowerPoint 15 | slide-deck into vMix scene inputs one has to ensure that the correct 16 | slide is selected within PowerPoint. Often, PowerPoint is running on a 17 | different computer than vMix, too. 18 | 19 | Additionally, when presenting slides with PowerPoint and teleprompter 20 | information with applications like QPrompt (which has to be 21 | interactively speed-adjusted by an operator to follow the speaker) in 22 | parallel, PowerPoint cannot be controlled with regular input devices 23 | like Logitech Presenter anymore, as this requires PowerPoint to 24 | have focus (in parallel to QPrompt) and under Windows only a single 25 | application can be focused. 26 | 27 | Solution 28 | -------- 29 | 30 | The PowerPoint plugin [Irisdown 31 | RemoteShowControl](https://www.irisdown.co.uk/rsc.html) is installed 32 | into PowerPoint and listens on a TCP port for control commands. This 33 | script can be run to instruct PowerPoint, through its installed Irisdown 34 | Remote Show Control plugin, to go to the previous, next or a particular 35 | slide. 36 | 37 | As IrisDown RemoteShowControl is a PowerPoint plugin which receives the 38 | commands via TCP, PowerPoint isn't required to be focused, too. 39 | 40 | Usage 41 | ----- 42 | 43 | Configure vMix Shortcuts or Triggers with: 44 | 45 | <key1> SetDynamicValue1 PREV 46 | <key1> ScriptStart remoteshowcontrol-once 47 | 48 | <key2> SetDynamicValue1 NEXT 49 | <key2> ScriptStart remoteshowcontrol-once 50 | 51 | <key3> SetDynamicValue1 <N> 52 | <key3> ScriptStart remoteshowcontrol-once 53 | 54 | -------------------------------------------------------------------------------- /remoteshowcontrol-once.vb: -------------------------------------------------------------------------------- 1 | '-- 2 | '-- remoteshowcontrol-once.vb -- vMix script for RemoteShowControl one-time commands 3 | '-- Copyright (c) 2022-2023 Dr. Ralf S. Engelschall <rse@engelschall.com> 4 | '-- Distributed under MIT license <https://spdx.org/licenses/MIT.html> 5 | '-- 6 | '-- Language: VB.NET 2.0 (vMix 4K/Pro flavor) 7 | '-- Version: 1.0.0 (2022-03-13) 8 | '-- 9 | 10 | '-- CONFIGURATION 11 | dim clientIP as String = "10.1.0.15" 12 | dim clientPort as String = "61001" 13 | 14 | '-- load the current API state 15 | dim xml as string = API.XML() 16 | dim cfg as new System.Xml.XmlDocument 17 | cfg.LoadXml(xml) 18 | 19 | '-- determine command parameter 20 | dim cmd as string = cfg.SelectSingleNode("//dynamic/value1").InnerText 21 | if not (cmd = "PREV" or cmd = "NEXT") then 22 | cmd = "GO " & cmd 23 | end if 24 | 25 | '-- connect to RemoteShowControl and send command 26 | dim client as new System.Net.Sockets.TcpClient(clientIP, clientPort) 27 | client.SendTimeout = 1000 28 | client.ReceiveTimeout = 1000 29 | dim data as [Byte]() = System.Text.Encoding.ASCII.GetBytes(cmd) 30 | dim stream as System.Net.Sockets.NetworkStream = client.GetStream() 31 | stream.Write(data, 0, data.Length) 32 | stream.Close() 33 | client.Close() 34 | 35 | -------------------------------------------------------------------------------- /smooth-pan-zoom.md: -------------------------------------------------------------------------------- 1 | 2 | [Smooth-Pan-Zoom](smooth-pan-zoom.vb) 3 | ===================================== 4 | 5 | **Smooth Virtual Pan/Zoom in Virtual Sets** 6 | 7 | Smoothly adjust the pan/zoom of an input, for a rough emulation of the 8 | vMix Virtual PTZ feature, which cannot be used on layered inputs like 9 | Virtual Sets. 10 | 11 | Problem 12 | ------- 13 | 14 | In a greenscreen-baded production you have to place a chroma-keyed 15 | camera input over a pre-rendered background image input. In order to 16 | PTZ into such a scene, a virtual PTZ is required. Unfortunately, 17 | the standard "Virtual PTZ" functionality (under input "Settings" / "PTZ") 18 | cannot be used, as it works only on the main content and not on the 19 | layers of an input. 20 | 21 | Solution 22 | -------- 23 | 24 | A "Virtual Set" type input (usually the "Blank" or "Blank 10" one) has 25 | to be used, as only this input allows you to use "Position" for its 26 | main content and its layers in parallel. This script then smoothly 27 | adjusts the pan/zoom of this input for a rough emulation of the original 28 | "Virtual PTZ" feature. 29 | 30 | Usage 31 | ----- 32 | 33 | Configure vMix Shortcuts with: 34 | 35 | <keyX> SetDynamicInput4 <input-name> 36 | <keyX> SetDynamicValue4 {pan:{up-left|up|up-right|left|reset|right|down-left|down|down-right}|zoom:{increase|reset|decrease}} 37 | <keyX> ScriptStart smooth-pan-zoom 38 | 39 | Alternatively, for splitting between input selection (`keyA`) and operation (`keyX`), configure vMix Shortcuts with: 40 | 41 | <keyA> SetDynamicInput4 <input-name> 42 | <keyX> SetDynamicValue4 {pan:{up-left|up|up-right|left|reset|right|down-left|down|down-right}|zoom:{increase|reset|decrease}} 43 | <keyX> ScriptStart smooth-pan-zoom 44 | 45 | -------------------------------------------------------------------------------- /smooth-pan-zoom.vb: -------------------------------------------------------------------------------- 1 | '-- 2 | '-- smooth-pan-zoom.vb -- vMix script for smooth adjusting pan/zoom of input 3 | '-- Copyright (c) 2022-2023 Dr. Ralf S. Engelschall <rse@engelschall.com> 4 | '-- Distributed under MIT license <https://spdx.org/licenses/MIT.html> 5 | '-- 6 | '-- Language: VB.NET 2.0 (vMix 4K/Pro flavor) 7 | '-- Version: 0.9.0 (2022-06-18) 8 | '-- 9 | 10 | '-- CONFIGURATION 11 | dim timeSlice as integer = 33 '-- (= 1000ms/30fps) 12 | dim duration as integer = 660 '-- (= multiple of timeSlice) 13 | dim deltaPan as double = 0.10 '-- (= 10%) 14 | dim deltaZoom as double = 0.10 '-- (= 10%) 15 | 16 | '-- load the current API state 17 | dim xml as string = API.XML() 18 | dim cfg as new System.Xml.XmlDocument 19 | cfg.LoadXml(xml) 20 | 21 | '-- determine parameters 22 | dim inputName as string = cfg.SelectSingleNode("//dynamic/input4").InnerText 23 | dim params() as string = cfg.SelectSingleNode("//dynamic/value4").InnerText.Split(":") 24 | dim opName as string = params(0) 25 | dim dirName as string = params(1) 26 | 27 | '-- determine operation function(s) 28 | dim func1 as string = "" 29 | dim func2 as string = "" 30 | dim value1 as string = "" 31 | dim value2 as string = "" 32 | dim delta as double = 0 33 | if opName = "pan" then 34 | delta = deltaPan 35 | if dirName = "up-left" then 36 | func1 = "SetPanY" 37 | func2 = "SetPanX" 38 | value1 = "-=" 39 | value2 = "+=" 40 | else if dirName = "up" then 41 | func1 = "SetPanY" 42 | value1 = "-=" 43 | else if dirName = "up-right" then 44 | func1 = "SetPanY" 45 | func2 = "SetPanX" 46 | value1 = "-=" 47 | value2 = "-=" 48 | else if dirName = "left" then 49 | func1 = "SetPanX" 50 | value1 = "+=" 51 | else if dirName = "reset" then 52 | func1 = "SetPanY" 53 | func2 = "SetPanX" 54 | else if dirName = "right" then 55 | func1 = "SetPanX" 56 | value1 = "-=" 57 | else if dirName = "down-left" then 58 | func1 = "SetPanY" 59 | func2 = "SetPanX" 60 | value1 = "+=" 61 | value2 = "+=" 62 | else if dirName = "down" then 63 | func1 = "SetPanY" 64 | value1 = "+=" 65 | else if dirName = "down-right" then 66 | func1 = "SetPanY" 67 | func2 = "SetPanX" 68 | value1 = "+=" 69 | value2 = "-=" 70 | end if 71 | else if opName = "zoom" then 72 | delta = deltaZoom 73 | func1 = "SetZoom" 74 | if dirName = "decrease" then 75 | value1 = "-=" 76 | else if dirName = "increase" then 77 | value1 = "+=" 78 | end if 79 | end if 80 | 81 | '-- determine operation value(s) 82 | dim timeSteps as integer = duration / timeSlice 83 | dim valueSlice as double = delta / timeSteps 84 | if value1 <> "" then 85 | value1 = value1 & valueSlice 86 | else 87 | value1 = "0" 88 | end if 89 | if func2 <> "" then 90 | if value2 <> "" then 91 | value2 = value2 & valueSlice 92 | else 93 | value2 = "0" 94 | end if 95 | end if 96 | 97 | '-- apply operation in a smooth way 98 | do while timeSteps > 0 99 | timeSteps = timeSteps - 1 100 | 101 | '-- perform single operation step 102 | API.Function(func1, Input := inputName, Value := value1) 103 | if func2 <> "" then 104 | API.Function(func2, Input := inputName, Value := value2) 105 | end if 106 | 107 | '-- wait until next iteration 108 | sleep(timeSlice) 109 | loop 110 | 111 | -------------------------------------------------------------------------------- /vmix-scripts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rse/vmix-scripts/842c01912f266bc711143cfc329336d03157d421/vmix-scripts.png -------------------------------------------------------------------------------- /vmix-scripts.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rse/vmix-scripts/842c01912f266bc711143cfc329336d03157d421/vmix-scripts.psd --------------------------------------------------------------------------------