├── 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 ,...
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
4 | '-- Distributed under MIT license
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
4 | '-- Distributed under MIT license
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
4 | '-- Distributed under MIT license
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
4 | '-- Distributed under MIT license
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 | SetDynamicValue1 :
33 | SetDynamicValue1
34 | 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
4 | '-- Distributed under MIT license
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 | SetDynamicValue3 recording-marker-simple
56 | 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
4 | '-- Distributed under MIT license
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
4 | '-- Distributed under MIT license
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 | SetDynamicValue1 PREV
46 | ScriptStart remoteshowcontrol-once
47 |
48 | SetDynamicValue1 NEXT
49 | ScriptStart remoteshowcontrol-once
50 |
51 | SetDynamicValue1
52 | 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
4 | '-- Distributed under MIT license
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 | SetDynamicInput4
36 | SetDynamicValue4 {pan:{up-left|up|up-right|left|reset|right|down-left|down|down-right}|zoom:{increase|reset|decrease}}
37 | ScriptStart smooth-pan-zoom
38 |
39 | Alternatively, for splitting between input selection (`keyA`) and operation (`keyX`), configure vMix Shortcuts with:
40 |
41 | SetDynamicInput4
42 | SetDynamicValue4 {pan:{up-left|up|up-right|left|reset|right|down-left|down|down-right}|zoom:{increase|reset|decrease}}
43 | 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
4 | '-- Distributed under MIT license
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
--------------------------------------------------------------------------------