Compare commits

..

40 Commits

Author SHA1 Message Date
WarmUpTill
ba38b8bf27 Don't block UI while executing long runnig actions
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
The previous approach had the problem of losing any action internal
state changes in the created copy.

Revert "Fix temp var values of actions not being accessible"
This reverts commit df42538319.
Revert "Don't block UI while running actions"
This reverts commit a01d26e25d.
2026-04-04 21:14:05 +02:00
WarmUpTill
38513735dd Refresh available interactable soruces on show 2026-04-04 21:14:05 +02:00
WarmUpTill
acfc7d605b Add "Get macro info" action
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-04-03 13:44:34 +02:00
WarmUpTill
df42538319 Fix temp var values of actions not being accessible
This a bug introduced with ec3052c823.
2026-04-03 13:44:34 +02:00
WarmUpTill
b02259e926 Add "Source interact" action 2026-04-03 09:41:09 +02:00
WarmUpTill
2b6bd9fb51 Add helpers to add widgets and separators to ListControls 2026-04-03 09:41:09 +02:00
WarmUpTill
aadedbb610 Enable setting min and max list size 2026-04-03 09:41:09 +02:00
WarmUpTill
d15a324137 Add placeholder text support 2026-04-03 09:41:09 +02:00
WarmUpTill
119fac17bf Cleanup
Some checks are pending
debian-build / build (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
* Member access
* Refresh settings on show to ensure "repeat" changes are visible
2026-04-02 22:39:12 +02:00
WarmUpTill
088881b674 Add HTTP condition
Some checks are pending
debian-build / build (push) Waiting to run
Check locale / ubuntu64 (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
2026-04-02 17:26:16 +02:00
WarmUpTill
f008d5367b Adjust layout of HTTP action 2026-04-02 17:26:16 +02:00
WarmUpTill
1ace185a57 Add action to play sound 2026-04-02 17:25:41 +02:00
WarmUpTill
a01d26e25d Don't block UI while running actions 2026-04-02 17:25:41 +02:00
WarmUpTill
7e2ef9aacc Add tests for "File" condition
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-04-01 11:13:01 +02:00
WarmUpTill
4a98d064f7 Enable testing 2026-04-01 11:13:01 +02:00
WarmUpTill
b0d6cea52a Remove deprecated functionality from "File" condition 2026-04-01 11:13:01 +02:00
WarmUpTill
d6bf9ed101 Fix date / content changed check triggering on plugin start 2026-04-01 11:13:01 +02:00
WarmUpTill
7e940e515b Add option to suppress crash dialog
Some checks are pending
debian-build / build (push) Waiting to run
Check locale / ubuntu64 (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
2026-03-31 23:04:35 +02:00
WarmUpTill
29e1ff0754 Adjust video condition temp vars
Some checks are pending
debian-build / build (push) Waiting to run
Check locale / ubuntu64 (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
2026-03-30 21:20:00 +02:00
WarmUpTill
6309100cc4 Rework pattern count temp var 2026-03-30 21:20:00 +02:00
WarmUpTill
ec3052c823 Enable checking if temp var is in use 2026-03-30 21:20:00 +02:00
WarmUpTill
a28b800228 Remove Snap installation instructions from README
Some checks failed
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
Removed Snap package installation instructions for OBS Studio.
2026-03-27 18:36:35 +01:00
WarmUpTill
713a64ab45 Fix crash when using math helpers concurrently
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-03-25 19:38:22 +01:00
WarmUpTill
4d5ffe38fc Fix race conditions when accessing variables 2026-03-25 19:38:22 +01:00
WarmUpTill
85460157b1 Fix title of script open dialog 2026-03-25 19:38:22 +01:00
WarmUpTill
35e5b7620e Add option to "release the T-Bar"
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-03-23 19:05:49 +01:00
WarmUpTill
e4c06b14a7 Cleanup 2026-03-23 19:05:49 +01:00
WarmUpTill
7fe3258269 Add support for changing T-bar value 2026-03-23 19:05:49 +01:00
WarmUpTill
ec19b4d4fb Add option to add new recording chapter
Some checks are pending
debian-build / build (push) Waiting to run
Check locale / ubuntu64 (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
2026-03-22 21:24:59 +01:00
WarmUpTill
9301ead060 Add chat settings toggles
Some checks are pending
debian-build / build (push) Waiting to run
Check locale / ubuntu64 (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
2026-03-21 22:52:27 +01:00
WarmUpTill
bc29ece526 Cleanup and layout adjustments 2026-03-21 22:52:27 +01:00
WarmUpTill
4158c7a363 Add Twitch actions
* Send shoutout
* Cancel raid
* Enable/disable shield mode
* Enable/disable branded content
* Snooze next ad
2026-03-21 22:52:27 +01:00
WarmUpTill
944d1059da Add user moderation actions 2026-03-21 22:52:27 +01:00
WarmUpTill
bf18d8e106 Validate that user id matches token 2026-03-21 22:52:27 +01:00
WarmUpTill
bf216e0917 Fix subscription handling and improve logging
* Revocations were not handled properly
* Active subscriptions were not cleared on reconnect
* Migration client handling could cause crash
* Add error logging
2026-03-21 22:52:27 +01:00
WarmUpTill
2405b6dbbf Add "Next Macro" temp var to Sequence actions when setting index
Some checks are pending
debian-build / build (push) Waiting to run
Check locale / ubuntu64 (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
2026-03-21 00:05:58 +01:00
WarmUpTill
e64f2d195e Add temp vars to "Sequence" and "Random" action for executed macro 2026-03-21 00:05:58 +01:00
WarmUpTill
f66bec8caf Add option to expose current settings of source as temp var 2026-03-20 23:40:05 +01:00
WarmUpTill
3eb79e3adb Scroll to new macro segments
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-03-19 20:21:08 +01:00
WarmUpTill
07e2ac3ca0 Fix macro list / macro edit area splitter resizing
When horizontally large widgets (e.g. the Window condition) were part of
the currently selected macro moving the splitter would result in it
either fully hiding the macro list or the macro edit area.

Now the list and edit area can be resized smoothly.
2026-03-19 20:21:08 +01:00
105 changed files with 5656 additions and 698 deletions

View File

@@ -14,11 +14,6 @@ On Linux the plugin is available via the **Flatpak** package manager for users w
flatpak install com.obsproject.Studio.Plugin.SceneSwitcher
```
The **Snap** package manager offers an OBS Studio installation which is bundled with the plugin:
```
sudo snap install obs-studio
```
More information can be found [here](https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation).
## Contributing

View File

@@ -145,11 +145,7 @@ AdvSceneSwitcher.condition.file="Datei"
AdvSceneSwitcher.condition.file.type.match="entspricht"
AdvSceneSwitcher.condition.file.type.contentChange="Inhalt geändert"
AdvSceneSwitcher.condition.file.type.dateChange="Änderungsdatum geändert"
AdvSceneSwitcher.condition.file.remote="Entfernte Datei"
AdvSceneSwitcher.condition.file.local="Lokale Datei"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.file.layout="{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.media="Medien"
AdvSceneSwitcher.condition.media.source="Quelle"
AdvSceneSwitcher.condition.media.anyOnScene="Beliebige Medienquelle in"
@@ -412,7 +408,6 @@ AdvSceneSwitcher.action.recording.type.unpause="Aufnahme nicht mehr pausieren"
AdvSceneSwitcher.action.recording.type.split="Aufnahme-Datei teilen"
AdvSceneSwitcher.action.recording.pause.hint="Bitte beachten, dass je nach Aufnahmeeinstellung die Aufnahme möglicherweise nicht unterbrochen werden kann"
AdvSceneSwitcher.action.recording.split.hint="Vergewissern, dass die automatische Dateiaufteilung in den OBS-Einstellungen aktiviert ist!"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="Replay Buffer"
AdvSceneSwitcher.action.replay.saveWarn="Warnung: Ein zu häufiges Speichern kann dazu führen, dass der Replay Buffer nicht gespeichert wird!"
AdvSceneSwitcher.action.replay.type.stop="Replay Buffer stoppen"
@@ -505,9 +500,9 @@ AdvSceneSwitcher.action.transition.type.scene="Szenenübergang"
AdvSceneSwitcher.action.transition.type.sceneOverride="Szenenübergang überschreiben"
AdvSceneSwitcher.action.transition.type.sourceShow="Übergang der Quelle anzeigen"
AdvSceneSwitcher.action.transition.type.sourceHide="Übergang der Quelle verstecken"
AdvSceneSwitcher.action.transition.entry.line1="Anpassen {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Übergangstyp festlegen auf {{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Übergangsdauer festlegen auf {{duration}}Sekunden"
AdvSceneSwitcher.action.transition.layout.type="Anpassen {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Übergangstyp festlegen auf {{transitions}}"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Übergangsdauer festlegen auf {{duration}}Sekunden"
AdvSceneSwitcher.action.timer="Timer"
AdvSceneSwitcher.action.timer.type.pause="Pausieren"
AdvSceneSwitcher.action.timer.type.continue="Fortsetzen"

View File

@@ -40,6 +40,7 @@ AdvSceneSwitcher.generalTab.generalBehavior.showTrayNotifications="Show system t
AdvSceneSwitcher.generalTab.generalBehavior.disableUIHints="Disable UI hints"
AdvSceneSwitcher.generalTab.generalBehavior.comboBoxFilterDisable="Disable filtering by typing in drop down menus"
AdvSceneSwitcher.generalTab.generalBehavior.warnPluginLoadFailure="Display warning if plugins cannot be loaded"
AdvSceneSwitcher.generalTab.generalBehavior.suppressCrashRecoveryDialog="Do not ask to keep the plugin stopped after an unclean shutdown"
AdvSceneSwitcher.generalTab.generalBehavior.warnPluginLoadFailureMessage="<html><body>Loading of the following plugin libraries was unsuccessful, which could result in some Advanced Scene Switcher functions not being available:%1Check the OBS logs for details.<br>This message can be disabled on the General tab.</body></html>"
AdvSceneSwitcher.generalTab.generalBehavior.warnCorruptedInstallMessage="The plugin installation seems to be corrupted and might crash!\nPlease make sure the plugin was installed correctly!"
AdvSceneSwitcher.generalTab.generalBehavior.hideLegacyTabs="Hide tabs which can be represented via macros"
@@ -368,11 +369,7 @@ AdvSceneSwitcher.condition.file.type.dateChange="modification date changed"
AdvSceneSwitcher.condition.file.type.exists="exists"
AdvSceneSwitcher.condition.file.type.isFile="is a file"
AdvSceneSwitcher.condition.file.type.isFolder="is a folder"
AdvSceneSwitcher.condition.file.remote="Remote file"
AdvSceneSwitcher.condition.file.local="Local file"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.file.layout="{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.media="Media"
AdvSceneSwitcher.condition.media.checkType.state="State matches"
AdvSceneSwitcher.condition.media.checkType.time="Time restriction matches"
@@ -676,6 +673,34 @@ AdvSceneSwitcher.condition.websocket.type.event="Scene Switcher Event"
AdvSceneSwitcher.condition.websocket.useRegex="Use regular expressions"
AdvSceneSwitcher.condition.websocket.entry.request="{{type}}was received:"
AdvSceneSwitcher.condition.websocket.entry.event="{{type}}was received from{{connection}}:"
AdvSceneSwitcher.condition.http="HTTP"
AdvSceneSwitcher.condition.http.layout="Receive{{method}}request on{{server}}"
AdvSceneSwitcher.condition.http.layout.path="Path:{{path}}{{regex}}"
AdvSceneSwitcher.condition.http.layout.body="Body:{{body}}{{regex}}"
AdvSceneSwitcher.condition.http.method.any="any"
AdvSceneSwitcher.condition.http.method.get="GET"
AdvSceneSwitcher.condition.http.method.post="POST"
AdvSceneSwitcher.condition.http.method.put="PUT"
AdvSceneSwitcher.condition.http.method.patch="PATCH"
AdvSceneSwitcher.condition.http.method.delete="DELETE"
AdvSceneSwitcher.httpServer.select="Select HTTP server"
AdvSceneSwitcher.httpServer.add="Add HTTP server"
AdvSceneSwitcher.httpServer.configure="Configure HTTP server"
AdvSceneSwitcher.httpServer.invalid="Invalid HTTP server"
AdvSceneSwitcher.httpServer.name="Name:"
AdvSceneSwitcher.httpServer.port="Port:"
AdvSceneSwitcher.httpServer.startOnLoad="Start on load"
AdvSceneSwitcher.httpServer.status.listening="Listening"
AdvSceneSwitcher.httpServer.status.stopped="Stopped"
AdvSceneSwitcher.httpServerTab.title="HTTP Servers"
AdvSceneSwitcher.httpServerTab.help="No HTTP servers configured.\nAdd one to receive incoming HTTP requests in conditions."
AdvSceneSwitcher.httpServerTab.addButton.tooltip="Add HTTP server"
AdvSceneSwitcher.httpServerTab.removeButton.tooltip="Remove HTTP server"
AdvSceneSwitcher.httpServerTab.name.header="Name"
AdvSceneSwitcher.httpServerTab.port.header="Port"
AdvSceneSwitcher.httpServerTab.status.header="Status"
AdvSceneSwitcher.httpServerTab.removeSingle.text="Are you sure you want to remove \"%1\"?"
AdvSceneSwitcher.httpServerTab.removeMultiple.text="Are you sure you want to remove %1 HTTP servers?"
AdvSceneSwitcher.condition.temporaryVariable="Macro property"
AdvSceneSwitcher.condition.variable="Variable"
AdvSceneSwitcher.condition.variable.type.compare="equals"
@@ -879,6 +904,15 @@ AdvSceneSwitcher.action.audio.fade.rate="{{fade}}Fade{{fadeTypes}}{{rate}}per se
AdvSceneSwitcher.action.audio.fade.wait="Wait for fade to complete."
AdvSceneSwitcher.action.audio.fade.abort="Abort already active fade."
AdvSceneSwitcher.action.audio.entry="{{actions}}{{audioSources}}{{volume}}{{volumeDB}}{{percentDBToggle}}{{syncOffset}}{{monitorTypes}}{{track}}"
AdvSceneSwitcher.action.playAudio="Play Audio File"
AdvSceneSwitcher.action.playAudio.file.browse="Browse for audio file"
AdvSceneSwitcher.action.playAudio.layout.volume="Volume{{volumeDB}}"
AdvSceneSwitcher.action.playAudio.layout.monitor="Monitoring{{monitorTypes}}"
AdvSceneSwitcher.action.playAudio.wait="Wait for playback to complete"
AdvSceneSwitcher.action.playAudio.monitorUnavailable="Audio monitoring not available"
AdvSceneSwitcher.action.playAudio.tracks="Tracks"
AdvSceneSwitcher.action.playAudio.layout.startOffset="{{useStartOffset}}Start at{{startOffset}}"
AdvSceneSwitcher.action.playAudio.layout.playbackDuration="{{useDuration}}Play for{{playbackDuration}}(0 = until end)"
AdvSceneSwitcher.action.recording="Recording"
AdvSceneSwitcher.action.recording.type.stop="Stop recording"
AdvSceneSwitcher.action.recording.type.start="Start recording"
@@ -887,9 +921,10 @@ AdvSceneSwitcher.action.recording.type.unpause="Unpause recording"
AdvSceneSwitcher.action.recording.type.split="Split recording file"
AdvSceneSwitcher.action.recording.type.changeOutputFolder="Change output folder"
AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="Change filename formatting"
AdvSceneSwitcher.action.recording.type.addChapter="Add chapter"
AdvSceneSwitcher.action.recording.pause.hint="Note that depending on your recording settings you might not be able to pause recording"
AdvSceneSwitcher.action.recording.split.hint="Make sure to enable automatic file splitting in the OBS settings first!"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{chapterName}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="Replay buffer"
AdvSceneSwitcher.action.replay.saveWarn="Warning: Saving too frequently might result in the replay buffer not actually being saved!"
AdvSceneSwitcher.action.replay.durationWarn="Warning: Changing the maximum replay time will only apply the next time the replay buffer is started!"
@@ -949,6 +984,8 @@ AdvSceneSwitcher.action.source.type.openPropertiesDialog="Open properties dialog
AdvSceneSwitcher.action.source.type.closeInteractionDialog="Close interaction dialog"
AdvSceneSwitcher.action.source.type.closeFilterDialog="Close filter dialog"
AdvSceneSwitcher.action.source.type.closePropertiesDialog="Close properties dialog"
AdvSceneSwitcher.action.source.type.getSetting="Get setting value"
AdvSceneSwitcher.action.source.type.getSettings="Get settings JSON"
AdvSceneSwitcher.action.source.entry="{{actions}}{{sources}}{{settingsButtons}}{{deinterlaceMode}}{{deinterlaceOrder}}{{refresh}}"
AdvSceneSwitcher.action.source.entry.settings="{{settings}}{{settingsInputMethod}}{{settingValue}}{{tempVar}}"
AdvSceneSwitcher.action.source.warning="Warning: Enabling and disabling sources globally cannot be controlled by the OBS UI\nYou might be looking for \"Scene item visibility\""
@@ -971,6 +1008,49 @@ AdvSceneSwitcher.action.source.inputMethod.json="Set setting JSON string"
AdvSceneSwitcher.action.source.refresh="Refresh"
AdvSceneSwitcher.action.source.refresh.tooltip="Repopulate the source settings selection with the settings of the source which's name matches the variable value."
AdvSceneSwitcher.action.source.dialog.accept="Accept changes"
AdvSceneSwitcher.action.sourceInteraction="Source Interaction"
AdvSceneSwitcher.action.sourceInteraction.source="Source"
AdvSceneSwitcher.action.sourceInteraction.noSelection="Select a step to edit it"
AdvSceneSwitcher.action.sourceInteraction.record="Record interaction ..."
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseMove="Mouse move (%1, %2)"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseUp="up"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseDown="down"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseClick="Mouse %1 %2 (%3, %4)"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseClickCount=" x%1"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseWheel="Mouse wheel (%1, %2) dx=%3 dy=%4"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyUp="up"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyDown="down"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyPress="Key %1"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyPressWithText="Key %1 '%2'"
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.typeText="Type \"%1\""
AdvSceneSwitcher.action.sourceInteraction.step.listEntry.wait="Wait %1 ms"
AdvSceneSwitcher.action.sourceInteraction.step.edit.type="Type"
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseMove="Mouse move"
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseClick="Mouse click"
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseWheel="Mouse wheel"
AdvSceneSwitcher.action.sourceInteraction.step.edit.keyPress="Key press"
AdvSceneSwitcher.action.sourceInteraction.step.edit.typeText="Type text"
AdvSceneSwitcher.action.sourceInteraction.step.edit.wait="Wait"
AdvSceneSwitcher.action.sourceInteraction.step.edit.x="X"
AdvSceneSwitcher.action.sourceInteraction.step.edit.y="Y"
AdvSceneSwitcher.action.sourceInteraction.step.edit.button="Button"
AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseUp="Mouse up"
AdvSceneSwitcher.action.sourceInteraction.step.edit.clickCount="Click count"
AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDx="Delta X"
AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDy="Delta Y"
AdvSceneSwitcher.action.sourceInteraction.step.edit.vkey="Virtual key"
AdvSceneSwitcher.action.sourceInteraction.step.edit.keyUp="Key up"
AdvSceneSwitcher.action.sourceInteraction.step.edit.text="Text"
AdvSceneSwitcher.action.sourceInteraction.step.edit.waitMs="Duration (ms)"
AdvSceneSwitcher.action.sourceInteraction.button.left="Left"
AdvSceneSwitcher.action.sourceInteraction.button.middle="Middle"
AdvSceneSwitcher.action.sourceInteraction.button.right="Right"
AdvSceneSwitcher.action.sourceInteraction.record.title="Record Source Interactions"
AdvSceneSwitcher.action.sourceInteraction.record.start="Start recording"
AdvSceneSwitcher.action.sourceInteraction.record.stop="Stop recording"
AdvSceneSwitcher.action.sourceInteraction.record.accept="Accept"
AdvSceneSwitcher.action.sourceInteraction.record.placeholder="Start recording and interact with the source to capture steps"
AdvSceneSwitcher.action.sourceInteraction.record.invalidSource="No valid source is selected.\nPlease select a source that supports interaction before recording."
AdvSceneSwitcher.action.media="Media"
AdvSceneSwitcher.action.media.type.play="Play"
AdvSceneSwitcher.action.media.type.pause="Pause"
@@ -1005,6 +1085,7 @@ AdvSceneSwitcher.action.macro.type.stop="Stop actions"
AdvSceneSwitcher.action.macro.type.disableAction="Disable action"
AdvSceneSwitcher.action.macro.type.enableAction="Enable action"
AdvSceneSwitcher.action.macro.type.toggleAction="Toggle action"
AdvSceneSwitcher.action.macro.type.getInfo="Get macro info"
AdvSceneSwitcher.action.macro.type.nestedMacro="Nested macro"
AdvSceneSwitcher.action.macro.actionSelectionType.index="at index"
AdvSceneSwitcher.action.macro.actionSelectionType.label="with label"
@@ -1087,13 +1168,16 @@ AdvSceneSwitcher.action.studioMode.type.enable="Enable studio mode"
AdvSceneSwitcher.action.studioMode.type.disable="Disable studio mode"
AdvSceneSwitcher.action.studioMode.entry="{{actions}}{{scenes}}"
AdvSceneSwitcher.action.transition="Transition"
AdvSceneSwitcher.action.transition.type.scene="scene transition"
AdvSceneSwitcher.action.transition.type.sceneOverride="scene transition override"
AdvSceneSwitcher.action.transition.type.sourceShow="source show transition"
AdvSceneSwitcher.action.transition.type.sourceHide="source hide transition"
AdvSceneSwitcher.action.transition.entry.line1="Modify{{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Set transition type to{{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Set transition duration to{{duration}}seconds"
AdvSceneSwitcher.action.transition.type.scene="Modify scene transition"
AdvSceneSwitcher.action.transition.type.sceneOverride="Modify scene transition override"
AdvSceneSwitcher.action.transition.type.sourceShow="Modify source show transition"
AdvSceneSwitcher.action.transition.type.sourceHide="Modify source hide transition"
AdvSceneSwitcher.action.transition.type.tbar="Modify T-Bar position"
AdvSceneSwitcher.action.transition.type.releaseTbar="Release T-Bar"
AdvSceneSwitcher.action.transition.layout.type="{{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Set transition type to{{transitions}}"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Set transition duration to{{duration}}seconds"
AdvSceneSwitcher.action.transition.layout.tbar="Set T-Bar position to{{tbarPosition}}"
AdvSceneSwitcher.action.timer="Timer"
AdvSceneSwitcher.action.timer.type.pause="Pause"
AdvSceneSwitcher.action.timer.type.continue="Continue"
@@ -1253,23 +1337,52 @@ AdvSceneSwitcher.action.twitch.type.channel.info.category.set="Set stream catego
AdvSceneSwitcher.action.twitch.type.channel.info.tags.set="Set stream tags"
AdvSceneSwitcher.action.twitch.type.channel.info.language.set="Set stream language"
AdvSceneSwitcher.action.twitch.type.raid.start="Start raid"
AdvSceneSwitcher.action.twitch.type.raid.end="Cancel raid"
AdvSceneSwitcher.action.twitch.type.shoutout.send="Send shoutout"
AdvSceneSwitcher.action.twitch.type.shieldMode.start="Enable shield mode"
AdvSceneSwitcher.action.twitch.type.shieldMode.end="Disable shield mode"
AdvSceneSwitcher.action.twitch.type.commercial.start="Start commercial"
AdvSceneSwitcher.action.twitch.type.commercial.snooze="Snooze next ad"
AdvSceneSwitcher.action.twitch.type.marker.create="Create stream marker"
AdvSceneSwitcher.action.twitch.type.clip.create="Create stream clip"
AdvSceneSwitcher.action.twitch.type.chat.announcement.send="Send chat announcement"
AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.enable="Enable chat's emote-only mode"
AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.disable="Disable chat's emote-only mode"
AdvSceneSwitcher.action.twitch.type.chat.followerOnly.enable="Enable chat's follower-only mode"
AdvSceneSwitcher.action.twitch.type.chat.followerOnly.disable="Disable chat's follower-only mode"
AdvSceneSwitcher.action.twitch.type.chat.subscriberOnly.enable="Enable chat's subscriber-only mode"
AdvSceneSwitcher.action.twitch.type.chat.subscriberOnly.disable="Disable chat's subscriber-only mode"
AdvSceneSwitcher.action.twitch.type.chat.slowMode.enable="Enable chat's slow mode"
AdvSceneSwitcher.action.twitch.type.chat.slowMode.disable="Disable chat's slow mode"
AdvSceneSwitcher.action.twitch.type.chat.nonModeratorDelay.enable="Enable chat's non-moderator message delay"
AdvSceneSwitcher.action.twitch.type.chat.nonModeratorDelay.disable="Disable chat's non-moderator message delay"
AdvSceneSwitcher.action.twitch.type.chat.uniqueMode.enable="Enable chat's unique message mode"
AdvSceneSwitcher.action.twitch.type.chat.uniqueMode.disable="Disable chat's unique message mode"
AdvSceneSwitcher.action.twitch.type.chat.sendMessage="Send chat message"
AdvSceneSwitcher.action.twitch.type.user.getInfo="Get user information"
AdvSceneSwitcher.action.twitch.type.user.ban="Ban user"
AdvSceneSwitcher.action.twitch.type.user.unban="Unban user"
AdvSceneSwitcher.action.twitch.type.user.block="Block user"
AdvSceneSwitcher.action.twitch.type.user.unblock="Unblock user"
AdvSceneSwitcher.action.twitch.type.user.moderator.add="Add user as moderator"
AdvSceneSwitcher.action.twitch.type.user.moderator.delete="Remove user as moderator"
AdvSceneSwitcher.action.twitch.type.user.vip.add="Add user as VIP"
AdvSceneSwitcher.action.twitch.type.user.vip.delete="Remove user as VIP"
AdvSceneSwitcher.action.twitch.type.reward.getInfo="Get channel points reward information"
AdvSceneSwitcher.action.twitch.type.channel.getInfo="Get channel information"
AdvSceneSwitcher.action.twitch.reward.toggleControl="Toggle reward name / variable selection control"
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Cannot select category without selecting a Twitch account first!"
AdvSceneSwitcher.action.twitch.layout.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}"
AdvSceneSwitcher.action.twitch.layout.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{nonModDelayDuration}}{{channel}}{{pointsReward}}"
AdvSceneSwitcher.action.twitch.layout.chat="Using account{{account}}{{actions}}on{{channel}}"
AdvSceneSwitcher.action.twitch.layout.channel.getInfo="Using account{{account}}{{actions}}of{{channel}}"
AdvSceneSwitcher.action.twitch.layout.user.getInfo="Using account{{account}}{{actions}}for{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo="Using account{{account}}{{actions}}for channel{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row1="Using account{{account}}{{actions}}"
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row2="for{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.user.moderation.row1="Using account{{account}}{{actions}}"
AdvSceneSwitcher.action.twitch.layout.user.moderation.row2="on channel{{channel}}for{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.user.ban.row1="Using account{{account}}{{actions}}timeout{{duration}}"
AdvSceneSwitcher.action.twitch.layout.user.ban.row2="on channel{{channel}}for{{userInfoQueryType}}{{userLogin}}{{userId}}reason{{banReason}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row1="Using account{{account}}{{actions}}for channel{{channel}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row2="{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
AdvSceneSwitcher.action.twitch.title.title="Enter title"
AdvSceneSwitcher.action.twitch.marker.description="Describe marker"
AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip"
@@ -1290,6 +1403,8 @@ AdvSceneSwitcher.action.twitch.tags.duplicate.info="This tag is already in the l
AdvSceneSwitcher.action.twitch.tags.limit="Tag Limit Reached"
AdvSceneSwitcher.action.twitch.tags.limit.info="You can only have up to %1 tags."
AdvSceneSwitcher.action.twitch.type.channel.info.contentClassification.set="Set content classification labels"
AdvSceneSwitcher.action.twitch.type.channel.info.brandedContent.enable="Enable branded content"
AdvSceneSwitcher.action.twitch.type.channel.info.brandedContent.disable="Disable branded content"
AdvSceneSwitcher.action.twitch.contentClassification.debatedSocialIssuesAndPolitics="Discussions or debates about politics or sensitive social issues such as elections, civic integrity, military conflict, and civil rights."
AdvSceneSwitcher.action.twitch.contentClassification.drugsIntoxication="Excessive tobacco glorification or promotion, any marijuana consumption/use, legal drug and alcohol induced intoxication, discussions of illegal drugs."
AdvSceneSwitcher.action.twitch.contentClassification.sexualThemes="Content that focuses on sexualized activities, sexual topics, or experiences."
@@ -1395,6 +1510,7 @@ AdvSceneSwitcher.askBackup="Detected a new version of the Advanced Scene Switche
AdvSceneSwitcher.askForMacro="Select macro{{macroSelection}}"
AdvSceneSwitcher.crashDetected="OBS did not shut down cleanly (for example, due to a crash or freeze).\n\nThe Advanced Scene Switcher plugin would normally start automatically.\nWould you like to keep it stopped for now?"
AdvSceneSwitcher.crashDetected.suppressCheckbox="Do not show this dialog again"
AdvSceneSwitcher.close="Close"
AdvSceneSwitcher.browse="Browse"
@@ -1664,6 +1780,7 @@ AdvSceneSwitcher.script.language.lua="LUA"
AdvSceneSwitcher.script.language.select="--select language--"
AdvSceneSwitcher.script.language.layout="Script language:{{language}}"
AdvSceneSwitcher.script.file.open="Open"
AdvSceneSwitcher.script.file.select="Select Script File"
AdvSceneSwitcher.script.file.open.failed="Could not open script file!"
AdvSceneSwitcher.script.file.warning.notFound="Warning: The given file path was not found!"
AdvSceneSwitcher.script.file.warning.openFail="Warning: Could not open the script file!"
@@ -2181,6 +2298,18 @@ AdvSceneSwitcher.tempVar.macro.runCount="Run count"
AdvSceneSwitcher.tempVar.macro.runCount.description="The number of times a macro was executed."
AdvSceneSwitcher.tempVar.macro.matchedCount="Matched count"
AdvSceneSwitcher.tempVar.macro.matchedCount.description="The number of macros of which the condition state was true."
AdvSceneSwitcher.tempVar.macro.info.conditionCount="Condition count"
AdvSceneSwitcher.tempVar.macro.info.conditionCount.description="The number of conditions in the macro."
AdvSceneSwitcher.tempVar.macro.info.actionCount="Action count"
AdvSceneSwitcher.tempVar.macro.info.actionCount.description="The number of actions in the macro."
AdvSceneSwitcher.tempVar.macro.info.elseActionCount="Else-action count"
AdvSceneSwitcher.tempVar.macro.info.elseActionCount.description="The number of else-actions in the macro."
AdvSceneSwitcher.tempVar.macro.info.paused="Paused"
AdvSceneSwitcher.tempVar.macro.info.paused.description="Whether the macro is currently paused. Value is \"true\" or \"false\"."
AdvSceneSwitcher.tempVar.macro.info.runCount="Run count"
AdvSceneSwitcher.tempVar.macro.info.runCount.description="The number of times the macro has been executed."
AdvSceneSwitcher.tempVar.macro.info.secondsSinceLastRun="Seconds since last run"
AdvSceneSwitcher.tempVar.macro.info.secondsSinceLastRun.description="The number of seconds elapsed since the macro was last executed. Value is -1 if the macro has never been executed."
AdvSceneSwitcher.tempVar.process.name="Process name"
@@ -2213,6 +2342,8 @@ AdvSceneSwitcher.tempVar.video.text="OCR text"
AdvSceneSwitcher.tempVar.video.text.description="The text detected in a given video input frame."
AdvSceneSwitcher.tempVar.video.color="Average color"
AdvSceneSwitcher.tempVar.video.color.description="The average RGB color in a given video input frame in HexArgb format."
AdvSceneSwitcher.tempVar.video.dominantColor="Dominant color"
AdvSceneSwitcher.tempVar.video.dominantColor.description="The most dominant RGB color in a given video input frame in HexArgb format.\nDetermined using k-means clustering, which groups similar colors and returns the center of the largest group.\nDue to the nature of the algorithm the result may vary slightly between evaluations."
AdvSceneSwitcher.tempVar.websocket.message="Received websocket message"
AdvSceneSwitcher.tempVar.websocket.message.description="The received websocket message, which matched the given pattern"
@@ -2342,9 +2473,14 @@ AdvSceneSwitcher.tempVar.gameCapture.executable="Executable"
AdvSceneSwitcher.tempVar.gameCapture.executable.description="Executable name of the application captured by the source."
AdvSceneSwitcher.tempVar.http.status="Status code"
AdvSceneSwitcher.tempVar.http.body="Message body"
AdvSceneSwitcher.tempVar.http.error="Error"
AdvSceneSwitcher.tempVar.http.error.description="Empty when no error occurred.\nOther possible values:\n\n * Could not establish connection\n * Failed to bind IP address\n * Failed to read connection\n * Failed to write connection\n * Maximum redirect count exceeded\n * Connection handling canceled\n * SSL connection failed\n * SSL certificate loading failed\n * SSL server verification failed\n * Unsupported HTTP multipart boundary characters\n * Compression failed\n * Connection timed out\n * Proxy connection failed\n * Unknown"
AdvSceneSwitcher.tempVar.http.method="Method"
AdvSceneSwitcher.tempVar.http.method.description="Received HTTP request method"
AdvSceneSwitcher.tempVar.http.path="Request path"
AdvSceneSwitcher.tempVar.http.path.description="Received HTTP request path"
AdvSceneSwitcher.tempVar.http.body="Message body"
AdvSceneSwitcher.tempVar.http.body.description="Received HTTP request body"
AdvSceneSwitcher.tempVar.mqtt.message="Message"
@@ -2354,6 +2490,14 @@ AdvSceneSwitcher.tempVar.cursor.y="Cursor position (Y)"
AdvSceneSwitcher.tempVar.replay.lastSavePath="Last save path"
AdvSceneSwitcher.tempVar.replay.lastSavePath.description="The file path of the last replay buffer saved."
AdvSceneSwitcher.tempVar.sequence.macro="Macro"
AdvSceneSwitcher.tempVar.sequence.macro.description="The name of the macro executed by the \"Sequence\" action.\nIf no macro was executed the returned value is the empty string."
AdvSceneSwitcher.tempVar.sequence.nextMacro="Next Macro"
AdvSceneSwitcher.tempVar.sequence.nextMacro.description="The name of the macro which will be executed next after the index was set.\nIf no macro will be executed the returned value is the empty string."
AdvSceneSwitcher.tempVar.random.macro="Macro"
AdvSceneSwitcher.tempVar.random.macro.description="The name of the macro executed by the \"Random\" action.\nIf no macro was executed the returned value is the empty string."
AdvSceneSwitcher.selectScene="--select scene--"
AdvSceneSwitcher.selectCanvas="--select canvas--"
AdvSceneSwitcher.selectPreviousScene="Previous Scene"

View File

@@ -117,9 +117,7 @@ AdvSceneSwitcher.condition.scene.currentSceneTransitionBehaviour="Durante la tra
AdvSceneSwitcher.condition.scene.previousSceneTransitionBehaviour="Durante la transición, verifique la escena de origen de la transición"
AdvSceneSwitcher.condition.window="Ventana"
AdvSceneSwitcher.condition.file="Archivo"
AdvSceneSwitcher.condition.file.entry.line1="Contenido de{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.file.layout="Contenido de{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.media="Medios"
AdvSceneSwitcher.condition.media.anyOnScene="Cualquier fuente multimedia activada"
AdvSceneSwitcher.condition.media.allOnScene="Todas las fuentes de medios activadas"
@@ -334,7 +332,6 @@ AdvSceneSwitcher.action.recording.type.start="Iniciar grabación"
AdvSceneSwitcher.action.recording.type.pause="Pausar grabación"
AdvSceneSwitcher.action.recording.type.unpause="Reanudar grabación"
AdvSceneSwitcher.action.recording.pause.hint="Tenga en cuenta que, dependiendo de la configuración de grabación, es posible que no pueda pausar la grabación"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="Búfer de reproducción"
AdvSceneSwitcher.action.replay.saveWarn="Advertencia: ¡Guardar con demasiada frecuencia puede hacer que el búfer de reproducción no se guarde realmente!"
AdvSceneSwitcher.action.replay.type.stop="Detener el búfer de reproducción"
@@ -423,9 +420,9 @@ AdvSceneSwitcher.action.transition.type.scene="transición de escena"
AdvSceneSwitcher.action.transition.type.sceneOverride="anulación de transición de escena"
AdvSceneSwitcher.action.transition.type.sourceShow="transición del programa de origen"
AdvSceneSwitcher.action.transition.type.sourceHide="fuente ocultar transición"
AdvSceneSwitcher.action.transition.entry.line1="Modificar {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Establecer el tipo de transición en {{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Establecer la duración de la transición en {{duration}}segundos"
AdvSceneSwitcher.action.transition.layout.type="Modificar {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Establecer el tipo de transición en {{transitions}}"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Establecer la duración de la transición en {{duration}}segundos"
AdvSceneSwitcher.action.timer="Temporizador"
AdvSceneSwitcher.action.timer.type.pause="Pausa"
AdvSceneSwitcher.action.timer.type.continue="Continuar"

View File

@@ -187,8 +187,6 @@ AdvSceneSwitcher.condition.file="Fichier"
AdvSceneSwitcher.condition.file.type.match="correspond à"
AdvSceneSwitcher.condition.file.type.contentChange="a changé de contenu"
AdvSceneSwitcher.condition.file.type.dateChange="a changé de date de modification"
AdvSceneSwitcher.condition.file.remote="Fichier distant"
AdvSceneSwitcher.condition.file.local="Fichier local"
AdvSceneSwitcher.condition.media="Média"
AdvSceneSwitcher.condition.media.source="Source"
AdvSceneSwitcher.condition.media.anyOnScene="Toute source média sur la scène"
@@ -622,9 +620,9 @@ AdvSceneSwitcher.action.transition.type.scene="Transition de scène"
AdvSceneSwitcher.action.transition.type.sceneOverride="Remplacement de la transition de scène"
AdvSceneSwitcher.action.transition.type.sourceShow="Transition d'affichage de la source"
AdvSceneSwitcher.action.transition.type.sourceHide="Transition de masquage de la source"
AdvSceneSwitcher.action.transition.entry.line1="Modifier{{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Définir le type de transition sur{{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Définir la durée de la transition à{{duration}}secondes"
AdvSceneSwitcher.action.transition.layout.type="Modifier{{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Définir le type de transition sur{{transitions}}"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Définir la durée de la transition à{{duration}}secondes"
AdvSceneSwitcher.action.timer="Minuterie"
AdvSceneSwitcher.action.timer.type.pause="Mettre en pause"
AdvSceneSwitcher.action.timer.type.continue="Continuer"

View File

@@ -331,14 +331,6 @@ AdvSceneSwitcher.condition.window.entry.text="{{checkText}}ウィンドウにテ
AdvSceneSwitcher.condition.window.entry.text.note="このオプションは、ウィンドウに表示されているすべてのテキストに対して機能するとは限りません。\nその場合は、代わりにビデオ条件 OCR チェックの使用を検討してください。"
AdvSceneSwitcher.condition.window.entry.currentFocus="現在のフォーカスウィンドウ:{{focusWindow}}"
AdvSceneSwitcher.condition.file="ファイル"
; AdvSceneSwitcher.condition.file.type.match="matches"
AdvSceneSwitcher.condition.file.type.contentChange="内容変更しました"
AdvSceneSwitcher.condition.file.type.dateChange="更新日変更"
AdvSceneSwitcher.condition.file.remote="リモートファイル"
AdvSceneSwitcher.condition.file.local="ローカルファイル"
; AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
; AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
; AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
; AdvSceneSwitcher.condition.media="Media"
AdvSceneSwitcher.condition.media.checkType.state="状態が一致"
AdvSceneSwitcher.condition.media.checkType.time="時間が一致"
@@ -1020,9 +1012,9 @@ AdvSceneSwitcher.action.transition="トランジション"
; AdvSceneSwitcher.action.transition.type.sceneOverride="シーントランジションオーバーライド"
; AdvSceneSwitcher.action.transition.type.sourceShow="ソース表示トランジション"
; AdvSceneSwitcher.action.transition.type.sourceHide="ソース非表示トランジション"
; AdvSceneSwitcher.action.transition.entry.line1="{{type}}{{scenes}}{{sources}} の変更"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}トランジションタイプを{{transitions}}に設定します"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}移行時間を {{duration}} 秒に設定します"
; AdvSceneSwitcher.action.transition.layout.type="{{type}}{{scenes}}{{sources}} の変更"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}トランジションタイプを{{transitions}}に設定します"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}移行時間を {{duration}} 秒に設定します"
AdvSceneSwitcher.action.timer="タイマー"
AdvSceneSwitcher.action.timer.type.pause="一時停止"
AdvSceneSwitcher.action.timer.type.continue="続行"
@@ -1190,8 +1182,15 @@ AdvSceneSwitcher.action.twitch.reward.toggleControl="リワード名/変数選
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Twitchアカウントを選択しないとカテゴリを選択できません"
; AdvSceneSwitcher.action.twitch.layout.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}"
AdvSceneSwitcher.action.twitch.layout.chat="{{channel}}でアカウント{{account}}{{actions}}を使用しています"
AdvSceneSwitcher.action.twitch.layout.user.getInfo="{{userInfoQueryType}}{{userLogin}}{{userId}}でアカウント{{account}}{{actions}}を使用"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo="チャンネル{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}でアカウント{{account}}{{actions}}を使用"
AdvSceneSwitcher.action.twitch.layout.channel.getInfo="チャンネル{{channel}}でアカウント{{account}}{{actions}}を使用"
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row1="アカウント{{account}}{{actions}}を使用"
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row2="{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.user.moderation.row1="アカウント{{account}}{{actions}}を使用"
AdvSceneSwitcher.action.twitch.layout.user.moderation.row2="チャンネル{{channel}}{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.user.ban.row1="アカウント{{account}}{{actions}}を使用 タイムアウト{{duration}}"
AdvSceneSwitcher.action.twitch.layout.user.ban.row2="チャンネル{{channel}}{{userInfoQueryType}}{{userLogin}}{{userId}} 理由{{banReason}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row1="チャンネル{{channel}}でアカウント{{account}}{{actions}}を使用"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row2="{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
AdvSceneSwitcher.action.twitch.title.title="タイトルを入力"
AdvSceneSwitcher.action.twitch.marker.description="マーカーの説明"
AdvSceneSwitcher.action.twitch.clip.hasDelay="クリップをキャプチャする前にわずかな遅延を追加します"

View File

@@ -295,11 +295,6 @@ AdvSceneSwitcher.condition.file="Arquivo"
AdvSceneSwitcher.condition.file.type.match="corresponde"
AdvSceneSwitcher.condition.file.type.contentChange="conteúdo mudou"
AdvSceneSwitcher.condition.file.type.dateChange="data de modificação mudou"
AdvSceneSwitcher.condition.file.remote="Arquivo remoto"
AdvSceneSwitcher.condition.file.local="Arquivo local"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Mídia"
AdvSceneSwitcher.condition.media.checkType.state="Estado corresponde"
AdvSceneSwitcher.condition.media.checkType.time="Restrição de tempo corresponde"
@@ -754,7 +749,6 @@ AdvSceneSwitcher.action.recording.type.changeOutputFolder="Alterar pasta de saí
AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="Alterar formatação do nome do arquivo"
AdvSceneSwitcher.action.recording.pause.hint="Observe que dependendo das suas configurações de gravação, você pode não conseguir pausar a gravação"
AdvSceneSwitcher.action.recording.split.hint="Certifique-se de habilitar a divisão automática de arquivos nas configurações do OBS primeiro!"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="Buffer de replay"
AdvSceneSwitcher.action.replay.saveWarn="Aviso: Salvar com muita frequência pode resultar em o buffer de replay não ser realmente salvo!"
AdvSceneSwitcher.action.replay.durationWarn="Aviso: Alterar o tempo máximo de replay só se aplicará na próxima vez que o buffer de replay for iniciado!"
@@ -933,9 +927,9 @@ AdvSceneSwitcher.action.transition.type.scene="transição de cena"
AdvSceneSwitcher.action.transition.type.sceneOverride="substituição de transição de cena"
AdvSceneSwitcher.action.transition.type.sourceShow="transição de exibição de fonte"
AdvSceneSwitcher.action.transition.type.sourceHide="transição de ocultação de fonte"
AdvSceneSwitcher.action.transition.entry.line1="Modificar{{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Definir tipo de transição para{{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Definir duração da transição para{{duration}}segundos"
AdvSceneSwitcher.action.transition.layout.type="Modificar{{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Definir tipo de transição para{{transitions}}"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Definir duração da transição para{{duration}}segundos"
AdvSceneSwitcher.action.timer="Temporizador"
AdvSceneSwitcher.action.timer.type.pause="Pausar"
AdvSceneSwitcher.action.timer.type.continue="Continuar"
@@ -1065,7 +1059,7 @@ AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.enable="Habilitar modo apenas
AdvSceneSwitcher.action.twitch.type.chat.emoteOnly.disable="Desabilitar modo apenas emotes no chat"
AdvSceneSwitcher.action.twitch.type.chat.sendMessage="Enviar mensagem de chat"
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Não é possível selecionar categoria sem selecionar uma conta Twitch primeiro!"
AdvSceneSwitcher.action.twitch.layout.default="Em{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}"
AdvSceneSwitcher.action.twitch.layout.default="Em{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}{{nonModDelayDuration}}"
AdvSceneSwitcher.action.twitch.layout.chat="Usando conta{{account}}{{actions}}em{{channel}}"
AdvSceneSwitcher.action.twitch.title.title="Digite o título"
AdvSceneSwitcher.action.twitch.marker.description="Descreva o marcador"

View File

@@ -85,8 +85,6 @@ AdvSceneSwitcher.condition.scene.type.current="Текущий"
AdvSceneSwitcher.condition.scene.type.previous="Предыдущий"
AdvSceneSwitcher.condition.window="Окно"
AdvSceneSwitcher.condition.file="Файл"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.media="Медиа"
AdvSceneSwitcher.condition.video="Видео"
AdvSceneSwitcher.condition.video.condition.match="точно соответствует"
@@ -130,7 +128,6 @@ AdvSceneSwitcher.action.recording.type.start="Начать запись"
AdvSceneSwitcher.action.recording.type.pause="Пауза записи"
AdvSceneSwitcher.action.recording.type.unpause="Снять запись с паузы"
AdvSceneSwitcher.action.recording.pause.hint="Обратите внимание, что в зависимости от настроек записи вы можете не иметь возможности приостановить запись"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="Буфер воспроизведения"
AdvSceneSwitcher.action.replay.type.stop="Остановить буфер воспроизведения"
AdvSceneSwitcher.action.replay.type.start="Начать воспроизведение буфера"

View File

@@ -107,9 +107,7 @@ AdvSceneSwitcher.condition.scene.type.notChanged="Sahne değişmedi"
AdvSceneSwitcher.condition.scene.currentSceneTransitionBehaviour="Geçiş hedefi sahnesi için geçiş kontrolü sırasında"
AdvSceneSwitcher.condition.window="Pencere"
AdvSceneSwitcher.condition.file="Dosya"
AdvSceneSwitcher.condition.file.entry.line1="İçerik{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.file.layout="İçerik{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.media="Medya"
AdvSceneSwitcher.condition.media.anyOnScene="Herhangi bir medya kaynağı"
AdvSceneSwitcher.condition.media.allOnScene="Tüm medya kaynakları "
@@ -265,7 +263,6 @@ AdvSceneSwitcher.action.recording.type.start="Kayıt Başlat"
AdvSceneSwitcher.action.recording.type.pause="Kayıt Duraklat"
AdvSceneSwitcher.action.recording.type.unpause="Kayıt Duraklatma"
AdvSceneSwitcher.action.recording.pause.hint="Kayıt ayarlarınıza bağlı olarak kaydı duraklatamayabileceğinizi unutmayın."
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="Tekrar arabelleği"
AdvSceneSwitcher.action.replay.type.stop="Tekrar arabelleğini durdur"
AdvSceneSwitcher.action.replay.type.start="Tekrar arabelleğini başlat"
@@ -341,8 +338,8 @@ AdvSceneSwitcher.action.file.type.write="Yaz"
AdvSceneSwitcher.action.file.type.append="Ekle"
AdvSceneSwitcher.action.file.entry="{{actions}} {{filePath}}:"
AdvSceneSwitcher.action.transition="Geçiş"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}Geçiş türünü ayarla {{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}Geçiş süresini şuna ayarla: {{duration}}saniyeler"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}Geçiş türünü ayarla {{transitions}}"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}Geçiş süresini şuna ayarla: {{duration}}saniyeler"
AdvSceneSwitcher.action.timer="Zamanlayıcı"
AdvSceneSwitcher.action.timer.type.pause="Duraklat"
AdvSceneSwitcher.action.timer.type.continue="Devam et"

View File

@@ -318,11 +318,7 @@ AdvSceneSwitcher.condition.file="文件"
AdvSceneSwitcher.condition.file.type.match="内容匹配"
AdvSceneSwitcher.condition.file.type.contentChange="内容发生改变"
AdvSceneSwitcher.condition.file.type.dateChange="修改日期发生改变"
AdvSceneSwitcher.condition.file.remote="网络文件"
AdvSceneSwitcher.condition.file.local="本地文件"
AdvSceneSwitcher.condition.file.entry.line1="{{fileType}}{{filePath}}{{conditions}}{{useRegex}}"
AdvSceneSwitcher.condition.file.entry.line2="{{matchText}}"
AdvSceneSwitcher.condition.file.entry.line3="{{checkModificationDate}}{{checkFileContent}}"
AdvSceneSwitcher.condition.file.layout="{{filePath}}{{conditions}}{{regex}}"
AdvSceneSwitcher.condition.media="媒体"
AdvSceneSwitcher.condition.media.checkType.state="状态匹配"
AdvSceneSwitcher.condition.media.checkType.time="时间限制匹配"
@@ -813,7 +809,6 @@ AdvSceneSwitcher.action.recording.type.changeOutputFolder="更改输出文件夹
AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="改变文件名的格式"
AdvSceneSwitcher.action.recording.pause.hint="请注意,根据您的录制设置,您可能无法暂停录制"
AdvSceneSwitcher.action.recording.split.hint="注意请先确保在OBS设置中启用自动分割文件!"
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
AdvSceneSwitcher.action.replay="回放缓存"
AdvSceneSwitcher.action.replay.saveWarn="警告:保存过于频繁可能会导致回放缓存实际上未保存!"
AdvSceneSwitcher.action.replay.durationWarn="警告:更改回放缓存时长上限,仅适用于下次回放缓存开启时!"
@@ -994,9 +989,9 @@ AdvSceneSwitcher.action.transition.type.scene="场景转场动画"
AdvSceneSwitcher.action.transition.type.sceneOverride="覆盖场景转场动画"
AdvSceneSwitcher.action.transition.type.sourceShow="显示源转场动画"
AdvSceneSwitcher.action.transition.type.sourceHide="隐藏源转场动画"
AdvSceneSwitcher.action.transition.entry.line1="更改 {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.entry.line2="{{setTransition}}将转场动画类型设置为 {{transitions}}"
AdvSceneSwitcher.action.transition.entry.line3="{{setDuration}}将转场动画时长设置为 {{duration}}秒"
AdvSceneSwitcher.action.transition.layout.type="更改 {{type}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.transition.layout.transition="{{setTransition}}将转场动画类型设置为 {{transitions}}"
AdvSceneSwitcher.action.transition.layout.duration="{{setDuration}}将转场动画时长设置为 {{duration}}秒"
AdvSceneSwitcher.action.timer="计时器"
AdvSceneSwitcher.action.timer.type.pause="暂停"
AdvSceneSwitcher.action.timer.type.continue="继续"
@@ -1139,10 +1134,17 @@ AdvSceneSwitcher.action.twitch.type.user.getInfo="获取用户信息"
AdvSceneSwitcher.action.twitch.type.reward.getInfo="获取频道积分奖励信息"
AdvSceneSwitcher.action.twitch.reward.toggleControl="切换奖励名称/变量选择控制"
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="在未选择 Twitch 帐户的情况下无法选择类别!"
AdvSceneSwitcher.action.twitch.layout.default="{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}"
AdvSceneSwitcher.action.twitch.layout.default="{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{channel}}{{pointsReward}}{{nonModDelayDuration}}"
AdvSceneSwitcher.action.twitch.layout.chat="使用账户{{account}}{{actions}}{{channel}}"
AdvSceneSwitcher.action.twitch.layout.user.getInfo="使用账户{{account}}{{actions}}{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo="使用账户{{account}}{{actions}} 频道{{channel}}{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
AdvSceneSwitcher.action.twitch.layout.channel.getInfo="使用账户{{account}}{{actions}}频道{{channel}}"
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row1="使用账户{{account}}{{actions}}"
AdvSceneSwitcher.action.twitch.layout.user.getInfo.row2="{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.user.moderation.row1="使用账户{{account}}{{actions}}"
AdvSceneSwitcher.action.twitch.layout.user.moderation.row2="频道{{channel}}{{userInfoQueryType}}{{userLogin}}{{userId}}"
AdvSceneSwitcher.action.twitch.layout.user.ban.row1="使用账户{{account}}{{actions}}超时{{duration}}"
AdvSceneSwitcher.action.twitch.layout.user.ban.row2="频道{{channel}}{{userInfoQueryType}}{{userLogin}}{{userId}}原因{{banReason}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row1="使用账户{{account}}{{actions}} 频道{{channel}}"
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row2="{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
AdvSceneSwitcher.action.twitch.title.title="输入标题"
AdvSceneSwitcher.action.twitch.marker.description="标记描述"
AdvSceneSwitcher.action.twitch.clip.hasDelay="在捕捉视频片段前稍加延迟"

View File

@@ -251,6 +251,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="suppressCrashRecoveryDialog">
<property name="text">
<string>AdvSceneSwitcher.generalTab.generalBehavior.suppressCrashRecoveryDialog</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -597,7 +604,7 @@
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QGroupBox" name="groupBox_5">
<widget class="QGroupBox" name="macroListBox">
<property name="title">
<string>AdvSceneSwitcher.macroTab.macros</string>
</property>

View File

@@ -66,6 +66,7 @@ public slots:
void on_uiHintsDisable_stateChanged(int state);
void on_disableComboBoxFilter_stateChanged(int state);
void on_warnPluginLoadFailure_stateChanged(int state);
void on_suppressCrashRecoveryDialog_stateChanged(int state);
void on_hideLegacyTabs_stateChanged(int state);
void on_priorityUp_clicked();
void on_priorityDown_clicked();

View File

@@ -1,4 +1,5 @@
#include "advanced-scene-switcher.hpp"
#include "crash-handler.hpp"
#include "file-selection.hpp"
#include "filter-combo-box.hpp"
#include "first-run-wizard.hpp"
@@ -194,6 +195,15 @@ void AdvSceneSwitcher::on_warnPluginLoadFailure_stateChanged(int state)
switcher->warnPluginLoadFailure = state;
}
void AdvSceneSwitcher::on_suppressCrashRecoveryDialog_stateChanged(int state)
{
if (loading) {
return;
}
SetSuppressCrashDialog(state);
}
static bool isLegacyTab(const QString &name)
{
return name == obs_module_text(
@@ -946,6 +956,7 @@ void AdvSceneSwitcher::SetupGeneralTab()
FilterComboBox::SetFilterBehaviourEnabled(
!switcher->disableFilterComboboxFilter);
ui->warnPluginLoadFailure->setChecked(switcher->warnPluginLoadFailure);
ui->suppressCrashRecoveryDialog->setChecked(GetSuppressCrashDialog());
ui->hideLegacyTabs->setChecked(switcher->hideLegacyTabs);
populatePriorityFunctionList(ui->priorityList);

View File

@@ -4,6 +4,8 @@
#include "macro.hpp"
#include "macro-action-factory.hpp"
#include <chrono>
namespace advss {
const std::string MacroActionMacro::id = "macro";
@@ -118,6 +120,36 @@ bool MacroActionMacro::PerformAction()
case Action::TOGGLE_ACTION:
AdjustActionState(macro);
break;
case Action::GET_INFO: {
SetTempVarValue(
"conditionCount",
std::to_string(macro->Conditions().size()));
SetTempVarValue(
"actionCount",
std::to_string(macro->Actions().size()));
SetTempVarValue(
"elseActionCount",
std::to_string(macro->ElseActions().size()));
SetTempVarValue("paused",
macro->Paused() ? "true" : "false");
SetTempVarValue("runCount",
std::to_string(macro->RunCount()));
const auto lastRun = macro->GetLastExecutionTime();
const bool hasRun =
lastRun.time_since_epoch().count() != 0;
const auto secondsSinceLastRun =
hasRun ? std::chrono::duration_cast<
std::chrono::seconds>(
std::chrono::
high_resolution_clock::
now() -
lastRun)
.count()
: -1;
SetTempVarValue("secondsSinceLastRun",
std::to_string(secondsSinceLastRun));
break;
}
default:
break;
}
@@ -175,6 +207,9 @@ void MacroActionMacro::LogAction() const
case Action::NESTED_MACRO:
ablog(LOG_INFO, "run nested macro");
break;
case Action::GET_INFO:
ablog(LOG_INFO, "get info for \"%s\"", macro->Name().c_str());
break;
default:
break;
}
@@ -202,8 +237,8 @@ bool MacroActionMacro::Save(obs_data_t *obj) const
bool MacroActionMacro::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_action = static_cast<MacroActionMacro::Action>(
obs_data_get_int(obj, "action"));
SetAction(static_cast<MacroActionMacro::Action>(
obs_data_get_int(obj, "action")));
_macro.Load(obj);
_actionSelectionType = static_cast<SelectionType>(
obs_data_get_int(obj, "actionSelectionType"));
@@ -303,6 +338,56 @@ void MacroActionMacro::RunActions(Macro *actionMacro) const
}
}
void MacroActionMacro::SetAction(Action action)
{
_action = action;
SetupTempVars();
}
void MacroActionMacro::SetupTempVars()
{
MacroAction::SetupTempVars();
if (_action != Action::GET_INFO) {
return;
}
AddTempvar(
"conditionCount",
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.conditionCount"),
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.conditionCount.description"));
AddTempvar(
"actionCount",
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.actionCount"),
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.actionCount.description"));
AddTempvar(
"elseActionCount",
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.elseActionCount"),
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.elseActionCount.description"));
AddTempvar(
"paused",
obs_module_text("AdvSceneSwitcher.tempVar.macro.info.paused"),
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.paused.description"));
AddTempvar(
"runCount",
obs_module_text("AdvSceneSwitcher.tempVar.macro.info.runCount"),
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.runCount.description"));
AddTempvar(
"secondsSinceLastRun",
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.secondsSinceLastRun"),
obs_module_text(
"AdvSceneSwitcher.tempVar.macro.info.secondsSinceLastRun.description"));
}
static void populateActionSelection(QComboBox *list)
{
static const std::vector<std::pair<MacroActionMacro::Action, std::string>>
@@ -327,6 +412,8 @@ static void populateActionSelection(QComboBox *list)
"AdvSceneSwitcher.action.macro.type.enableAction"},
{MacroActionMacro::Action::TOGGLE_ACTION,
"AdvSceneSwitcher.action.macro.type.toggleAction"},
{MacroActionMacro::Action::GET_INFO,
"AdvSceneSwitcher.action.macro.type.getInfo"},
};
for (const auto &[value, name] : actions) {
@@ -496,7 +583,7 @@ void MacroActionMacroEdit::UpdateEntryData()
}
_actions->setCurrentIndex(
_actions->findData(static_cast<int>(_entryData->_action)));
_actions->findData(static_cast<int>(_entryData->GetAction())));
_actionSelectionType->setCurrentIndex(_actionSelectionType->findData(
static_cast<int>(_entryData->_actionSelectionType)));
_actionIndex->SetValue(_entryData->_actionIndex);
@@ -545,8 +632,8 @@ void MacroActionMacroEdit::MacroChanged(const QString &text)
void MacroActionMacroEdit::ActionChanged(int idx)
{
GUARD_LOADING_AND_LOCK();
_entryData->_action = static_cast<MacroActionMacro::Action>(
_actions->itemData(idx).toInt());
_entryData->SetAction(static_cast<MacroActionMacro::Action>(
_actions->itemData(idx).toInt()));
SetWidgetVisibility();
}
@@ -665,7 +752,7 @@ void MacroActionMacroEdit::SetWidgetVisibility()
};
const auto action = _entryData->_action;
const auto action = _entryData->GetAction();
const char *layoutText = "";
switch (action) {
case MacroActionMacro::Action::PAUSE:
@@ -674,6 +761,7 @@ void MacroActionMacroEdit::SetWidgetVisibility()
case MacroActionMacro::Action::RESET_COUNTER:
case MacroActionMacro::Action::STOP:
case MacroActionMacro::Action::NESTED_MACRO:
case MacroActionMacro::Action::GET_INFO:
layoutText = "AdvSceneSwitcher.action.macro.layout.other";
break;
case MacroActionMacro::Action::RUN_ACTIONS:

View File

@@ -56,11 +56,14 @@ public:
TOGGLE_ACTION,
TOGGLE_PAUSE,
NESTED_MACRO,
GET_INFO,
};
void SetAction(Action);
Action GetAction() const { return _action; }
enum class SelectionType { INDEX, LABEL, ID };
Action _action = Action::NESTED_MACRO;
SelectionType _actionSelectionType = SelectionType::INDEX;
bool _useElseSection = false;
IntVariable _actionIndex = 1;
@@ -72,9 +75,12 @@ public:
int _customWidgetHeight = 0;
private:
void SetupTempVars();
void RunActions(Macro *actionMacro) const;
void AdjustActionState(Macro *) const;
Action _action = Action::NESTED_MACRO;
static bool _registered;
static const std::string id;
};

View File

@@ -17,6 +17,14 @@ namespace advss {
const std::string MacroActionVariable::id = "variable";
std::vector<TempVariableRef> MacroActionVariable::GetTempVarRefs() const
{
if (!_tempVar.HasValidID()) {
return {};
}
return {_tempVar};
}
bool MacroActionVariable::_registered = MacroActionFactory::Register(
MacroActionVariable::id,
{MacroActionVariable::Create, MacroActionVariableEdit::Create,
@@ -1450,6 +1458,7 @@ void MacroActionVariableEdit::SelectionChanged(const TempVariableRef &var)
{
GUARD_LOADING_AND_LOCK();
_entryData->_tempVar = var;
IncrementTempVarInUseGeneration();
SetWidgetVisibility();
}

View File

@@ -23,6 +23,7 @@ public:
bool PostLoad();
std::string GetShortDesc() const;
std::string GetId() const { return id; };
std::vector<TempVariableRef> GetTempVarRefs() const;
static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const;
void SetSegmentIndexValue(int);

View File

@@ -6,6 +6,14 @@ namespace advss {
const std::string MacroConditionTempVar::id = "temp_var";
std::vector<TempVariableRef> MacroConditionTempVar::GetTempVarRefs() const
{
if (!_tempVar.HasValidID()) {
return {};
}
return {_tempVar};
}
bool MacroConditionTempVar::_registered = MacroConditionFactory::Register(
MacroConditionTempVar::id,
{MacroConditionTempVar::Create, MacroConditionTempVarEdit::Create,
@@ -266,6 +274,7 @@ void MacroConditionTempVarEdit::VariableChanged(const TempVariableRef &var)
{
GUARD_LOADING_AND_LOCK();
_entryData->_tempVar = var;
IncrementTempVarInUseGeneration();
}
void MacroConditionTempVarEdit::Variable2Changed(const QString &text)

View File

@@ -20,6 +20,7 @@ public:
bool Load(obs_data_t *obj);
std::string GetShortDesc() const;
std::string GetId() const { return id; };
std::vector<TempVariableRef> GetTempVarRefs() const;
static std::shared_ptr<MacroCondition> Create(Macro *m)
{
return std::make_shared<MacroConditionTempVar>(m);

View File

@@ -366,7 +366,7 @@ static void runSegmentHighlightChecksHelper(MacroSegmentList *list)
// highlight its segments if required
auto macroAction = dynamic_cast<MacroActionMacro *>(data.get());
if (macroAction &&
macroAction->_action ==
macroAction->GetAction() ==
MacroActionMacro::Action::NESTED_MACRO) {
continue;
}
@@ -1086,6 +1086,11 @@ void MacroEdit::AddMacroAction(Macro *macro, int idx, const std::string &id,
HighlightAction(idx);
ui->actionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() {
ui->actionsList->ensureWidgetVisible(
ui->actionsList->WidgetAt(currentActionIdx));
});
}
void MacroEdit::AddMacroAction(int idx)
@@ -1389,6 +1394,11 @@ void MacroEdit::AddMacroElseAction(Macro *macro, int idx, const std::string &id,
HighlightElseAction(idx);
ui->elseActionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() {
ui->elseActionsList->ensureWidgetVisible(
ui->elseActionsList->WidgetAt(currentElseActionIdx));
});
}
void MacroEdit::AddMacroElseAction(int idx)
@@ -1591,6 +1601,11 @@ void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id,
HighlightCondition(idx);
ui->conditionsList->SetHelpMsgVisible(false);
emit(MacroSegmentOrderChanged());
QTimer::singleShot(0, this, [this]() {
ui->conditionsList->ensureWidgetVisible(
ui->conditionsList->WidgetAt(currentConditionIdx));
});
}
void MacroEdit::on_conditionAdd_clicked()

View File

@@ -13,6 +13,11 @@
namespace advss {
std::vector<TempVariableRef> MacroSegment::GetTempVarRefs() const
{
return {};
}
MacroSegment::MacroSegment(Macro *m, bool supportsVariableValue)
: _macro(m),
_supportsVariableValue(supportsVariableValue)
@@ -152,6 +157,16 @@ void MacroSegment::AddTempvar(const std::string &id, const std::string &name,
NotifyUIAboutTempVarChange(this);
}
bool MacroSegment::IsTempVarInUse(const std::string &id) const
{
for (const auto &var : _tempVariables) {
if (var.ID() == id) {
return var.IsInUse();
}
}
return false;
}
void MacroSegment::SetTempVarValue(const std::string &id,
const std::string &value)
{

View File

@@ -34,6 +34,7 @@ public:
std::string GetCustomLabel() const { return _customLabel; }
virtual bool Save(obs_data_t *obj) const = 0;
virtual bool Load(obs_data_t *obj) = 0;
virtual std::vector<TempVariableRef> GetTempVarRefs() const;
virtual bool PostLoad();
virtual std::string GetShortDesc() const;
virtual std::string GetId() const = 0;
@@ -54,6 +55,7 @@ protected:
void AddTempvar(const std::string &id, const std::string &name,
const std::string &description = "");
bool IsTempVarInUse(const std::string &id) const;
void SetTempVarValue(const std::string &id, const std::string &value);
template<typename T, typename = std::enable_if_t<
@@ -64,6 +66,14 @@ protected:
: std::string("false"));
}
template<typename F, typename = std::enable_if_t<std::is_invocable_v<F>>>
void SetTempVarValue(const std::string &id, F &&valueProvider)
{
if (IsTempVarInUse(id)) {
SetTempVarValue(id, valueProvider());
}
}
private:
void ClearAvailableTempvars();
std::optional<const TempVariable>

View File

@@ -670,18 +670,6 @@ void AdvSceneSwitcher::SetupMacroTab()
SLOT(HighlightOnChange()));
onChangeHighlightTimer.start();
// Reserve more space for macro edit area than for the macro list
ui->macroListMacroEditSplitter->setStretchFactor(0, 1);
ui->macroListMacroEditSplitter->setStretchFactor(1, 4);
if (switcher->saveWindowGeo) {
if (shouldRestoreSplitter(
switcher->macroListMacroEditSplitterPosition)) {
ui->macroListMacroEditSplitter->setSizes(
switcher->macroListMacroEditSplitterPosition);
}
}
SetupMacroSearchWidgets(ui->macroSearchLayout, ui->macroSearchText,
ui->macroSearchClear, ui->macroSearchType,
ui->macroSearchRegex,
@@ -705,6 +693,26 @@ void AdvSceneSwitcher::SetupMacroTab()
ui->actionTriggerMode->addItem(obs_module_text(name),
static_cast<int>(mode));
}
ui->macroListBox->setSizePolicy(QSizePolicy::Ignored,
QSizePolicy::Preferred);
ui->macroListBox->setMinimumWidth(0);
ui->macroEditGroup->setSizePolicy(QSizePolicy::Ignored,
QSizePolicy::Preferred);
ui->macroEditGroup->setMinimumWidth(0);
if (shouldRestoreSplitter(
switcher->macroListMacroEditSplitterPosition)) {
ui->macroListMacroEditSplitter->setSizes(
switcher->macroListMacroEditSplitterPosition);
} else {
QTimer::singleShot(0, this, [this]() {
const auto totalWidth =
ui->macroListMacroEditSplitter->width();
ui->macroListMacroEditSplitter->setSizes(
{totalWidth / 5, totalWidth * 4 / 5});
});
}
}
void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos)

View File

@@ -2,17 +2,18 @@
#include "log-helper.hpp"
#include "obs-module-helper.hpp"
#include "plugin-state-helpers.hpp"
#include "ui-helpers.hpp"
#include <obs-frontend-api.h>
#include <obs-module.h>
#include <QCheckBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QLabel>
#include <QMainWindow>
#include <QTimer>
#include <thread>
#include <QVBoxLayout>
namespace advss {
@@ -25,10 +26,29 @@ static constexpr bool handleUncleanShutdown = true;
#endif
static bool wasCleanShutdown = false;
static bool suppressCrashDialog = false;
bool GetSuppressCrashDialog()
{
return suppressCrashDialog;
}
void SetSuppressCrashDialog(bool suppress)
{
suppressCrashDialog = suppress;
}
static void setup();
static bool setupDone = []() {
AddPluginInitStep(setup);
AddSaveStep([](obs_data_t *obj) {
obs_data_set_bool(obj, "suppressCrashDialog",
suppressCrashDialog);
});
AddLoadStep([](obs_data_t *obj) {
suppressCrashDialog =
obs_data_get_bool(obj, "suppressCrashDialog");
});
return true;
}();
@@ -108,8 +128,39 @@ static bool wasUncleanShutdown()
static void askForStartupSkip()
{
bool skipStart = DisplayMessage(
obs_module_text("AdvSceneSwitcher.crashDetected"), true, false);
auto mainWindow =
static_cast<QMainWindow *>(obs_frontend_get_main_window());
auto dialog = new QDialog(mainWindow);
dialog->setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
dialog->setWindowFlags(dialog->windowFlags() &
~Qt::WindowContextHelpButtonHint);
auto layout = new QVBoxLayout(dialog);
auto label = new QLabel(
obs_module_text("AdvSceneSwitcher.crashDetected"), dialog);
label->setWordWrap(true);
layout->addWidget(label);
auto checkbox = new QCheckBox(
obs_module_text(
"AdvSceneSwitcher.crashDetected.suppressCheckbox"),
dialog);
layout->addWidget(checkbox);
auto buttonbox = new QDialogButtonBox(
QDialogButtonBox::Yes | QDialogButtonBox::No, dialog);
QObject::connect(buttonbox, &QDialogButtonBox::accepted, dialog,
&QDialog::accept);
QObject::connect(buttonbox, &QDialogButtonBox::rejected, dialog,
&QDialog::reject);
layout->addWidget(buttonbox);
dialog->setLayout(layout);
bool skipStart = dialog->exec() == QDialog::Accepted;
suppressCrashDialog = checkbox->isChecked();
dialog->deleteLater();
if (!skipStart) {
StartPlugin();
}
@@ -121,6 +172,10 @@ bool ShouldSkipPluginStartOnUncleanShutdown()
return false;
}
if (suppressCrashDialog) {
return false;
}
// This function is called while the plugin settings are being loaded.
// Blocking at this stage can cause issues such as OBS failing to start
// or crashing.

View File

@@ -4,4 +4,7 @@ namespace advss {
bool ShouldSkipPluginStartOnUncleanShutdown();
bool GetSuppressCrashDialog();
void SetSuppressCrashDialog(bool suppress);
} // namespace advss

View File

@@ -7,9 +7,11 @@
namespace advss {
FileSelection::FileSelection(FileSelection::Type type, QWidget *parent)
FileSelection::FileSelection(FileSelection::Type type, QWidget *parent,
const QString &browseTitle)
: QWidget(parent),
_type(type),
_browseTitle(browseTitle),
_filePath(new VariableLineEdit(this)),
_browseButton(
new QPushButton(obs_module_text("AdvSceneSwitcher.browse")))
@@ -55,11 +57,14 @@ void FileSelection::BrowseButtonClicked()
QString defaultPath = ValidPathOrDesktop(_filePath->text());
QString path;
if (_type == FileSelection::Type::WRITE) {
path = QFileDialog::getSaveFileName(this, "", defaultPath);
path = QFileDialog::getSaveFileName(this, _browseTitle,
defaultPath);
} else if (_type == FileSelection::Type::READ) {
path = QFileDialog::getOpenFileName(this, "", defaultPath);
path = QFileDialog::getOpenFileName(this, _browseTitle,
defaultPath);
} else {
path = QFileDialog::getExistingDirectory(this, "", defaultPath);
path = QFileDialog::getExistingDirectory(this, _browseTitle,
defaultPath);
}
if (path.isEmpty()) {

View File

@@ -19,7 +19,7 @@ public:
EXPORT
FileSelection(FileSelection::Type type = FileSelection::Type::READ,
QWidget *parent = 0);
QWidget *parent = 0, const QString &browseTitle = "");
EXPORT void SetPath(const StringVariable &);
EXPORT void SetPath(const QString &);
EXPORT QString GetPath() const;
@@ -34,6 +34,7 @@ signals:
private:
Type _type;
QString _browseTitle;
VariableLineEdit *_filePath;
QPushButton *_browseButton;
};

View File

@@ -32,6 +32,17 @@ ListControls::ListControls(QWidget *parent, bool reorder) : QToolBar(parent)
}
}
void ListControls::AddWidget(QWidget *widget)
{
addSeparator();
addWidget(widget);
}
void ListControls::AddSeparator()
{
addSeparator();
}
void ListControls::AddActionHelper(const char *theme, const char *className,
const char *tooltip,
const std::function<void()> &signal)

View File

@@ -10,6 +10,8 @@ class ADVSS_EXPORT ListControls final : public QToolBar {
public:
ListControls(QWidget *parent = nullptr, bool reorder = true);
void AddWidget(QWidget *widget);
void AddSeparator();
signals:
void Add();

View File

@@ -1,13 +1,16 @@
#include "list-editor.hpp"
#include "ui-helpers.hpp"
#include <QEvent>
namespace advss {
ListEditor::ListEditor(QWidget *parent, bool reorder)
: QWidget(parent),
_list(new QListWidget()),
_controls(new ListControls(this, reorder)),
_mainLayout(new QVBoxLayout())
_mainLayout(new QVBoxLayout()),
_placeholder(new QLabel(_list->viewport()))
{
QWidget::connect(_controls, SIGNAL(Add()), this, SLOT(Add()));
QWidget::connect(_controls, SIGNAL(Remove()), this, SLOT(Remove()));
@@ -15,6 +18,19 @@ ListEditor::ListEditor(QWidget *parent, bool reorder)
QWidget::connect(_controls, SIGNAL(Down()), this, SLOT(Down()));
QWidget::connect(_list, SIGNAL(itemDoubleClicked(QListWidgetItem *)),
this, SLOT(Clicked(QListWidgetItem *)));
QWidget::connect(_list->model(),
SIGNAL(rowsInserted(QModelIndex, int, int)), this,
SLOT(UpdatePlaceholder()));
QWidget::connect(_list->model(),
SIGNAL(rowsRemoved(QModelIndex, int, int)), this,
SLOT(UpdatePlaceholder()));
QWidget::connect(_list->model(), SIGNAL(modelReset()), this,
SLOT(UpdatePlaceholder()));
_placeholder->setAlignment(Qt::AlignCenter);
_placeholder->setWordWrap(true);
_placeholder->hide();
_list->viewport()->installEventFilter(this);
_mainLayout->setContentsMargins(0, 0, 0, 0);
_mainLayout->addWidget(_list);
@@ -22,6 +38,40 @@ ListEditor::ListEditor(QWidget *parent, bool reorder)
setLayout(_mainLayout);
}
void ListEditor::SetPlaceholderText(const QString &text)
{
_placeholder->setText(text);
UpdatePlaceholder();
}
void ListEditor::SetMinListHeight(int value)
{
_minHeight = value;
}
void ListEditor::SetMaxListHeight(int value)
{
_maxHeight = value;
}
void ListEditor::UpdatePlaceholder()
{
bool visible = !_placeholder->text().isEmpty() && _list->count() == 0;
_placeholder->setVisible(visible);
if (visible) {
_placeholder->setGeometry(_list->viewport()->rect());
}
}
bool ListEditor::eventFilter(QObject *obj, QEvent *event)
{
if (obj == _list->viewport() && event->type() == QEvent::Resize &&
_placeholder->isVisible()) {
_placeholder->setGeometry(_list->viewport()->rect());
}
return QWidget::eventFilter(obj, event);
}
void ListEditor::showEvent(QShowEvent *e)
{
QWidget::showEvent(e);
@@ -64,6 +114,24 @@ int ListEditor::GetIndexOfSignal() const
void ListEditor::UpdateListSize()
{
SetHeightToContentHeight(_list);
if (_list->count() == 0 && !_placeholder->text().isEmpty()) {
auto height = _list->fontMetrics().height() * 3;
_list->setMinimumHeight(height);
_list->setMaximumHeight(height);
}
if (_minHeight >= 0 && _list->minimumHeight() != _minHeight) {
_list->setMinimumHeight(_minHeight);
}
if (_maxHeight >= 0 && _list->maximumHeight() != _maxHeight) {
if (_list->minimumHeight() > _maxHeight) {
_list->setMinimumHeight(_maxHeight);
}
_list->setMaximumHeight(_maxHeight);
}
adjustSize();
updateGeometry();
}

View File

@@ -2,6 +2,7 @@
#include "export-symbol-helper.hpp"
#include "list-controls.hpp"
#include <QLabel>
#include <QListWidget>
#include <QLayout>
@@ -13,9 +14,13 @@ class ADVSS_EXPORT ListEditor : public QWidget {
public:
ListEditor(QWidget *parent = nullptr, bool reorder = true);
int count() const { return _list->count(); };
void SetPlaceholderText(const QString &text);
void SetMinListHeight(int);
void SetMaxListHeight(int);
protected:
void showEvent(QShowEvent *);
bool eventFilter(QObject *, QEvent *);
private slots:
virtual void Add() = 0;
@@ -23,6 +28,7 @@ private slots:
virtual void Up(){};
virtual void Down(){};
virtual void Clicked(QListWidgetItem *) {}
void UpdatePlaceholder();
protected:
void UpdateListSize();
@@ -31,6 +37,11 @@ protected:
QListWidget *_list;
ListControls *_controls;
QVBoxLayout *_mainLayout;
private:
QLabel *_placeholder;
int _minHeight = -1;
int _maxHeight = -1;
};
} // namespace advss

View File

@@ -9,19 +9,15 @@ namespace advss {
std::variant<double, std::string> EvalMathExpression(const std::string &expr)
{
static bool setupDone = false;
static exprtk::symbol_table<double> symbolTable;
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_real_distribution<double> dis(0.0, 1.0);
static auto randomFunc = []() {
thread_local std::mt19937 gen(std::random_device{}());
thread_local std::uniform_real_distribution<double> dis(0.0,
1.0);
return dis(gen);
};
if (!setupDone) {
symbolTable.add_function("random", randomFunc);
setupDone = true;
}
exprtk::symbol_table<double> symbolTable;
symbolTable.add_function("random", randomFunc);
exprtk::expression<double> expression;
expression.register_symbol_table(symbolTable);

View File

@@ -66,4 +66,15 @@ void Lockable::WithLock(const std::function<void()> &func)
func();
}
SuspendLock::SuspendLock(Lockable &lockable)
: _mtx(static_cast<std::mutex &>(lockable._mtx))
{
_mtx.unlock();
}
SuspendLock::~SuspendLock()
{
_mtx.lock();
}
} // namespace advss

View File

@@ -51,6 +51,24 @@ public:
private:
PerInstanceMutex _mtx;
friend class SuspendLock;
};
// RAII guard that temporarily releases a Lockable's per-segment lock.
// Use this inside PerformAction() / CheckCondition() to unblock the UI
// during long-running operations while still running on the original segment.
// The caller MUST be holding the lock (i.e. be inside WithLock) when
// constructing this object; the lock is re-acquired on destruction.
class EXPORT SuspendLock {
public:
SuspendLock(Lockable &lockable);
~SuspendLock();
SuspendLock(const SuspendLock &) = delete;
SuspendLock &operator=(const SuspendLock &) = delete;
private:
std::mutex &_mtx;
};
} // namespace advss

View File

@@ -10,9 +10,12 @@
#include <QVariant>
#include <QAbstractItemView>
#include <atomic>
Q_DECLARE_METATYPE(advss::TempVariableRef);
static std::atomic<uint64_t> tempVarInUseGeneration{0};
namespace advss {
TempVariable::TempVariable(const std::string &id, const std::string &name,
@@ -120,6 +123,60 @@ TempVariableRef TempVariable::GetRef() const
return ref;
}
static bool refsContain(const std::vector<TempVariableRef> &refs,
const TempVariableRef &ref)
{
for (const auto &r : refs) {
if (r == ref) {
return true;
}
}
return false;
}
template<typename T>
static bool segmentsReferTo(const T &segments, const TempVariableRef &ref)
{
for (const auto &segment : segments) {
if (refsContain(segment->GetTempVarRefs(), ref)) {
return true;
}
}
return false;
}
bool TempVariable::IsInUse() const
{
const auto currentGen =
tempVarInUseGeneration.load(std::memory_order_relaxed);
if (_isInUseCacheGeneration == currentGen) {
return _isInUseCache;
}
bool inUse = false;
const auto ref = GetRef();
if (ref.HasValidID()) {
for (const auto &macro : GetAllMacros()) {
if (segmentsReferTo(macro->Conditions(), ref) ||
segmentsReferTo(macro->Actions(), ref) ||
segmentsReferTo(macro->ElseActions(), ref)) {
inUse = true;
break;
}
}
}
_isInUseCache = inUse;
_isInUseCacheGeneration = currentGen;
return inUse;
}
void IncrementTempVarInUseGeneration()
{
tempVarInUseGeneration.fetch_add(1, std::memory_order_relaxed);
}
TempVariableRef::SegmentType TempVariableRef::GetType() const
{
auto segment = _segment.lock();
@@ -665,13 +722,13 @@ TempVarSignalManager *TempVarSignalManager::Instance()
void NotifyUIAboutTempVarChange(MacroSegment *segment)
{
obs_queue_task(
OBS_TASK_UI,
IncrementTempVarInUseGeneration();
QueueUITask(
[](void *segment) {
TempVarSignalManager::Instance()->SegmentTempVarsChanged(
(MacroSegment *)segment);
},
segment, false);
segment);
}
} // namespace advss

View File

@@ -44,6 +44,7 @@ public:
void SetValue(const std::string &val);
void InvalidateValue();
TempVariableRef GetRef() const;
EXPORT bool IsInUse() const;
private:
std::string _id = "";
@@ -53,6 +54,8 @@ private:
mutable std::mutex _lastValuesMutex;
std::vector<std::string> _lastValues;
bool _valueIsValid = false;
mutable bool _isInUseCache = false;
mutable uint64_t _isInUseCacheGeneration = UINT64_MAX;
std::weak_ptr<MacroSegment> _segment;
friend TempVariableSelection;
@@ -122,5 +125,6 @@ private:
};
void NotifyUIAboutTempVarChange(MacroSegment *);
EXPORT void IncrementTempVarInUseGeneration();
} // namespace advss

View File

@@ -9,11 +9,12 @@ void StringVariable::Resolve() const
_resolvedValue = _value;
return;
}
if (_lastResolve == GetLastVariableChangeTime()) {
const auto lastChange = GetLastVariableChangeTime();
if (_lastResolve == lastChange) {
return;
}
_resolvedValue = SubstitueVariables(_value);
_lastResolve = GetLastVariableChangeTime();
_lastResolve = lastChange;
}
StringVariable::operator std::string() const
@@ -81,7 +82,7 @@ std::string SubstitueVariables(std::string str)
const auto &variable = std::dynamic_pointer_cast<Variable>(v);
const std::string pattern = "${" + variable->Name() + "}";
if (ReplaceAll(str, pattern, variable->Value(false))) {
variable->UpdateLastUsed();
variable->MarkAsUsed();
}
}
return str;

View File

@@ -12,16 +12,23 @@ static std::deque<std::shared_ptr<Item>> variables;
// Keep track of the last time a variable was changed to save some work when
// when resolving strings containing variables, etc.
static std::mutex lastVariableChangeMutex;
static std::chrono::high_resolution_clock::time_point lastVariableChange{};
static void setLastVariableChangeTime()
{
std::lock_guard<std::mutex> lock(lastVariableChangeMutex);
lastVariableChange = std::chrono::high_resolution_clock::now();
}
Variable::Variable() : Item()
{
lastVariableChange = std::chrono::high_resolution_clock::now();
setLastVariableChangeTime();
}
Variable::~Variable()
{
lastVariableChange = std::chrono::high_resolution_clock::now();
setLastVariableChangeTime();
}
void Variable::Load(obs_data_t *obj)
@@ -37,7 +44,7 @@ void Variable::Load(obs_data_t *obj)
SetValue(_defaultValue);
}
lastVariableChange = std::chrono::high_resolution_clock::now();
setLastVariableChangeTime();
}
void Variable::Save(obs_data_t *obj) const
@@ -45,8 +52,11 @@ void Variable::Save(obs_data_t *obj) const
Item::Save(obj);
obs_data_set_int(obj, "saveAction", static_cast<int>(_saveAction));
if (_saveAction == SaveAction::SAVE) {
obs_data_set_string(obj, "value", _value.c_str());
{
std::lock_guard<std::mutex> lock(_mutex);
if (_saveAction == SaveAction::SAVE) {
obs_data_set_string(obj, "value", _value.c_str());
}
}
obs_data_set_string(obj, "defaultValue", _defaultValue.c_str());
@@ -62,6 +72,18 @@ std::string Variable::Value(bool updateLastUsed) const
return _value;
}
std::string Variable::GetPreviousValue() const
{
std::lock_guard<std::mutex> lock(_mutex);
return _previousValue;
}
int Variable::GetValueChangeCount() const
{
std::lock_guard<std::mutex> lock(_mutex);
return _valueChangeCount;
}
std::optional<double> Variable::DoubleValue() const
{
return GetDouble(Value());
@@ -79,8 +101,11 @@ void Variable::SetValue(const std::string &value)
_value = value;
UpdateLastUsed();
UpdateLastChanged();
lastVariableChange = std::chrono::high_resolution_clock::now();
if (_previousValue != _value) {
_lastChanged = std::chrono::high_resolution_clock::now();
++_valueChangeCount;
}
setLastVariableChangeTime();
}
void Variable::SetValue(double value)
@@ -90,6 +115,7 @@ void Variable::SetValue(double value)
std::optional<uint64_t> Variable::GetSecondsSinceLastUse() const
{
std::lock_guard<std::mutex> lock(_mutex);
if (_lastUsed.time_since_epoch().count() == 0) {
return {};
}
@@ -101,6 +127,7 @@ std::optional<uint64_t> Variable::GetSecondsSinceLastUse() const
std::optional<uint64_t> Variable::GetSecondsSinceLastChange() const
{
std::lock_guard<std::mutex> lock(_mutex);
if (_lastChanged.time_since_epoch().count() == 0) {
return {};
}
@@ -116,12 +143,10 @@ void Variable::UpdateLastUsed() const
_lastUsed = std::chrono::high_resolution_clock::now();
}
void Variable::UpdateLastChanged()
void Variable::MarkAsUsed() const
{
if (_previousValue != _value) {
_lastChanged = std::chrono::high_resolution_clock::now();
++_valueChangeCount;
}
std::lock_guard<std::mutex> lock(_mutex);
UpdateLastUsed();
}
static void populateSaveActionSelection(QComboBox *list)
@@ -206,7 +231,7 @@ bool VariableSettingsDialog::AskForSettings(QWidget *parent, Variable &settings)
dialog._defaultValue->toPlainText().toStdString();
settings._saveAction =
static_cast<Variable::SaveAction>(dialog._save->currentIndex());
lastVariableChange = std::chrono::high_resolution_clock::now();
setLastVariableChangeTime();
return true;
}
@@ -461,6 +486,7 @@ void ImportVariables(obs_data_t *data)
std::chrono::high_resolution_clock::time_point GetLastVariableChangeTime()
{
std::lock_guard<std::mutex> lock(lastVariableChangeMutex);
return lastVariableChange;
}

View File

@@ -34,16 +34,15 @@ public:
EXPORT std::string Value(bool updateLastUsed = true) const;
EXPORT std::optional<double> DoubleValue() const;
EXPORT std::optional<int> IntValue() const;
std::string GetPreviousValue() const { return _previousValue; };
std::string GetPreviousValue() const;
std::string GetDefaultValue() const { return _defaultValue; }
EXPORT void SetValue(const std::string &value);
void SetValue(double value);
SaveAction GetSaveAction() const { return _saveAction; }
int GetValueChangeCount() const { return _valueChangeCount; }
int GetValueChangeCount() const;
std::optional<uint64_t> GetSecondsSinceLastUse() const;
std::optional<uint64_t> GetSecondsSinceLastChange() const;
void UpdateLastUsed() const;
void UpdateLastChanged();
void MarkAsUsed() const;
private:
SaveAction _saveAction = SaveAction::DONT_SAVE;
@@ -55,6 +54,8 @@ private:
mutable std::chrono::high_resolution_clock::time_point _lastChanged;
mutable std::mutex _mutex;
void UpdateLastUsed() const;
friend VariableSelection;
friend VariableSettingsDialog;
};

View File

@@ -25,6 +25,8 @@ target_sources(
macro-action-obs-settings.hpp
macro-action-osc.cpp
macro-action-osc.hpp
macro-action-play-audio.cpp
macro-action-play-audio.hpp
macro-action-plugin-state.cpp
macro-action-plugin-state.hpp
macro-action-profile.cpp
@@ -57,6 +59,8 @@ target_sources(
macro-action-sequence.hpp
macro-action-source.cpp
macro-action-source.hpp
macro-action-source-interaction.cpp
macro-action-source-interaction.hpp
macro-action-streaming.cpp
macro-action-streaming.hpp
macro-action-studio-mode.cpp
@@ -144,7 +148,17 @@ target_sources(
target_sources(
${PROJECT_NAME}
PRIVATE utils/audio-helpers.cpp
PRIVATE utils/source-interaction-recorder.cpp
utils/source-interaction-recorder.hpp
utils/source-interaction-step.cpp
utils/source-interaction-step.hpp
utils/source-interaction-step-edit.cpp
utils/source-interaction-step-edit.hpp
utils/source-interaction-step-list.cpp
utils/source-interaction-step-list.hpp
utils/source-preview-widget.cpp
utils/source-preview-widget.hpp
utils/audio-helpers.cpp
utils/audio-helpers.hpp
utils/connection-manager.cpp
utils/connection-manager.hpp

View File

@@ -9,6 +9,14 @@ namespace advss {
const std::string MacroActionFilter::id = "filter";
std::vector<TempVariableRef> MacroActionFilter::GetTempVarRefs() const
{
if (!_tempVar.HasValidID()) {
return {};
}
return {_tempVar};
}
bool MacroActionFilter::_registered = MacroActionFactory::Register(
MacroActionFilter::id,
{MacroActionFilter::Create, MacroActionFilterEdit::Create,
@@ -428,6 +436,7 @@ void MacroActionFilterEdit::SelectionChanged(const TempVariableRef &var)
{
GUARD_LOADING_AND_LOCK();
_entryData->_tempVar = var;
IncrementTempVarInUseGeneration();
}
void MacroActionFilterEdit::SelectionChanged(const SourceSetting &setting)

View File

@@ -41,6 +41,8 @@ public:
SettingsInputMethod _settingsInputMethod =
SettingsInputMethod::INDIVIDUAL_MANUAL;
std::vector<TempVariableRef> GetTempVarRefs() const;
SourceSelection _source;
FilterSelection _filter;
Action _action = Action::ENABLE;

View File

@@ -95,7 +95,7 @@ static void waitHelper(std::unique_lock<std::mutex> *lock, Macro *macro,
}
}
void MacroActionMedia::PerformActionHelper(obs_source_t *source) const
void MacroActionMedia::PerformActionHelper(obs_source_t *source)
{
obs_media_state state = obs_source_media_get_state(source);
@@ -130,6 +130,7 @@ void MacroActionMedia::PerformActionHelper(obs_source_t *source) const
SeekToPercentage(source);
break;
case Action::WAIT_FOR_PLAYBACK_STOP: {
SuspendLock suspendLock(*this);
std::unique_lock<std::mutex> lock(*GetMutex());
waitHelper(&lock, GetMacro(), source);
break;

View File

@@ -45,7 +45,7 @@ public:
SceneSelection _scene;
private:
void PerformActionHelper(obs_source_t *) const;
void PerformActionHelper(obs_source_t *);
void SeekToPercentage(obs_source_t *source) const;
static bool _registered;

View File

@@ -0,0 +1,446 @@
#include "macro-action-play-audio.hpp"
#include "audio-helpers.hpp"
#include "layout-helpers.hpp"
#include "macro-helpers.hpp"
#include "sync-helpers.hpp"
#include <chrono>
#include <QFileInfo>
#include <QLabel>
namespace advss {
// Use a high output channel index that is unlikely to be claimed by OBS or
// other plugins. OBS supports channels 0-63; channel 0 is the main scene.
static constexpr uint32_t kPlaybackOutputChannel = 63;
const std::string MacroActionPlayAudio::id = "play_audio";
bool MacroActionPlayAudio::_registered = MacroActionFactory::Register(
MacroActionPlayAudio::id,
{MacroActionPlayAudio::Create, MacroActionPlayAudioEdit::Create,
"AdvSceneSwitcher.action.playAudio"});
static void deactivatePlayback(obs_source_t *source, bool wantsOutput)
{
if (wantsOutput) {
obs_set_output_source(kPlaybackOutputChannel, nullptr);
} else {
obs_source_dec_active(source);
}
}
static void waitForPlaybackToEnd(Macro *macro, obs_source_t *source,
int64_t maxMs = 0)
{
using namespace std::chrono_literals;
std::unique_lock<std::mutex> lock(*GetMutex());
SetMacroAbortWait(false);
// The media source needs time to open and decode before reaching
// PLAYING state. Poll until it starts (or the macro is stopped).
while (!MacroWaitShouldAbort() && !MacroIsStopped(macro)) {
if (obs_source_media_get_state(source) ==
OBS_MEDIA_STATE_PLAYING) {
break;
}
GetMacroWaitCV().wait_for(lock, 10ms);
}
// Now wait for playback to end. Require two consecutive non-playing
// samples to avoid false positives on brief state transitions.
// If maxMs > 0, also stop once that many milliseconds have elapsed.
const auto playbackStart = std::chrono::steady_clock::now();
static const int kStopThreshold = 2;
int stoppedCount = 0;
while (!MacroWaitShouldAbort() && !MacroIsStopped(macro)) {
if (maxMs > 0) {
const auto elapsed =
std::chrono::duration_cast<
std::chrono::milliseconds>(
std::chrono::steady_clock::now() -
playbackStart)
.count();
if (elapsed >= maxMs) {
obs_source_media_stop(source);
break;
}
}
if (obs_source_media_get_state(source) !=
OBS_MEDIA_STATE_PLAYING) {
if (++stoppedCount >= kStopThreshold) {
break;
}
} else {
stoppedCount = 0;
}
GetMacroWaitCV().wait_for(lock, 10ms);
}
}
bool MacroActionPlayAudio::PerformAction()
{
std::string path = _filePath;
if (path.empty()) {
return true;
}
if (!QFileInfo::exists(QString::fromStdString(path))) {
blog(LOG_WARNING, "audio file not found: \"%s\"", path.c_str());
return true;
}
OBSDataAutoRelease settings = obs_data_create();
obs_data_set_string(settings, "local_file", path.c_str());
obs_data_set_bool(settings, "is_local_file", true);
obs_data_set_bool(settings, "looping", false);
// Disable automatic restart on activate so we control start explicitly.
obs_data_set_bool(settings, "restart_on_activate", false);
obs_data_set_bool(settings, "close_when_inactive", true);
OBSSourceAutoRelease source = obs_source_create_private(
"ffmpeg_source", "advss_play_audio", settings);
if (!source) {
blog(LOG_WARNING,
"Failed to create ffmpeg_source for audio playback of \"%s\"",
path.c_str());
return true;
}
const float vol =
DecibelToPercent(static_cast<float>(_volumeDB.GetValue()));
obs_source_set_volume(source, vol);
obs_source_set_monitoring_type(source, _monitorType);
// Fall back to monitor-only if all output tracks are deselected —
// there is no point routing through the output channel if the mixer
// mask would silence every track.
const bool wantsOutput =
(_monitorType != OBS_MONITORING_TYPE_MONITOR_ONLY) &&
(_audioMixers != 0);
if (!wantsOutput &&
_monitorType == OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT) {
obs_source_set_monitoring_type(
source, OBS_MONITORING_TYPE_MONITOR_ONLY);
}
if (wantsOutput) {
obs_source_set_audio_mixers(source, _audioMixers);
// Route through a private scene so we can position the scene
// item far off-screen. This keeps the item "visible" (so audio
// is still mixed) while ensuring its video never intersects the
// output frame.
OBSSceneAutoRelease audioScene =
obs_scene_create_private("advss_audio_scene");
obs_sceneitem_t *item = obs_scene_add(audioScene, source);
if (item) {
struct vec2 pos = {-99999.0f, -99999.0f};
obs_sceneitem_set_pos(item, &pos);
}
obs_set_output_source(kPlaybackOutputChannel,
obs_scene_get_source(audioScene));
// audioScene released here; the output channel holds the
// remaining reference and keeps the scene alive.
} else {
obs_source_set_audio_mixers(source, 0);
obs_source_inc_active(source);
}
if (_useStartOffset && _startOffset.Milliseconds() > 0) {
obs_source_media_set_time(source, _startOffset.Milliseconds());
}
obs_source_media_play_pause(source, false);
const int64_t maxMs = _useDuration ? _playbackDuration.Milliseconds()
: 0;
if (_waitForCompletion) {
SetMacroAbortWait(false);
{
SuspendLock suspendLock(*this);
waitForPlaybackToEnd(GetMacro(), source, maxMs);
}
deactivatePlayback(source, wantsOutput);
return true;
}
// Keep the source alive in a background thread that cleans up
// once playback finishes. Grab an extra strong reference so the
// source survives beyond this stack frame.
auto rawSource = obs_source_get_ref(source);
auto macro = GetMacro();
std::thread cleanupThread([rawSource, wantsOutput, macro, maxMs]() {
waitForPlaybackToEnd(macro, rawSource, maxMs);
deactivatePlayback(rawSource, wantsOutput);
obs_source_release(rawSource);
});
AddMacroHelperThread(macro, std::move(cleanupThread));
return true;
}
void MacroActionPlayAudio::LogAction() const
{
ablog(LOG_INFO,
"playing audio file \"%s\" at %.1f dB (monitoring type %d, wait %d)",
_filePath.UnresolvedValue().c_str(),
static_cast<double>(_volumeDB.GetFixedValue()), _monitorType,
(int)_waitForCompletion);
}
bool MacroActionPlayAudio::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
_filePath.Save(obj, "filePath");
_volumeDB.Save(obj, "volumeDB");
obs_data_set_int(obj, "monitorType", _monitorType);
obs_data_set_int(obj, "audioMixers", _audioMixers);
obs_data_set_bool(obj, "useStartOffset", _useStartOffset);
_startOffset.Save(obj, "startOffset");
obs_data_set_bool(obj, "useDuration", _useDuration);
_playbackDuration.Save(obj, "playbackDuration");
obs_data_set_bool(obj, "waitForCompletion", _waitForCompletion);
return true;
}
bool MacroActionPlayAudio::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_filePath.Load(obj, "filePath");
_volumeDB.Load(obj, "volumeDB");
_monitorType = static_cast<obs_monitoring_type>(
obs_data_get_int(obj, "monitorType"));
_audioMixers =
static_cast<uint32_t>(obs_data_get_int(obj, "audioMixers"));
_useStartOffset = obs_data_get_bool(obj, "useStartOffset");
_startOffset.Load(obj, "startOffset");
_useDuration = obs_data_get_bool(obj, "useDuration");
_playbackDuration.Load(obj, "playbackDuration");
_waitForCompletion = obs_data_get_bool(obj, "waitForCompletion");
return true;
}
std::string MacroActionPlayAudio::GetShortDesc() const
{
return _filePath.UnresolvedValue();
}
std::shared_ptr<MacroAction> MacroActionPlayAudio::Create(Macro *m)
{
return std::make_shared<MacroActionPlayAudio>(m);
}
std::shared_ptr<MacroAction> MacroActionPlayAudio::Copy() const
{
return std::make_shared<MacroActionPlayAudio>(*this);
}
void MacroActionPlayAudio::ResolveVariablesToFixedValues()
{
_filePath.ResolveVariables();
_volumeDB.ResolveVariables();
_startOffset.ResolveVariables();
_playbackDuration.ResolveVariables();
}
MacroActionPlayAudioEdit::MacroActionPlayAudioEdit(
QWidget *parent, std::shared_ptr<MacroActionPlayAudio> entryData)
: QWidget(parent),
_filePath(new FileSelection(
FileSelection::Type::READ, this,
obs_module_text(
"AdvSceneSwitcher.action.playAudio.file.browse"))),
_volumeDB(new VariableDoubleSpinBox),
_monitorTypes(new QComboBox),
_tracksContainer(new QWidget),
_useStartOffset(new QCheckBox(this)),
_startOffset(new DurationSelection(this, true, 0.0)),
_useDuration(new QCheckBox(this)),
_playbackDuration(new DurationSelection(this, true, 0.0)),
_waitForCompletion(new QCheckBox(
obs_module_text("AdvSceneSwitcher.action.playAudio.wait")))
{
_volumeDB->setMinimum(-100.0);
_volumeDB->setMaximum(0.0);
_volumeDB->setSuffix("dB");
_volumeDB->setSpecialValueText("-inf");
if (obs_audio_monitoring_available()) {
PopulateMonitorTypeSelection(_monitorTypes);
} else {
_monitorTypes->addItem(obs_module_text(
"AdvSceneSwitcher.action.playAudio.monitorUnavailable"));
_monitorTypes->setEnabled(false);
}
for (int i = 0; i < 6; ++i) {
_tracks[i] = new QCheckBox(QString::number(i + 1));
QWidget::connect(_tracks[i], SIGNAL(stateChanged(int)), this,
SLOT(TrackChanged()));
}
QWidget::connect(_filePath, SIGNAL(PathChanged(const QString &)), this,
SLOT(FilePathChanged(const QString &)));
QWidget::connect(
_volumeDB,
SIGNAL(NumberVariableChanged(const NumberVariable<double> &)),
this, SLOT(VolumeDBChanged(const NumberVariable<double> &)));
QWidget::connect(_monitorTypes, SIGNAL(currentIndexChanged(int)), this,
SLOT(MonitorTypeChanged(int)));
QWidget::connect(_useStartOffset, SIGNAL(stateChanged(int)), this,
SLOT(UseStartOffsetChanged(int)));
QWidget::connect(_useDuration, SIGNAL(stateChanged(int)), this,
SLOT(UseDurationChanged(int)));
QWidget::connect(_startOffset,
SIGNAL(DurationChanged(const Duration &)), this,
SLOT(StartOffsetChanged(const Duration &)));
QWidget::connect(_playbackDuration,
SIGNAL(DurationChanged(const Duration &)), this,
SLOT(PlaybackDurationChanged(const Duration &)));
QWidget::connect(_waitForCompletion, SIGNAL(stateChanged(int)), this,
SLOT(WaitChanged(int)));
auto tracksLayout = new QHBoxLayout;
tracksLayout->setContentsMargins(0, 0, 0, 0);
tracksLayout->addWidget(new QLabel(
obs_module_text("AdvSceneSwitcher.action.playAudio.tracks")));
for (auto *cb : _tracks) {
tracksLayout->addWidget(cb);
}
tracksLayout->addStretch();
_tracksContainer->setLayout(tracksLayout);
auto volumeLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.action.playAudio.layout.volume"),
volumeLayout, {{"{{volumeDB}}", _volumeDB}});
auto monitorLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.playAudio.layout.monitor"),
monitorLayout, {{"{{monitorTypes}}", _monitorTypes}});
auto startOffsetLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.playAudio.layout.startOffset"),
startOffsetLayout,
{{"{{useStartOffset}}", _useStartOffset},
{"{{startOffset}}", _startOffset}});
auto playbackDurationLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.playAudio.layout.playbackDuration"),
playbackDurationLayout,
{{"{{useDuration}}", _useDuration},
{"{{playbackDuration}}", _playbackDuration}});
auto mainLayout = new QVBoxLayout;
mainLayout->addWidget(_filePath);
mainLayout->addLayout(volumeLayout);
mainLayout->addLayout(monitorLayout);
mainLayout->addWidget(_tracksContainer);
mainLayout->addLayout(startOffsetLayout);
mainLayout->addLayout(playbackDurationLayout);
mainLayout->addWidget(_waitForCompletion);
setLayout(mainLayout);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionPlayAudioEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_filePath->SetPath(_entryData->_filePath);
_volumeDB->SetValue(_entryData->_volumeDB);
_monitorTypes->setCurrentIndex(
static_cast<int>(_entryData->_monitorType));
for (int i = 0; i < 6; ++i) {
_tracks[i]->setChecked(_entryData->_audioMixers & (1u << i));
}
_tracksContainer->setVisible(_entryData->_monitorType !=
OBS_MONITORING_TYPE_MONITOR_ONLY);
_useStartOffset->setChecked(_entryData->_useStartOffset);
_startOffset->SetDuration(_entryData->_startOffset);
_startOffset->setEnabled(_entryData->_useStartOffset);
_useDuration->setChecked(_entryData->_useDuration);
_playbackDuration->SetDuration(_entryData->_playbackDuration);
_playbackDuration->setEnabled(_entryData->_useDuration);
_waitForCompletion->setChecked(_entryData->_waitForCompletion);
}
void MacroActionPlayAudioEdit::FilePathChanged(const QString &path)
{
GUARD_LOADING_AND_LOCK();
_entryData->_filePath = path.toStdString();
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}
void MacroActionPlayAudioEdit::VolumeDBChanged(
const NumberVariable<double> &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_volumeDB = value;
}
void MacroActionPlayAudioEdit::MonitorTypeChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_monitorType = static_cast<obs_monitoring_type>(value);
_tracksContainer->setVisible(value != OBS_MONITORING_TYPE_MONITOR_ONLY);
}
void MacroActionPlayAudioEdit::TrackChanged()
{
GUARD_LOADING_AND_LOCK();
uint32_t mixers = 0;
for (int i = 0; i < 6; ++i) {
if (_tracks[i]->isChecked()) {
mixers |= (1u << i);
}
}
_entryData->_audioMixers = mixers;
}
void MacroActionPlayAudioEdit::UseStartOffsetChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_useStartOffset = value;
_startOffset->setEnabled(_entryData->_useStartOffset);
}
void MacroActionPlayAudioEdit::StartOffsetChanged(const Duration &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_startOffset = value;
}
void MacroActionPlayAudioEdit::UseDurationChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_useDuration = value;
_playbackDuration->setEnabled(_entryData->_useDuration);
}
void MacroActionPlayAudioEdit::PlaybackDurationChanged(const Duration &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_playbackDuration = value;
}
void MacroActionPlayAudioEdit::WaitChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_waitForCompletion = value;
}
} // namespace advss

View File

@@ -0,0 +1,91 @@
#pragma once
#include "macro-action-edit.hpp"
#include "duration-control.hpp"
#include "file-selection.hpp"
#include "variable-spinbox.hpp"
#include <obs.hpp>
#include <array>
#include <QCheckBox>
#include <QComboBox>
#include <QWidget>
namespace advss {
class MacroActionPlayAudio : public MacroAction {
public:
MacroActionPlayAudio(Macro *m) : MacroAction(m) {}
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetShortDesc() const;
std::string GetId() const { return id; }
static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const;
void ResolveVariablesToFixedValues();
StringVariable _filePath =
obs_module_text("AdvSceneSwitcher.enterPath");
DoubleVariable _volumeDB = 0.0;
obs_monitoring_type _monitorType = OBS_MONITORING_TYPE_MONITOR_ONLY;
uint32_t _audioMixers = 0x3F; // tracks 1-6, all on by default
bool _useStartOffset = false;
Duration _startOffset;
bool _useDuration = false;
Duration _playbackDuration;
bool _waitForCompletion = false;
static bool _registered;
static const std::string id;
};
class MacroActionPlayAudioEdit : public QWidget {
Q_OBJECT
public:
MacroActionPlayAudioEdit(
QWidget *parent,
std::shared_ptr<MacroActionPlayAudio> entryData = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionPlayAudioEdit(
parent, std::dynamic_pointer_cast<MacroActionPlayAudio>(
action));
}
private slots:
void FilePathChanged(const QString &path);
void VolumeDBChanged(const NumberVariable<double> &value);
void MonitorTypeChanged(int value);
void TrackChanged();
void UseStartOffsetChanged(int);
void StartOffsetChanged(const Duration &);
void UseDurationChanged(int);
void PlaybackDurationChanged(const Duration &);
void WaitChanged(int value);
signals:
void HeaderInfoChanged(const QString &);
private:
FileSelection *_filePath;
VariableDoubleSpinBox *_volumeDB;
QComboBox *_monitorTypes;
QWidget *_tracksContainer;
std::array<QCheckBox *, 6> _tracks;
QCheckBox *_useStartOffset;
DurationSelection *_startOffset;
QCheckBox *_useDuration;
DurationSelection *_playbackDuration;
QCheckBox *_waitForCompletion;
std::shared_ptr<MacroActionPlayAudio> _entryData;
bool _loading = true;
};
} // namespace advss

View File

@@ -47,20 +47,28 @@ getNextMacros(std::vector<MacroRef> &macros, MacroRef &lastRandomMacroRef,
bool MacroActionRandom::PerformAction()
{
if (_macros.size() == 0) {
SetTempVarValue("macro", "");
return true;
}
auto macros = getNextMacros(_macros, lastRandomMacro, _allowRepeat);
if (macros.size() == 0) {
SetTempVarValue("macro", "");
return true;
}
if (macros.size() == 1) {
lastRandomMacro = macros[0];
SetTempVarValue("macro", GetMacroName(macros[0].get()));
return RunMacroActions(macros[0].get());
}
srand((unsigned int)time(0));
size_t idx = std::rand() % (macros.size());
lastRandomMacro = macros[idx];
SetTempVarValue("macro", GetMacroName(macros[idx].get()));
return RunMacroActions(macros[idx].get());
}
@@ -85,6 +93,11 @@ bool MacroActionRandom::Load(obs_data_t *obj)
return true;
}
bool MacroActionRandom::PostLoad()
{
return MacroAction::PostLoad() && MultiMacroRefAction::PostLoad();
}
std::shared_ptr<MacroAction> MacroActionRandom::Create(Macro *m)
{
return std::make_shared<MacroActionRandom>(m);
@@ -95,6 +108,16 @@ std::shared_ptr<MacroAction> MacroActionRandom::Copy() const
return std::make_shared<MacroActionRandom>(*this);
}
void MacroActionRandom::SetupTempVars()
{
MacroAction::SetupTempVars();
AddTempvar(
"macro",
obs_module_text("AdvSceneSwitcher.tempVar.random.macro"),
obs_module_text(
"AdvSceneSwitcher.tempVar.random.macro.description"));
}
MacroActionRandomEdit::MacroActionRandomEdit(
QWidget *parent, std::shared_ptr<MacroActionRandom> entryData)
: QWidget(parent),

View File

@@ -15,6 +15,7 @@ public:
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
bool PostLoad();
std::string GetId() const { return id; };
static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const;
@@ -22,7 +23,10 @@ public:
bool _allowRepeat = false;
private:
void SetupTempVars();
MacroRef lastRandomMacro;
static bool _registered;
static const std::string id;
};

View File

@@ -22,12 +22,18 @@ const static std::map<MacroActionRecord::Action, std::string> actionTypes = {
"AdvSceneSwitcher.action.recording.type.pause"},
{MacroActionRecord::Action::UNPAUSE,
"AdvSceneSwitcher.action.recording.type.unpause"},
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(28, 0, 0)
{MacroActionRecord::Action::SPLIT,
"AdvSceneSwitcher.action.recording.type.split"},
#endif
{MacroActionRecord::Action::FOLDER,
"AdvSceneSwitcher.action.recording.type.changeOutputFolder"},
{MacroActionRecord::Action::FILE_FORMAT,
"AdvSceneSwitcher.action.recording.type.changeOutputFileFormat"},
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(30, 2, 0)
{MacroActionRecord::Action::ADD_CHAPTER,
"AdvSceneSwitcher.action.recording.type.addChapter"},
#endif
};
bool MacroActionRecord::PerformAction()
@@ -87,6 +93,13 @@ bool MacroActionRecord::PerformAction()
}
break;
}
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(30, 2, 0)
case Action::ADD_CHAPTER:
if (!obs_frontend_recording_add_chapter(_chapterName.c_str())) {
blog(LOG_WARNING, "failed to add recoding chapter!");
}
break;
#endif
default:
break;
}
@@ -110,6 +123,7 @@ bool MacroActionRecord::Save(obs_data_t *obj) const
obs_data_set_int(obj, "action", static_cast<int>(_action));
_folder.Save(obj, "folder");
_fileFormat.Save(obj, "format");
_chapterName.Save(obj, "chapterName");
return true;
}
@@ -119,6 +133,7 @@ bool MacroActionRecord::Load(obs_data_t *obj)
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
_folder.Load(obj, "folder");
_fileFormat.Load(obj, "format");
_chapterName.Load(obj, "chapterName");
return true;
}
@@ -136,6 +151,7 @@ void MacroActionRecord::ResolveVariablesToFixedValues()
{
_folder.ResolveVariables();
_fileFormat.ResolveVariables();
_chapterName.ResolveVariables();
}
static inline void populateActionSelection(QComboBox *list)
@@ -154,7 +170,8 @@ MacroActionRecordEdit::MacroActionRecordEdit(
_splitHint(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.recording.split.hint"))),
_recordFolder(new FileSelection(FileSelection::Type::FOLDER, this)),
_recordFileFormat(new VariableLineEdit(this))
_recordFileFormat(new VariableLineEdit(this)),
_chapterName(new VariableLineEdit(this))
{
populateActionSelection(_actions);
@@ -164,6 +181,8 @@ MacroActionRecordEdit::MacroActionRecordEdit(
this, SLOT(FolderChanged(const QString &)));
QWidget::connect(_recordFileFormat, SIGNAL(editingFinished()), this,
SLOT(FormatStringChanged()));
QWidget::connect(_chapterName, SIGNAL(editingFinished()), this,
SLOT(ChapterNameChanged()));
auto mainLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.recording.entry"),
@@ -172,7 +191,8 @@ MacroActionRecordEdit::MacroActionRecordEdit(
{"{{pauseHint}}", _pauseHint},
{"{{splitHint}}", _splitHint},
{"{{recordFolder}}", _recordFolder},
{"{{recordFileFormat}}", _recordFileFormat}});
{"{{recordFileFormat}}", _recordFileFormat},
{"{{chapterName}}", _chapterName}});
setLayout(mainLayout);
_entryData = entryData;
@@ -188,6 +208,7 @@ void MacroActionRecordEdit::UpdateEntryData()
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_recordFolder->SetPath(_entryData->_folder);
_recordFileFormat->setText(_entryData->_fileFormat);
_chapterName->setText(_entryData->_chapterName);
SetWidgetVisibility();
}
@@ -209,6 +230,12 @@ void MacroActionRecordEdit::FormatStringChanged()
_entryData->_fileFormat = _recordFileFormat->text().toStdString();
}
void MacroActionRecordEdit::ChapterNameChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_chapterName = _chapterName->text().toStdString();
}
void MacroActionRecordEdit::SetWidgetVisibility()
{
_pauseHint->setVisible(isPauseAction(_entryData->_action));
@@ -218,6 +245,8 @@ void MacroActionRecordEdit::SetWidgetVisibility()
MacroActionRecord::Action::FOLDER);
_recordFileFormat->setVisible(_entryData->_action ==
MacroActionRecord::Action::FILE_FORMAT);
_chapterName->setVisible(_entryData->_action ==
MacroActionRecord::Action::ADD_CHAPTER);
}
void MacroActionRecordEdit::ActionChanged(int value)

View File

@@ -29,11 +29,13 @@ public:
SPLIT,
FOLDER,
FILE_FORMAT,
ADD_CHAPTER,
};
Action _action = Action::STOP;
StringVariable _folder = QDir::homePath().toStdString() + "/Videos";
StringVariable _fileFormat = "%CCYY-%MM-%DD %hh-%mm-%ss";
StringVariable _chapterName = "";
private:
static bool _registered;
@@ -60,6 +62,7 @@ private slots:
void ActionChanged(int value);
void FolderChanged(const QString &);
void FormatStringChanged();
void ChapterNameChanged();
protected:
QComboBox *_actions;
@@ -67,6 +70,7 @@ protected:
QLabel *_splitHint;
FileSelection *_recordFolder;
VariableLineEdit *_recordFileFormat;
VariableLineEdit *_chapterName;
std::shared_ptr<MacroActionRecord> _entryData;
private:

View File

@@ -15,9 +15,20 @@ bool MacroActionRun::_registered = MacroActionFactory::Register(
bool MacroActionRun::PerformAction()
{
if (_wait) {
_procConfig.StartProcessAndWait(_timeout.Milliseconds());
SetTempVarValues();
// Snapshot config before releasing the lock for the blocking wait
auto procConfig = _procConfig;
const auto timeout = _timeout.Milliseconds();
{
SuspendLock suspendLock(*this);
procConfig.StartProcessAndWait(timeout);
}
SetTempVarValue("process.id", procConfig.GetProcessId());
SetTempVarValue("process.exitCode",
procConfig.GetProcessExitCode());
SetTempVarValue("process.stream.output",
procConfig.GetProcessOutputStream());
SetTempVarValue("process.stream.error",
procConfig.GetProcessErrorStream());
return true;
}

View File

@@ -82,13 +82,22 @@ void MacroActionSequence::ResolveVariablesToFixedValues()
_resetIndex.ResolveVariables();
}
void MacroActionSequence::SetAction(Action action)
{
_action = action;
SetupTempVars();
}
bool MacroActionSequence::RunSequence()
{
if (_macros.size() == 0) {
SetTempVarValue("macro", "");
return true;
}
auto macro = GetNextMacro().GetMacro();
SetTempVarValue("macro", GetMacroName(macro.get()));
if (!macro.get()) {
return true;
}
@@ -96,18 +105,21 @@ bool MacroActionSequence::RunSequence()
return RunMacroActions(macro.get());
}
bool MacroActionSequence::SetSequenceIndex() const
bool MacroActionSequence::SetSequenceIndex()
{
auto macro = _macro.GetMacro();
if (!macro) {
SetTempVarValue("nextMacro", "");
return true;
}
auto actions = GetMacroActions(macro.get());
if (!actions) {
SetTempVarValue("nextMacro", "");
return true;
}
std::string nextMacroName;
for (const auto &action : *actions) {
if (action->GetId() != id) {
continue;
@@ -121,10 +133,34 @@ bool MacroActionSequence::SetSequenceIndex() const
// -2 is needed since the _lastIndex starts at -1 and the reset
// index starts at 1
sequenceAction->_lastIdx = _resetIndex - 2;
nextMacroName = GetMacroName(
sequenceAction->GetNextMacro(false).GetMacro().get());
}
SetTempVarValue("nextMacro", nextMacroName);
return true;
}
void MacroActionSequence::SetupTempVars()
{
MacroAction::SetupTempVars();
if (_action == Action::RUN_SEQUENCE) {
AddTempvar(
"macro",
obs_module_text(
"AdvSceneSwitcher.tempVar.sequence.macro"),
obs_module_text(
"AdvSceneSwitcher.tempVar.sequence.macro.description"));
} else if (_action == Action::SET_INDEX) {
AddTempvar(
"nextMacro",
obs_module_text(
"AdvSceneSwitcher.tempVar.sequence.nextMacro"),
obs_module_text(
"AdvSceneSwitcher.tempVar.sequence.nextMacro.description"));
}
}
bool MacroActionSequence::PerformAction()
{
if (_action == Action::RUN_SEQUENCE) {
@@ -162,7 +198,7 @@ bool MacroActionSequence::Load(obs_data_t *obj)
LoadMacroList(obj, _macros);
_restart = obs_data_get_bool(obj, "restart");
_macro.Load(obj);
_action = static_cast<Action>(obs_data_get_int(obj, "action"));
SetAction(static_cast<Action>(obs_data_get_int(obj, "action")));
_resetIndex.Load(obj, "resetIndex");
return true;
}
@@ -256,7 +292,7 @@ void MacroActionSequenceEdit::UpdateEntryData()
_macroList->SetContent(_entryData->_macros);
_restart->setChecked(_entryData->_restart);
_resetIndex->SetValue(_entryData->_resetIndex);
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_actions->setCurrentIndex(static_cast<int>(_entryData->GetAction()));
_macros->SetCurrentMacro(_entryData->_macro);
SetWidgetVisibility();
adjustSize();
@@ -361,7 +397,7 @@ void MacroActionSequenceEdit::UpdateStatusLine()
void MacroActionSequenceEdit::ActionChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_action = static_cast<MacroActionSequence::Action>(value);
_entryData->SetAction(static_cast<MacroActionSequence::Action>(value));
SetWidgetVisibility();
}
@@ -385,10 +421,10 @@ void MacroActionSequenceEdit::SetWidgetVisibility()
ClearLayout(_layout);
const auto action = _entryData->GetAction();
PlaceWidgets(
obs_module_text(
_entryData->_action ==
MacroActionSequence::Action::RUN_SEQUENCE
action == MacroActionSequence::Action::RUN_SEQUENCE
? "AdvSceneSwitcher.action.sequence.entry.run"
: "AdvSceneSwitcher.action.sequence.entry.setIndex"),
_layout,
@@ -396,15 +432,14 @@ void MacroActionSequenceEdit::SetWidgetVisibility()
{"{{macros}}", _macros},
{"{{index}}", _resetIndex}});
_macroList->setVisible(_entryData->_action ==
_macroList->setVisible(action ==
MacroActionSequence::Action::RUN_SEQUENCE);
_restart->setVisible(_entryData->_action ==
_restart->setVisible(action ==
MacroActionSequence::Action::RUN_SEQUENCE);
_statusLine->setVisible(_entryData->_action ==
_statusLine->setVisible(action ==
MacroActionSequence::Action::RUN_SEQUENCE);
_macros->setVisible(_entryData->_action ==
MacroActionSequence::Action::SET_INDEX);
_resetIndex->setVisible(_entryData->_action ==
_macros->setVisible(action == MacroActionSequence::Action::SET_INDEX);
_resetIndex->setVisible(action ==
MacroActionSequence::Action::SET_INDEX);
adjustSize();

View File

@@ -34,7 +34,10 @@ public:
RUN_SEQUENCE,
SET_INDEX,
};
Action _action = Action::RUN_SEQUENCE;
Action GetAction() const { return _action; }
void SetAction(Action);
bool _restart = true;
IntVariable _resetIndex = 1;
@@ -43,7 +46,11 @@ public:
private:
bool RunSequence();
bool SetSequenceIndex() const;
bool SetSequenceIndex();
void SetupTempVars();
Action _action = Action::RUN_SEQUENCE;
static bool _registered;
static const std::string id;

View File

@@ -0,0 +1,292 @@
#include "macro-action-source-interaction.hpp"
#include "source-interaction-recorder.hpp"
#include "layout-helpers.hpp"
#include "selection-helpers.hpp"
#include "sync-helpers.hpp"
#include "log-helper.hpp"
#include <obs.hpp>
#include <obs-interaction.h>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QVBoxLayout>
namespace advss {
const std::string MacroActionSourceInteraction::id = "source_interaction";
bool MacroActionSourceInteraction::_registered = MacroActionFactory::Register(
MacroActionSourceInteraction::id,
{MacroActionSourceInteraction::Create,
MacroActionSourceInteractionEdit::Create,
"AdvSceneSwitcher.action.sourceInteraction"});
bool MacroActionSourceInteraction::PerformAction()
{
OBSSourceAutoRelease source =
obs_weak_source_get_source(_source.GetSource());
if (!source) {
blog(LOG_WARNING, "source interaction: source not found");
return true;
}
uint32_t flags = obs_source_get_output_flags(source);
if (!(flags & OBS_SOURCE_INTERACTION)) {
blog(LOG_WARNING,
"source interaction: source \"%s\" does not support interaction",
obs_source_get_name(source));
return true;
}
for (const auto &step : _steps) {
PerformInteractionStep(source, step);
}
return true;
}
void MacroActionSourceInteraction::LogAction() const
{
ablog(LOG_INFO, "performed source interaction on \"%s\" (%d steps)",
_source.ToString(true).c_str(), (int)_steps.size());
}
bool MacroActionSourceInteraction::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
_source.Save(obj);
OBSDataArrayAutoRelease arr = obs_data_array_create();
for (const auto &step : _steps) {
OBSDataAutoRelease stepObj = obs_data_create();
step.Save(stepObj);
obs_data_array_push_back(arr, stepObj);
}
obs_data_set_array(obj, "steps", arr);
return true;
}
bool MacroActionSourceInteraction::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_source.Load(obj);
_steps.clear();
OBSDataArrayAutoRelease arr = obs_data_get_array(obj, "steps");
size_t count = obs_data_array_count(arr);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease stepObj = obs_data_array_item(arr, i);
SourceInteractionStep step;
step.Load(stepObj);
_steps.push_back(step);
}
return true;
}
std::string MacroActionSourceInteraction::GetShortDesc() const
{
return _source.ToString();
}
void MacroActionSourceInteraction::ResolveVariablesToFixedValues()
{
for (auto &step : _steps) {
step.x.ResolveVariables();
step.y.ResolveVariables();
step.clickCount.ResolveVariables();
step.wheelDeltaX.ResolveVariables();
step.wheelDeltaY.ResolveVariables();
step.nativeVkey.ResolveVariables();
step.text.ResolveVariables();
step.waitMs.ResolveVariables();
}
}
std::shared_ptr<MacroAction> MacroActionSourceInteraction::Create(Macro *m)
{
return std::make_shared<MacroActionSourceInteraction>(m);
}
std::shared_ptr<MacroAction> MacroActionSourceInteraction::Copy() const
{
return std::make_shared<MacroActionSourceInteraction>(*this);
}
static QStringList getInteractableSourceNames()
{
QStringList names;
obs_enum_sources(
[](void *param, obs_source_t *source) -> bool {
uint32_t flags = obs_source_get_output_flags(source);
if (flags & OBS_SOURCE_INTERACTION) {
auto list = static_cast<QStringList *>(param);
const char *name = obs_source_get_name(source);
if (name) {
list->append(QString(name));
}
}
return true;
},
&names);
names.sort();
return names;
}
MacroActionSourceInteractionEdit::MacroActionSourceInteractionEdit(
QWidget *parent,
std::shared_ptr<MacroActionSourceInteraction> entryData)
: QWidget(parent),
_sources(new SourceSelectionWidget(this, getInteractableSourceNames,
true)),
_stepList(new SourceInteractionStepList(this)),
_recordButton(new QPushButton(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record"))),
_noSelectionLabel(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.noSelection")))
{
_stepList->AddControlWidget(_recordButton);
auto sourceRow = new QHBoxLayout;
sourceRow->addWidget(new QLabel(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.source")));
sourceRow->addWidget(_sources);
sourceRow->addStretch();
_noSelectionLabel->setAlignment(Qt::AlignCenter);
_noSelectionLabel->hide();
auto mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(sourceRow);
mainLayout->addWidget(_stepList);
mainLayout->addWidget(_noSelectionLabel);
setLayout(mainLayout);
connect(_sources, SIGNAL(SourceChanged(const SourceSelection &)), this,
SLOT(SourceChanged(const SourceSelection &)));
connect(_stepList, &SourceInteractionStepList::StepsChanged, this,
&MacroActionSourceInteractionEdit::OnStepsChanged);
connect(_stepList, &SourceInteractionStepList::RowSelected, this,
&MacroActionSourceInteractionEdit::SetCurrentStepEditor);
connect(_recordButton, &QPushButton::clicked, this,
&MacroActionSourceInteractionEdit::OpenRecorder);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionSourceInteractionEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_sources->SetSource(_entryData->_source);
_stepList->SetSteps(_entryData->_steps);
_noSelectionLabel->setVisible(_stepList->count() > 0);
}
void MacroActionSourceInteractionEdit::SourceChanged(
const SourceSelection &source)
{
GUARD_LOADING_AND_LOCK();
_entryData->_source = source;
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}
void MacroActionSourceInteractionEdit::OnStepsChanged(
const std::vector<SourceInteractionStep> &steps)
{
GUARD_LOADING_AND_LOCK();
_entryData->_steps = steps;
}
void MacroActionSourceInteractionEdit::StepChanged(
const SourceInteractionStep &step)
{
GUARD_LOADING_AND_LOCK();
int row = _stepList->CurrentRow();
if (row < 0 || row >= (int)_entryData->_steps.size()) {
return;
}
_entryData->_steps[row] = step;
_stepList->UpdateStep(row, step);
}
void MacroActionSourceInteractionEdit::OpenRecorder()
{
if (!_entryData) {
return;
}
OBSSourceAutoRelease source =
obs_weak_source_get_source(_entryData->_source.GetSource());
if (!source) {
QMessageBox::warning(
this,
obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.title"),
obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.invalidSource"));
return;
}
auto dlg = new SourceInteractionRecorder(
window(), _entryData->_source.GetSource());
connect(dlg, &SourceInteractionRecorder::StepsRecorded, this,
&MacroActionSourceInteractionEdit::AcceptRecorded);
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->show();
}
void MacroActionSourceInteractionEdit::AcceptRecorded(
const std::vector<SourceInteractionStep> &steps)
{
if (!_entryData) {
return;
}
{
auto lock = _entryData->Lock();
_entryData->_steps.insert(_entryData->_steps.end(),
steps.begin(), steps.end());
}
_stepList->SetSteps(_entryData->_steps);
}
void MacroActionSourceInteractionEdit::showEvent(QShowEvent *event)
{
const QSignalBlocker b(this);
UpdateEntryData();
}
void MacroActionSourceInteractionEdit::SetCurrentStepEditor(int row)
{
if (_stepEditor) {
delete _stepEditor;
_stepEditor = nullptr;
}
if (!_entryData || row < 0 || row >= (int)_entryData->_steps.size()) {
_noSelectionLabel->setVisible(_stepList->count() > 0);
return;
}
_noSelectionLabel->setVisible(false);
_stepEditor =
new SourceInteractionStepEdit(this, _entryData->_steps[row]);
static_cast<QVBoxLayout *>(layout())->addWidget(_stepEditor);
connect(_stepEditor, &SourceInteractionStepEdit::StepChanged, this,
&MacroActionSourceInteractionEdit::StepChanged);
adjustSize();
updateGeometry();
}
} // namespace advss

View File

@@ -0,0 +1,81 @@
#pragma once
#include "macro-action-edit.hpp"
#include "source-selection.hpp"
#include "source-interaction-step.hpp"
#include "source-interaction-step-list.hpp"
#include "source-interaction-step-edit.hpp"
#include <QLabel>
#include <QPushButton>
namespace advss {
class MacroActionSourceInteraction : public MacroAction {
public:
MacroActionSourceInteraction(Macro *m) : MacroAction(m) {}
static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const;
bool PerformAction();
void LogAction() const;
void ResolveVariablesToFixedValues() override;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetId() const { return id; }
std::string GetShortDesc() const;
SourceSelection _source;
std::vector<SourceInteractionStep> _steps;
private:
static bool _registered;
static const std::string id;
};
class MacroActionSourceInteractionEdit : public QWidget {
Q_OBJECT
public:
MacroActionSourceInteractionEdit(
QWidget *parent,
std::shared_ptr<MacroActionSourceInteraction> entryData =
nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionSourceInteractionEdit(
parent,
std::dynamic_pointer_cast<MacroActionSourceInteraction>(
action));
}
private slots:
void SourceChanged(const SourceSelection &);
void OnStepsChanged(const std::vector<SourceInteractionStep> &);
void StepChanged(const SourceInteractionStep &);
void OpenRecorder();
void AcceptRecorded(const std::vector<SourceInteractionStep> &);
signals:
void HeaderInfoChanged(const QString &);
protected:
void showEvent(QShowEvent *event) override;
private:
void SetCurrentStepEditor(int row);
SourceSelectionWidget *_sources;
SourceInteractionStepList *_stepList;
QPushButton *_recordButton;
SourceInteractionStepEdit *_stepEditor = nullptr;
QLabel *_noSelectionLabel;
std::shared_ptr<MacroActionSourceInteraction> _entryData;
bool _loading = true;
};
} // namespace advss

View File

@@ -13,6 +13,14 @@ namespace advss {
const std::string MacroActionSource::id = "source";
std::vector<TempVariableRef> MacroActionSource::GetTempVarRefs() const
{
if (!_tempVar.HasValidID()) {
return {};
}
return {_tempVar};
}
bool MacroActionSource::_registered = MacroActionFactory::Register(
MacroActionSource::id,
{MacroActionSource::Create, MacroActionSourceEdit::Create,
@@ -45,6 +53,10 @@ const static std::map<MacroActionSource::Action, std::string> actionTypes = {
"AdvSceneSwitcher.action.source.type.closeFilterDialog"},
{MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG,
"AdvSceneSwitcher.action.source.type.closePropertiesDialog"},
{MacroActionSource::Action::GET_SETTING,
"AdvSceneSwitcher.action.source.type.getSetting"},
{MacroActionSource::Action::GET_SETTINGS,
"AdvSceneSwitcher.action.source.type.getSettings"},
};
const static std::map<obs_deinterlace_mode, std::string> deinterlaceModes = {
@@ -273,6 +285,22 @@ bool MacroActionSource::PerformAction()
"OBSBasicProperties");
});
break;
case Action::GET_SETTING: {
const auto value =
GetSourceSettingValue(_source.GetSource(), _setting);
if (value) {
SetTempVarValue("setting", *value);
}
break;
}
case Action::GET_SETTINGS: {
const auto settings =
GetSourceSettings(_source.GetSource(), true);
if (settings) {
SetTempVarValue("settings", *settings);
}
break;
}
default:
break;
}
@@ -360,6 +388,31 @@ void MacroActionSource::ResolveVariablesToFixedValues()
_manualSettingValue.ResolveVariables();
}
void MacroActionSource::SetupTempVars()
{
MacroAction::SetupTempVars();
switch (_action) {
case Action::GET_SETTING:
AddTempvar("setting",
obs_module_text(
"AdvSceneSwitcher.tempVar.source.setting"));
break;
case Action::GET_SETTINGS:
AddTempvar("settings",
obs_module_text(
"AdvSceneSwitcher.tempVar.source.settings"));
break;
default:
break;
}
}
void MacroActionSource::SetAction(Action action)
{
_action = action;
SetupTempVars();
}
static inline void populateActionSelection(QComboBox *list)
{
for (auto &[actionType, name] : actionTypes) {
@@ -502,7 +555,7 @@ void MacroActionSourceEdit::UpdateEntryData()
const auto weakSource = _entryData->_source.GetSource();
_settingsButtons->SetSelection(weakSource, _entryData->_button);
_actions->setCurrentIndex(static_cast<int>(_entryData->_action));
_actions->setCurrentIndex(static_cast<int>(_entryData->GetAction()));
_sources->SetSource(_entryData->_source);
_sourceSettings->SetSelection(weakSource, _entryData->_setting);
_settingsString->setPlainText(_entryData->_settingsString);
@@ -537,7 +590,7 @@ void MacroActionSourceEdit::SourceChanged(const SourceSelection &source)
void MacroActionSourceEdit::ActionChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_action = static_cast<MacroActionSource::Action>(value);
_entryData->SetAction(static_cast<MacroActionSource::Action>(value));
SetWidgetVisibility();
}
@@ -607,6 +660,7 @@ void MacroActionSourceEdit::SelectionChanged(const TempVariableRef &var)
{
GUARD_LOADING_AND_LOCK();
_entryData->_tempVar = var;
IncrementTempVarInUseGeneration();
}
void MacroActionSourceEdit::SettingsInputMethodChanged(int idx)
@@ -664,19 +718,29 @@ static QString GetIndividualListEntryName()
void MacroActionSourceEdit::SetWidgetVisibility()
{
const auto action = _entryData->GetAction();
const bool isSetSettings = action ==
MacroActionSource::Action::SETTINGS;
const bool isGetSetting = action ==
MacroActionSource::Action::GET_SETTING;
const bool isGetSettings = action ==
MacroActionSource::Action::GET_SETTINGS;
SetLayoutVisible(_settingsLayout,
_entryData->_action ==
MacroActionSource::Action::SETTINGS);
isSetSettings || isGetSetting || isGetSettings);
_settingsInputMethods->setVisible(isSetSettings);
_sourceSettings->setVisible(
_entryData->_action == MacroActionSource::Action::SETTINGS &&
_entryData->_settingsInputMethod !=
MacroActionSource::SettingsInputMethod::JSON_STRING);
(isSetSettings &&
_entryData->_settingsInputMethod !=
MacroActionSource::SettingsInputMethod::JSON_STRING) ||
isGetSetting);
_settingsString->setVisible(
_entryData->_action == MacroActionSource::Action::SETTINGS &&
isSetSettings &&
_entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::JSON_STRING);
_getSettings->setVisible(
_entryData->_action == MacroActionSource::Action::SETTINGS &&
isSetSettings &&
_entryData->_settingsInputMethod !=
MacroActionSource::SettingsInputMethod::
INDIVIDUAL_TEMPVAR);
@@ -685,13 +749,12 @@ void MacroActionSourceEdit::SetWidgetVisibility()
GetIndividualListEntryName(),
_entryData->_setting.IsList());
_tempVars->setVisible(_entryData->_action ==
MacroActionSource::Action::SETTINGS &&
_tempVars->setVisible(isSetSettings &&
_entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::
INDIVIDUAL_TEMPVAR);
if (_entryData->_action == MacroActionSource::Action::SETTINGS &&
if (isSetSettings &&
(_entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::INDIVIDUAL_MANUAL ||
_entryData->_settingsInputMethod ==
@@ -704,35 +767,31 @@ void MacroActionSourceEdit::SetWidgetVisibility()
_manualSettingValue->hide();
}
const bool showWarning =
_entryData->_action == MacroActionSource::Action::ENABLE ||
_entryData->_action == MacroActionSource::Action::DISABLE;
const bool showWarning = action == MacroActionSource::Action::ENABLE ||
action == MacroActionSource::Action::DISABLE;
_warning->setVisible(showWarning);
_settingsButtons->setVisible(
_entryData->_action ==
MacroActionSource::Action::SETTINGS_BUTTON);
action == MacroActionSource::Action::SETTINGS_BUTTON);
_deinterlaceMode->setVisible(
_entryData->_action ==
MacroActionSource::Action::DEINTERLACE_MODE);
action == MacroActionSource::Action::DEINTERLACE_MODE);
_deinterlaceOrder->setVisible(
_entryData->_action ==
MacroActionSource::Action::DEINTERLACE_FIELD_ORDER);
action == MacroActionSource::Action::DEINTERLACE_FIELD_ORDER);
_refreshSettingSelection->setVisible(
(_entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::
INDIVIDUAL_MANUAL ||
_entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::
INDIVIDUAL_LIST_ENTRY) &&
((isSetSettings &&
(_entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::
INDIVIDUAL_MANUAL ||
_entryData->_settingsInputMethod ==
MacroActionSource::SettingsInputMethod::
INDIVIDUAL_LIST_ENTRY)) ||
isGetSetting) &&
_entryData->_source.GetType() ==
SourceSelection::Type::VARIABLE);
_acceptDialog->setVisible(
_entryData->_action ==
MacroActionSource::Action::CLOSE_FILTER_DIALOG ||
_entryData->_action ==
MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG);
action == MacroActionSource::Action::CLOSE_FILTER_DIALOG ||
action == MacroActionSource::Action::CLOSE_PROPERTIES_DIALOG);
adjustSize();
updateGeometry();

View File

@@ -15,26 +15,22 @@ namespace advss {
class MacroActionSource : public MacroAction {
public:
MacroActionSource(Macro *m) : MacroAction(m) {}
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetShortDesc() const;
std::string GetId() const { return id; };
static std::shared_ptr<MacroAction> Create(Macro *m);
std::shared_ptr<MacroAction> Copy() const;
void ResolveVariablesToFixedValues();
SourceSelection _source;
SourceSettingButton _button;
StringVariable _settingsString = "";
StringVariable _manualSettingValue = "";
obs_deinterlace_mode _deinterlaceMode = OBS_DEINTERLACE_MODE_DISABLE;
obs_deinterlace_field_order _deinterlaceOrder =
OBS_DEINTERLACE_FIELD_ORDER_TOP;
TempVariableRef _tempVar;
SourceSetting _setting;
bool _acceptDialog = false;
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetShortDesc() const;
std::string GetId() const { return id; };
void ResolveVariablesToFixedValues();
void SetupTempVars();
std::vector<TempVariableRef> GetTempVarRefs() const;
enum class Action {
ENABLE,
@@ -50,8 +46,23 @@ public:
CLOSE_INTERACTION_DIALOG,
CLOSE_FILTER_DIALOG,
CLOSE_PROPERTIES_DIALOG,
GET_SETTING,
GET_SETTINGS,
};
Action _action = Action::SETTINGS;
void SetAction(Action);
Action GetAction() const { return _action; }
SourceSelection _source;
SourceSettingButton _button;
StringVariable _settingsString = "";
StringVariable _manualSettingValue = "";
obs_deinterlace_mode _deinterlaceMode = OBS_DEINTERLACE_MODE_DISABLE;
obs_deinterlace_field_order _deinterlaceOrder =
OBS_DEINTERLACE_FIELD_ORDER_TOP;
TempVariableRef _tempVar;
SourceSetting _setting;
bool _acceptDialog = false;
enum class SettingsInputMethod {
INDIVIDUAL_MANUAL,
@@ -63,6 +74,8 @@ public:
SettingsInputMethod::INDIVIDUAL_MANUAL;
private:
Action _action = Action::SETTINGS;
static bool _registered;
static const std::string id;
};

View File

@@ -3,6 +3,7 @@
#include "transition-helpers.hpp"
#include <obs-frontend-api.h>
#include <algorithm>
namespace advss {
@@ -22,6 +23,10 @@ const static std::map<MacroActionTransition::Type, std::string> actionTypes = {
"AdvSceneSwitcher.action.transition.type.sourceShow"},
{MacroActionTransition::Type::SOURCE_HIDE,
"AdvSceneSwitcher.action.transition.type.sourceHide"},
{MacroActionTransition::Type::TBAR,
"AdvSceneSwitcher.action.transition.type.tbar"},
{MacroActionTransition::Type::RELEASE_TBAR,
"AdvSceneSwitcher.action.transition.type.releaseTbar"},
};
void MacroActionTransition::SetSceneTransition()
@@ -76,6 +81,13 @@ static void obs_sceneitem_set_transition_duration(obs_sceneitem_t *item,
}
#endif
void MacroActionTransition::SetTbarPosition()
{
const double percent = std::clamp(_tbarPosition.GetValue(), 0.0, 100.0);
const int tbarValue = (int)std::round(percent / 100.0 * 1023.0);
obs_frontend_set_tbar_position(tbarValue);
}
void MacroActionTransition::SetSourceTransition(bool show)
{
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(27, 0, 0)
@@ -110,6 +122,12 @@ bool MacroActionTransition::PerformAction()
case Type::SOURCE_HIDE:
SetSourceTransition(false);
break;
case Type::TBAR:
SetTbarPosition();
break;
case Type::RELEASE_TBAR:
obs_frontend_release_tbar();
break;
}
return true;
}
@@ -119,22 +137,29 @@ void MacroActionTransition::LogAction() const
std::string msgBegin;
switch (_type) {
case Type::SCENE:
msgBegin += "set scene transition";
msgBegin = "set scene transition";
break;
case Type::SCENE_OVERRIDE:
msgBegin += "set scene override transition of " +
_scene.ToString(true);
msgBegin = "set scene override transition of " +
_scene.ToString(true);
break;
case Type::SOURCE_SHOW:
msgBegin += "set source show transition of " +
_source.ToString(true) + " on scene " +
_scene.ToString(true);
msgBegin = "set source show transition of " +
_source.ToString(true) + " on scene " +
_scene.ToString(true);
break;
case Type::SOURCE_HIDE:
msgBegin += "set source hide transition of " +
_source.ToString(true) + " on scene " +
_scene.ToString(true);
msgBegin = "set source hide transition of " +
_source.ToString(true) + " on scene " +
_scene.ToString(true);
break;
case Type::TBAR:
ablog(LOG_INFO, "set T-Bar position to %.2f%%",
_tbarPosition.GetValue());
return;
case Type::RELEASE_TBAR:
ablog(LOG_INFO, "release T-Bar");
return;
}
if (_setDuration) {
ablog(LOG_INFO, "%s duration to %s", msgBegin.c_str(),
@@ -156,6 +181,7 @@ bool MacroActionTransition::Save(obs_data_t *obj) const
_transition.Save(obj);
obs_data_set_bool(obj, "setDuration", _setDuration);
obs_data_set_bool(obj, "setType", _setTransitionType);
_tbarPosition.Save(obj, "tbarPosition");
return true;
}
@@ -169,23 +195,25 @@ bool MacroActionTransition::Load(obs_data_t *obj)
_transition.Load(obj);
_setDuration = obs_data_get_bool(obj, "setDuration");
_setTransitionType = obs_data_get_bool(obj, "setType");
_tbarPosition.Load(obj, "tbarPosition");
return true;
}
std::string MacroActionTransition::GetShortDesc() const
{
std::string msgBegin;
switch (_type) {
case Type::SCENE:
return _transition.ToString();
case Type::SCENE_OVERRIDE:
return _scene.ToString() + " - " + _transition.ToString();
case Type::SOURCE_SHOW:
return _scene.ToString() + " - " + _source.ToString() + " - " +
_transition.ToString();
case Type::SOURCE_HIDE:
return _scene.ToString() + " - " + _source.ToString() + " - " +
_transition.ToString();
case Type::TBAR:
return std::to_string(_tbarPosition.GetValue()) + "%";
case Type::RELEASE_TBAR:
return "";
}
return "";
}
@@ -205,6 +233,7 @@ void MacroActionTransition::ResolveVariablesToFixedValues()
_source.ResolveVariables();
_scene.ResolveVariables();
_duration.ResolveVariables();
_tbarPosition.ResolveVariables();
}
static inline void populateActionSelection(QComboBox *list)
@@ -236,9 +265,16 @@ MacroActionTransitionEdit::MacroActionTransitionEdit(
_setDuration(new QCheckBox),
_transitions(new TransitionSelectionWidget(this, false)),
_duration(new DurationSelection(this, false)),
_tbarPosition(new VariableDoubleSpinBox),
_transitionLayout(new QHBoxLayout),
_durationLayout(new QHBoxLayout)
_durationLayout(new QHBoxLayout),
_tbarLayout(new QHBoxLayout)
{
_tbarPosition->setMinimum(0.0);
_tbarPosition->setMaximum(100.0);
_tbarPosition->setDecimals(2);
_tbarPosition->setSuffix("%");
populateActionSelection(_actions);
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
@@ -260,6 +296,11 @@ MacroActionTransitionEdit::MacroActionTransitionEdit(
SLOT(SetTransitionChanged(int)));
QWidget::connect(_setDuration, SIGNAL(stateChanged(int)), this,
SLOT(SetDurationChanged(int)));
QWidget::connect(
_tbarPosition,
SIGNAL(NumberVariableChanged(const NumberVariable<double> &)),
this,
SLOT(TbarPositionChanged(const NumberVariable<double> &)));
const std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{type}}", _actions},
@@ -269,22 +310,29 @@ MacroActionTransitionEdit::MacroActionTransitionEdit(
{"{{duration}}", _duration},
{"{{setTransition}}", _setTransition},
{"{{setDuration}}", _setDuration},
{"{{tbarPosition}}", _tbarPosition},
};
auto typeLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.action.transition.entry.line1"),
"AdvSceneSwitcher.action.transition.layout.type"),
typeLayout, widgetPlaceholders);
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.transition.layout.transition"),
_transitionLayout, widgetPlaceholders);
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.action.transition.layout.duration"),
_durationLayout, widgetPlaceholders);
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.action.transition.entry.line2"),
_transitionLayout, widgetPlaceholders);
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.action.transition.entry.line3"),
_durationLayout, widgetPlaceholders);
"AdvSceneSwitcher.action.transition.layout.tbar"),
_tbarLayout, widgetPlaceholders);
auto mainLayout = new QVBoxLayout;
mainLayout->addLayout(typeLayout);
mainLayout->addLayout(_transitionLayout);
mainLayout->addLayout(_durationLayout);
mainLayout->addLayout(_tbarLayout);
setLayout(mainLayout);
_entryData = entryData;
@@ -300,13 +348,14 @@ void MacroActionTransitionEdit::UpdateEntryData()
_actions->setCurrentIndex(static_cast<int>(_entryData->_type));
_scenes->SetScene(_entryData->_scene);
_sources->SetSceneItem((_entryData->_source));
_sources->SetSceneItem(_entryData->_source);
_setDuration->setChecked(_entryData->_setDuration);
_duration->SetDuration(_entryData->_duration);
_setTransition->setChecked(_entryData->_setTransitionType);
_transitions->SetTransition(_entryData->_transition);
_transitions->setEnabled(_entryData->_setTransitionType);
_duration->setEnabled(_entryData->_setDuration);
_tbarPosition->SetValue(_entryData->_tbarPosition);
SetWidgetVisibility();
}
@@ -353,11 +402,19 @@ void MacroActionTransitionEdit::DurationChanged(const Duration &dur)
void MacroActionTransitionEdit::SetWidgetVisibility()
{
const bool isTbar = _entryData->_type ==
MacroActionTransition::Type::TBAR;
const bool isReleaseTbar = _entryData->_type ==
MacroActionTransition::Type::RELEASE_TBAR;
_sources->setVisible(
_entryData->_type == MacroActionTransition::Type::SOURCE_HIDE ||
_entryData->_type == MacroActionTransition::Type::SOURCE_SHOW);
_scenes->setVisible(_entryData->_type !=
MacroActionTransition::Type::SCENE);
MacroActionTransition::Type::SCENE &&
!isTbar && !isReleaseTbar);
SetLayoutVisible(_transitionLayout, !isTbar && !isReleaseTbar);
SetLayoutVisible(_durationLayout, !isTbar && !isReleaseTbar);
SetLayoutVisible(_tbarLayout, isTbar);
adjustSize();
}
@@ -381,4 +438,13 @@ void MacroActionTransitionEdit::SetDurationChanged(int state)
_duration->setEnabled(state);
}
void MacroActionTransitionEdit::TbarPositionChanged(
const NumberVariable<double> &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_tbarPosition = value;
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}
} // namespace advss

View File

@@ -4,6 +4,7 @@
#include "transition-selection.hpp"
#include "scene-selection.hpp"
#include "scene-item-selection.hpp"
#include "variable-spinbox.hpp"
#include <QCheckBox>
#include <QHBoxLayout>
@@ -28,6 +29,8 @@ public:
SCENE_OVERRIDE,
SOURCE_SHOW,
SOURCE_HIDE,
TBAR,
RELEASE_TBAR,
};
Type _type = Type::SCENE;
@@ -37,11 +40,13 @@ public:
bool _setTransitionType = true;
TransitionSelection _transition;
Duration _duration;
DoubleVariable _tbarPosition = 0.0;
private:
void SetSceneTransition();
void SetTransitionOverride();
void SetSourceTransition(bool);
void SetTbarPosition();
static bool _registered;
static const std::string id;
@@ -72,6 +77,7 @@ private slots:
void SetDurationChanged(int state);
void TransitionChanged(const TransitionSelection &);
void DurationChanged(const Duration &seconds);
void TbarPositionChanged(const NumberVariable<double> &);
signals:
void HeaderInfoChanged(const QString &);
@@ -83,8 +89,10 @@ protected:
QCheckBox *_setDuration;
TransitionSelectionWidget *_transitions;
DurationSelection *_duration;
VariableDoubleSpinBox *_tbarPosition;
QHBoxLayout *_transitionLayout;
QHBoxLayout *_durationLayout;
QHBoxLayout *_tbarLayout;
std::shared_ptr<MacroActionTransition> _entryData;
private:

View File

@@ -57,8 +57,11 @@ bool MacroActionWait::PerformAction()
std::chrono::milliseconds((int)(sleepDuration * 1000));
SetMacroAbortWait(false);
std::unique_lock<std::mutex> lock(*GetMutex());
waitHelper(&lock, GetMacro(), time);
{
SuspendLock suspendLock(*this);
std::unique_lock<std::mutex> lock(*GetMutex());
waitHelper(&lock, GetMacro(), time);
}
return !MacroWaitShouldAbort();
}

View File

@@ -710,6 +710,12 @@ void MacroConditionDateEdit::UpdateEntryData()
SetWidgetStatus();
}
void MacroConditionDateEdit::showEvent(QShowEvent *event)
{
const QSignalBlocker b(this);
UpdateEntryData();
}
void MacroConditionDateEdit::SetupSimpleView()
{
SetLayoutVisible(_simpleLayout, true);

View File

@@ -107,6 +107,16 @@ signals:
void HeaderInfoChanged(const QString &);
protected:
void showEvent(QShowEvent *event) override;
private:
void SetupSimpleView();
void SetupAdvancedView();
void SetupPatternView();
void SetWidgetStatus();
void ShowFirstDateSelection(bool visible);
void ShowSecondDateSelection(bool visible);
QComboBox *_weekCondition;
DayOfWeekSelector *_days;
QCheckBox *_ignoreWeekTime;
@@ -134,16 +144,9 @@ protected:
QHBoxLayout *_repeatUpdateLayout;
QHBoxLayout *_patternLayout;
std::shared_ptr<MacroConditionDate> _entryData;
private:
void SetupSimpleView();
void SetupAdvancedView();
void SetupPatternView();
void SetWidgetStatus();
void ShowFirstDateSelection(bool visible);
void ShowSecondDateSelection(bool visible);
QTimer _timer;
std::shared_ptr<MacroConditionDate> _entryData;
bool _loading = true;
};

View File

@@ -1,12 +1,10 @@
#include "macro-condition-file.hpp"
#include "curl-helper.hpp"
#include "layout-helpers.hpp"
#include "plugin-state-helpers.hpp"
#include "utility.hpp"
#include <QFileDialog>
#include <QTextStream>
#include <regex>
#include <QFileInfo>
namespace advss {
@@ -19,29 +17,6 @@ bool MacroConditionFile::_registered = MacroConditionFactory::Register(
static std::hash<std::string> strHash;
static size_t WriteCallback(void *contents, size_t size, size_t nmemb,
void *userp)
{
((std::string *)userp)->append((char *)contents, size * nmemb);
return size * nmemb;
}
static std::string getRemoteData(std::string &url)
{
std::string readBuffer;
CurlHelper::SetOpt(CURLOPT_URL, url.c_str());
CurlHelper::SetOpt(CURLOPT_WRITEFUNCTION, WriteCallback);
CurlHelper::SetOpt(CURLOPT_WRITEDATA, &readBuffer);
// Set timeout to at least one second
int timeout = GetIntervalValue() / 1000;
if (timeout == 0) {
timeout = 1;
}
CurlHelper::SetOpt(CURLOPT_TIMEOUT, 1);
CurlHelper::Perform();
return readBuffer;
}
void MacroConditionFile::SetCondition(Condition condition)
{
_condition = condition;
@@ -50,14 +25,6 @@ void MacroConditionFile::SetCondition(Condition condition)
bool MacroConditionFile::MatchFileContent(QString &filedata)
{
if (_onlyMatchIfChanged) {
size_t newHash = strHash(filedata.toUtf8().constData());
if (newHash == _lastHash) {
return false;
}
_lastHash = newHash;
}
if (_regex.Enabled()) {
return _regex.Matches(filedata, QString::fromStdString(_text));
}
@@ -66,31 +33,13 @@ bool MacroConditionFile::MatchFileContent(QString &filedata)
return CompareIgnoringLineEnding(text, filedata);
}
bool MacroConditionFile::CheckRemoteFileContent()
{
std::string path = _file;
std::string data = getRemoteData(path);
SetVariableValue(data);
SetTempVarValue("content", data);
QString qdata = QString::fromStdString(data);
return MatchFileContent(qdata);
}
bool MacroConditionFile::CheckLocalFileContent()
bool MacroConditionFile::CheckFileContent()
{
QFile file(QString::fromStdString(_file));
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false;
}
if (_useTime) {
QDateTime newLastMod = QFileInfo(file).lastModified();
if (_lastMod == newLastMod) {
return false;
}
_lastMod = newLastMod;
}
QString filedata = QTextStream(&file).readAll();
SetVariableValue(filedata.toStdString());
SetTempVarValue("content", filedata.toStdString());
@@ -103,42 +52,30 @@ bool MacroConditionFile::CheckLocalFileContent()
bool MacroConditionFile::CheckChangeContent()
{
QString filedata;
switch (_fileType) {
case FileType::LOCAL: {
std::string path = _file;
QFile file(QString::fromStdString(path));
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false;
}
filedata = QTextStream(&file).readAll();
file.close();
} break;
case FileType::REMOTE: {
std::string path = _file;
std::string data = getRemoteData(path);
QString filedata = QString::fromStdString(data);
} break;
default:
break;
std::string path = _file;
QFile file(QString::fromStdString(path));
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false;
}
filedata = QTextStream(&file).readAll();
file.close();
SetTempVarValue("content", filedata.toStdString());
size_t newHash = strHash(filedata.toUtf8().constData());
const bool contentChanged = newHash != _lastHash;
const bool contentChanged = !_firstContentCheck &&
(newHash != _lastHash);
_lastHash = newHash;
_firstContentCheck = false;
return contentChanged;
}
bool MacroConditionFile::CheckChangeDate()
{
if (_fileType == FileType::REMOTE) {
return false;
}
QFile file(QString::fromStdString(_file));
QDateTime newLastMod = QFileInfo(file).lastModified();
SetVariableValue(newLastMod.toString().toStdString());
const bool dateChanged = _lastMod != newLastMod;
const bool dateChanged = _lastMod.isValid() && (_lastMod != newLastMod);
_lastMod = newLastMod;
SetTempVarValue("date", newLastMod.toString(Qt::ISODate).toStdString());
return dateChanged;
@@ -157,10 +94,6 @@ void MacroConditionFile::SetupTempVars()
"AdvSceneSwitcher.tempVar.file.content"));
}
if (_fileType == FileType::REMOTE) {
return;
}
AddTempvar(
"basename",
obs_module_text("AdvSceneSwitcher.tempVar.file.basename"),
@@ -199,11 +132,7 @@ bool MacroConditionFile::CheckCondition()
bool ret = false;
switch (_condition) {
case Condition::MATCH:
if (_fileType == FileType::REMOTE) {
ret = CheckRemoteFileContent();
break;
}
ret = CheckLocalFileContent();
ret = CheckFileContent();
break;
case Condition::CONTENT_CHANGE:
ret = CheckChangeContent();
@@ -264,10 +193,7 @@ bool MacroConditionFile::Save(obs_data_t *obj) const
_regex.Save(obj);
_file.Save(obj, "file");
_text.Save(obj, "text");
obs_data_set_int(obj, "fileType", static_cast<int>(_fileType));
obs_data_set_int(obj, "condition", static_cast<int>(_condition));
obs_data_set_bool(obj, "useTime", _useTime);
obs_data_set_bool(obj, "onlyMatchIfChanged", _onlyMatchIfChanged);
return true;
}
@@ -275,18 +201,10 @@ bool MacroConditionFile::Load(obs_data_t *obj)
{
MacroCondition::Load(obj);
_regex.Load(obj);
// TODO: remove in future version
if (obs_data_has_user_value(obj, "useRegex")) {
_regex.CreateBackwardsCompatibleRegex(
obs_data_get_bool(obj, "useRegex"));
}
_file.Load(obj, "file");
_text.Load(obj, "text");
_fileType = static_cast<FileType>(obs_data_get_int(obj, "fileType"));
SetCondition(
static_cast<Condition>(obs_data_get_int(obj, "condition")));
_useTime = obs_data_get_bool(obj, "useTime");
_onlyMatchIfChanged = obs_data_get_bool(obj, "onlyMatchIfChanged");
return true;
}
@@ -295,13 +213,6 @@ std::string MacroConditionFile::GetShortDesc() const
return _file.UnresolvedValue();
}
static void populateFileTypes(QComboBox *list)
{
list->addItem(obs_module_text("AdvSceneSwitcher.condition.file.local"));
list->addItem(
obs_module_text("AdvSceneSwitcher.condition.file.remote"));
}
static void populateConditions(QComboBox *list)
{
list->addItem(
@@ -321,21 +232,13 @@ static void populateConditions(QComboBox *list)
MacroConditionFileEdit::MacroConditionFileEdit(
QWidget *parent, std::shared_ptr<MacroConditionFile> entryData)
: QWidget(parent),
_fileTypes(new QComboBox()),
_conditions(new QComboBox()),
_filePath(new FileSelection()),
_matchText(new VariableTextEdit(this)),
_regex(new RegexConfigWidget(parent)),
_checkModificationDate(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.fileTab.checkfileContentTime"))),
_checkFileContent(new QCheckBox(
obs_module_text("AdvSceneSwitcher.fileTab.checkfileContent")))
_regex(new RegexConfigWidget(parent))
{
populateFileTypes(_fileTypes);
populateConditions(_conditions);
QWidget::connect(_fileTypes, SIGNAL(currentIndexChanged(int)), this,
SLOT(FileTypeChanged(int)));
QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this,
SLOT(ConditionChanged(int)));
QWidget::connect(_filePath, SIGNAL(PathChanged(const QString &)), this,
@@ -345,42 +248,20 @@ MacroConditionFileEdit::MacroConditionFileEdit(
QWidget::connect(_regex,
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
SLOT(RegexChanged(const RegexConfig &)));
QWidget::connect(_checkModificationDate, SIGNAL(stateChanged(int)),
this, SLOT(CheckModificationDateChanged(int)));
QWidget::connect(_checkFileContent, SIGNAL(stateChanged(int)), this,
SLOT(OnlyMatchIfChangedChanged(int)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{fileType}}", _fileTypes},
{"{{conditions}}", _conditions},
{"{{filePath}}", _filePath},
{"{{matchText}}", _matchText},
{"{{useRegex}}", _regex},
{"{{checkModificationDate}}", _checkModificationDate},
{"{{checkFileContent}}", _checkFileContent},
};
auto widgetLayout = new QHBoxLayout;
widgetLayout->setContentsMargins(0, 0, 0, 0);
PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.file.layout"),
widgetLayout,
{{"{{conditions}}", _conditions},
{"{{filePath}}", _filePath},
{"{{regex}}", _regex}});
QVBoxLayout *mainLayout = new QVBoxLayout;
QHBoxLayout *line1Layout = new QHBoxLayout;
QHBoxLayout *line2Layout = new QHBoxLayout;
QHBoxLayout *line3Layout = new QHBoxLayout;
line1Layout->setContentsMargins(0, 0, 0, 0);
line2Layout->setContentsMargins(0, 0, 0, 0);
line3Layout->setContentsMargins(0, 0, 0, 0);
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.file.entry.line1"),
line1Layout, widgetPlaceholders);
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.file.entry.line2"),
line2Layout, widgetPlaceholders, false);
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.file.entry.line3"),
line3Layout, widgetPlaceholders);
mainLayout->addLayout(line1Layout);
mainLayout->addLayout(line2Layout);
mainLayout->addLayout(line3Layout);
auto layout = new QVBoxLayout;
layout->addLayout(widgetLayout);
layout->addWidget(_matchText);
setLayout(mainLayout);
setLayout(layout);
_entryData = entryData;
UpdateEntryData();
@@ -393,47 +274,15 @@ void MacroConditionFileEdit::UpdateEntryData()
return;
}
_fileTypes->setCurrentIndex(static_cast<int>(_entryData->_fileType));
_conditions->setCurrentIndex(
static_cast<int>(_entryData->GetCondition()));
_filePath->SetPath(_entryData->_file);
_matchText->setPlainText(_entryData->_text);
_regex->SetRegexConfig(_entryData->_regex);
_checkModificationDate->setChecked(_entryData->_useTime);
_checkFileContent->setChecked(_entryData->_onlyMatchIfChanged);
// TODO: Remove in future version
if (!_entryData->_useTime) {
_checkModificationDate->hide();
}
if (!_entryData->_onlyMatchIfChanged) {
_checkFileContent->hide();
}
SetWidgetVisibility();
}
void MacroConditionFileEdit::FileTypeChanged(int index)
{
if (_loading || !_entryData) {
return;
}
MacroConditionFile::FileType type =
static_cast<MacroConditionFile::FileType>(index);
if (type == MacroConditionFile::FileType::LOCAL) {
_filePath->Button()->setDisabled(false);
_checkModificationDate->setDisabled(false);
} else {
_filePath->Button()->setDisabled(true);
_checkModificationDate->setDisabled(true);
}
auto lock = LockContext();
_entryData->_fileType = type;
}
void MacroConditionFileEdit::ConditionChanged(int index)
{
GUARD_LOADING_AND_LOCK();
@@ -467,18 +316,6 @@ void MacroConditionFileEdit::RegexChanged(const RegexConfig &conf)
updateGeometry();
}
void MacroConditionFileEdit::CheckModificationDateChanged(int state)
{
GUARD_LOADING_AND_LOCK();
_entryData->_useTime = state;
}
void MacroConditionFileEdit::OnlyMatchIfChangedChanged(int state)
{
GUARD_LOADING_AND_LOCK();
_entryData->_onlyMatchIfChanged = state;
}
void MacroConditionFileEdit::SetWidgetVisibility()
{
if (!_entryData) {
@@ -489,20 +326,6 @@ void MacroConditionFileEdit::SetWidgetVisibility()
MacroConditionFile::Condition::MATCH);
_regex->setVisible(_entryData->GetCondition() ==
MacroConditionFile::Condition::MATCH);
_checkModificationDate->setVisible(
_entryData->_useTime &&
_entryData->GetCondition() ==
MacroConditionFile::Condition::MATCH);
_checkFileContent->setVisible(
_entryData->_onlyMatchIfChanged &&
_entryData->GetCondition() ==
MacroConditionFile::Condition::MATCH);
// TODO: Remove remote file support in future version in favor of HTTP
// action.
// Hide the option for now, if it is not used already.
_fileTypes->setVisible(_entryData->_fileType ==
MacroConditionFile::FileType::REMOTE);
adjustSize();
updateGeometry();

View File

@@ -7,9 +7,6 @@
#include <QWidget>
#include <QComboBox>
#include <QDateTime>
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
namespace advss {
@@ -26,11 +23,6 @@ public:
return std::make_shared<MacroConditionFile>(m);
}
enum class FileType {
LOCAL,
REMOTE,
};
enum class Condition {
MATCH,
CONTENT_CHANGE,
@@ -46,15 +38,9 @@ public:
StringVariable _text = obs_module_text("AdvSceneSwitcher.enterText");
RegexConfig _regex;
// TODO: Remove in future version
bool _useTime = false;
bool _onlyMatchIfChanged = false;
FileType _fileType = FileType::LOCAL;
private:
bool MatchFileContent(QString &filedata);
bool CheckRemoteFileContent();
bool CheckLocalFileContent();
bool CheckFileContent();
bool CheckChangeContent();
bool CheckChangeDate();
void SetupTempVars();
@@ -62,6 +48,7 @@ private:
Condition _condition = Condition::MATCH;
QDateTime _lastMod;
size_t _lastHash = 0;
bool _firstContentCheck = true;
std::string _lastFile;
std::string _basename;
std::string _basenameComplete;
@@ -92,29 +79,22 @@ public:
}
private slots:
void FileTypeChanged(int index);
void ConditionChanged(int index);
void PathChanged(const QString &text);
void MatchTextChanged();
void RegexChanged(const RegexConfig &);
void CheckModificationDateChanged(int state);
void OnlyMatchIfChangedChanged(int state);
signals:
void HeaderInfoChanged(const QString &);
protected:
QComboBox *_fileTypes;
private:
void SetWidgetVisibility();
QComboBox *_conditions;
FileSelection *_filePath;
VariableTextEdit *_matchText;
RegexConfigWidget *_regex;
QCheckBox *_checkModificationDate;
QCheckBox *_checkFileContent;
std::shared_ptr<MacroConditionFile> _entryData;
private:
void SetWidgetVisibility();
bool _loading = true;
};

View File

@@ -0,0 +1,237 @@
#include "source-interaction-recorder.hpp"
#include "obs-module-helper.hpp"
#include <obs.hpp>
#include <obs-interaction.h>
#include <QEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QWheelEvent>
namespace advss {
SourceInteractionRecorder::SourceInteractionRecorder(QWidget *parent,
obs_weak_source_t *source)
: QDialog(parent),
_source(source),
_preview(new SourcePreviewWidget(this, source)),
_stepList(new SourceInteractionStepList(this)),
_startStopButton(new QPushButton(
obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.start"),
this))
{
setWindowTitle(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.title"));
_preview->setMouseTracking(true);
_preview->setFocusPolicy(Qt::StrongFocus);
_preview->installEventFilter(this);
_stepList->HideControls();
_stepList->SetMinListHeight(50);
auto buttonRow = new QHBoxLayout;
buttonRow->addWidget(_startStopButton);
buttonRow->addStretch();
auto acceptButton = new QPushButton(
obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.accept"),
this);
buttonRow->addWidget(acceptButton);
auto layout = new QVBoxLayout(this);
layout->addWidget(_preview, 1);
layout->addWidget(_stepList);
layout->addLayout(buttonRow);
setLayout(layout);
connect(_startStopButton, &QPushButton::clicked, this,
&SourceInteractionRecorder::StartStop);
connect(acceptButton, &QPushButton::clicked, this, [this]() {
if (_recording) {
StartStop();
}
emit StepsRecorded(_steps);
accept();
});
}
SourceInteractionRecorder::~SourceInteractionRecorder() {}
void SourceInteractionRecorder::StartStop()
{
_recording = !_recording;
if (_recording) {
_steps.clear();
_stepList->Clear();
_firstEvent = true;
_startStopButton->setText(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.stop"));
} else {
_startStopButton->setText(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.start"));
}
}
void SourceInteractionRecorder::AppendStep(const SourceInteractionStep &step)
{
if (!_firstEvent) {
auto now = std::chrono::steady_clock::now();
int ms = static_cast<int>(
std::chrono::duration_cast<std::chrono::milliseconds>(
now - _lastEventTime)
.count());
if (ms > 10) {
SourceInteractionStep wait;
wait.type = SourceInteractionStep::Type::WAIT;
wait.waitMs.SetValue(ms);
_steps.push_back(wait);
_stepList->Insert(
QString::fromStdString(wait.ToString()));
}
}
_firstEvent = false;
_lastEventTime = std::chrono::steady_clock::now();
_steps.push_back(step);
_stepList->Insert(QString::fromStdString(step.ToString()));
_stepList->ScrollToBottom();
}
bool SourceInteractionRecorder::eventFilter(QObject *obj, QEvent *event)
{
if (obj != _preview || !_recording) {
return QDialog::eventFilter(obj, event);
}
switch (event->type()) {
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease:
case QEvent::MouseButtonDblClick:
return HandleMouseClick(static_cast<QMouseEvent *>(event));
case QEvent::MouseMove:
return HandleMouseMove(static_cast<QMouseEvent *>(event));
case QEvent::Wheel:
return HandleMouseWheel(static_cast<QWheelEvent *>(event));
case QEvent::KeyPress:
case QEvent::KeyRelease:
return HandleKeyEvent(static_cast<QKeyEvent *>(event));
case QEvent::FocusIn:
case QEvent::FocusOut: {
OBSSourceAutoRelease source =
obs_weak_source_get_source(_source);
if (source) {
obs_source_send_focus(source,
event->type() == QEvent::FocusIn);
}
return true;
}
default:
break;
}
return QDialog::eventFilter(obj, event);
}
bool SourceInteractionRecorder::HandleMouseClick(QMouseEvent *event)
{
SourceInteractionStep step;
step.type = SourceInteractionStep::Type::MOUSE_CLICK;
int srcX = 0, srcY = 0;
QPoint pos = event->pos();
_preview->GetSourceRelativeXY(pos.x(), pos.y(), srcX, srcY);
step.x.SetValue(srcX);
step.y.SetValue(srcY);
step.mouseUp = (event->type() == QEvent::MouseButtonRelease);
step.clickCount.SetValue(
(event->type() == QEvent::MouseButtonDblClick) ? 2 : 1);
switch (event->button()) {
case Qt::LeftButton:
step.button = MOUSE_LEFT;
break;
case Qt::MiddleButton:
step.button = MOUSE_MIDDLE;
break;
case Qt::RightButton:
step.button = MOUSE_RIGHT;
break;
default:
return false;
}
AppendStep(step);
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
if (source) {
PerformInteractionStep(source, step);
}
return true;
}
bool SourceInteractionRecorder::HandleMouseMove(QMouseEvent *event)
{
SourceInteractionStep step;
step.type = SourceInteractionStep::Type::MOUSE_MOVE;
int srcX = 0, srcY = 0;
QPoint pos = event->pos();
_preview->GetSourceRelativeXY(pos.x(), pos.y(), srcX, srcY);
step.x.SetValue(srcX);
step.y.SetValue(srcY);
// Always forward moves to the source so hover/cursor effects work,
// but only record them when a button is held (drag), to reduce noise.
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
if (source) {
PerformInteractionStep(source, step);
}
if (event->buttons() != Qt::NoButton) {
AppendStep(step);
}
return true;
}
bool SourceInteractionRecorder::HandleMouseWheel(QWheelEvent *event)
{
SourceInteractionStep step;
step.type = SourceInteractionStep::Type::MOUSE_WHEEL;
int srcX = 0, srcY = 0;
const QPointF pos = event->position();
_preview->GetSourceRelativeXY((int)pos.x(), (int)pos.y(), srcX, srcY);
step.x.SetValue(srcX);
step.y.SetValue(srcY);
const QPoint angle = event->angleDelta();
step.wheelDeltaX.SetValue(angle.x());
step.wheelDeltaY.SetValue(angle.y());
AppendStep(step);
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
if (source) {
PerformInteractionStep(source, step);
}
return true;
}
bool SourceInteractionRecorder::HandleKeyEvent(QKeyEvent *event)
{
SourceInteractionStep step;
step.type = SourceInteractionStep::Type::KEY_PRESS;
step.keyUp = (event->type() == QEvent::KeyRelease);
step.nativeVkey.SetValue(static_cast<int>(event->nativeVirtualKey()));
step.text = event->text().toStdString();
AppendStep(step);
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
if (source) {
PerformInteractionStep(source, step);
}
return true;
}
} // namespace advss

View File

@@ -0,0 +1,54 @@
#pragma once
#include "source-interaction-step.hpp"
#include "source-interaction-step-list.hpp"
#include "source-preview-widget.hpp"
#include <chrono>
#include <vector>
#include <QDialog>
#include <QPushButton>
class QKeyEvent;
class QMouseEvent;
class QWheelEvent;
namespace advss {
class SourceInteractionRecorder : public QDialog {
Q_OBJECT
public:
SourceInteractionRecorder(QWidget *parent, obs_weak_source_t *source);
~SourceInteractionRecorder();
const std::vector<SourceInteractionStep> &GetSteps() const
{
return _steps;
}
signals:
void StepsRecorded(const std::vector<SourceInteractionStep> &);
private slots:
void StartStop();
private:
bool eventFilter(QObject *obj, QEvent *event) override;
bool HandleMouseClick(QMouseEvent *);
bool HandleMouseMove(QMouseEvent *);
bool HandleMouseWheel(QWheelEvent *);
bool HandleKeyEvent(QKeyEvent *);
void AppendStep(const SourceInteractionStep &);
obs_weak_source_t *_source;
SourcePreviewWidget *_preview;
SourceInteractionStepList *_stepList;
QPushButton *_startStopButton;
bool _recording = false;
std::vector<SourceInteractionStep> _steps;
std::chrono::steady_clock::time_point _lastEventTime;
bool _firstEvent = true;
};
} // namespace advss

View File

@@ -0,0 +1,315 @@
#include "source-interaction-step-edit.hpp"
#include "obs-module-helper.hpp"
#include "variable-line-edit.hpp"
#include "variable-spinbox.hpp"
#include <map>
#include <string>
#include <QCheckBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QVBoxLayout>
namespace advss {
static const std::map<SourceInteractionStep::Type, std::string> stepTypeNames = {
{SourceInteractionStep::Type::MOUSE_MOVE,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseMove"},
{SourceInteractionStep::Type::MOUSE_CLICK,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseClick"},
{SourceInteractionStep::Type::MOUSE_WHEEL,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseWheel"},
{SourceInteractionStep::Type::KEY_PRESS,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.keyPress"},
{SourceInteractionStep::Type::TYPE_TEXT,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.typeText"},
{SourceInteractionStep::Type::WAIT,
"AdvSceneSwitcher.action.sourceInteraction.step.edit.wait"},
};
static const std::map<obs_mouse_button_type, std::string> mouseButtonNames = {
{MOUSE_LEFT, "AdvSceneSwitcher.action.sourceInteraction.button.left"},
{MOUSE_MIDDLE,
"AdvSceneSwitcher.action.sourceInteraction.button.middle"},
{MOUSE_RIGHT, "AdvSceneSwitcher.action.sourceInteraction.button.right"},
};
static void populateTypeCombo(QComboBox *combo)
{
for (const auto &[type, name] : stepTypeNames) {
combo->addItem(obs_module_text(name.c_str()),
static_cast<int>(type));
}
}
static void populateButtonCombo(QComboBox *combo)
{
for (const auto &[btn, name] : mouseButtonNames) {
combo->addItem(obs_module_text(name.c_str()),
static_cast<int>(btn));
}
}
static QHBoxLayout *labelledRow(const char *labelKey, QWidget *w)
{
auto row = new QHBoxLayout;
row->addWidget(new QLabel(obs_module_text(labelKey)));
row->addWidget(w);
row->addStretch();
return row;
}
SourceInteractionStepEdit::SourceInteractionStepEdit(
QWidget *parent, const SourceInteractionStep &step)
: QWidget(parent),
_typeCombo(new QComboBox(this)),
_fields(new QStackedWidget(this)),
_step(step)
{
populateTypeCombo(_typeCombo);
auto mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
auto typeRow = labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.type",
_typeCombo);
mainLayout->addLayout(typeRow);
mainLayout->addWidget(_fields);
setLayout(mainLayout);
connect(_typeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &SourceInteractionStepEdit::TypeChanged);
RebuildFields();
int idx = _typeCombo->findData(static_cast<int>(_step.type));
if (idx >= 0) {
_typeCombo->setCurrentIndex(idx);
}
}
void SourceInteractionStepEdit::TypeChanged(int)
{
_step.type = static_cast<SourceInteractionStep::Type>(
_typeCombo->currentData().toInt());
RebuildFields();
emit StepChanged(_step);
}
void SourceInteractionStepEdit::UpdateStep()
{
emit StepChanged(_step);
}
void SourceInteractionStepEdit::RebuildFields()
{
while (_fields->count() > 0) {
auto w = _fields->widget(0);
_fields->removeWidget(w);
delete w;
}
auto makeVarSpin = [](int min, int max,
const NumberVariable<int> &val) {
auto sb = new VariableSpinBox;
sb->setMinimum(min);
sb->setMaximum(max);
sb->SetValue(val);
return sb;
};
switch (_step.type) {
case SourceInteractionStep::Type::MOUSE_MOVE:
case SourceInteractionStep::Type::MOUSE_CLICK:
case SourceInteractionStep::Type::MOUSE_WHEEL: {
auto page = new QWidget;
auto layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
auto xSpin = makeVarSpin(-32768, 32767, _step.x);
auto ySpin = makeVarSpin(-32768, 32767, _step.y);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.x",
xSpin));
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.y",
ySpin));
connect(xSpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::NumberVariableChanged),
this, [this, xSpin](const NumberVariable<int> &) {
_step.x = xSpin->Value();
UpdateStep();
});
connect(ySpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::NumberVariableChanged),
this, [this, ySpin](const NumberVariable<int> &) {
_step.y = ySpin->Value();
UpdateStep();
});
if (_step.type == SourceInteractionStep::Type::MOUSE_CLICK) {
auto btnCombo = new QComboBox;
populateButtonCombo(btnCombo);
btnCombo->setCurrentIndex(btnCombo->findData(
static_cast<int>(_step.button)));
auto upCheck = new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.mouseUp"));
upCheck->setChecked(_step.mouseUp);
auto countSpin = makeVarSpin(1, 3, _step.clickCount);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.button",
btnCombo));
layout->addWidget(upCheck);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.clickCount",
countSpin));
connect(btnCombo,
QOverload<int>::of(
&QComboBox::currentIndexChanged),
this, [this, btnCombo]() {
_step.button = static_cast<
obs_mouse_button_type>(
btnCombo->currentData().toInt());
UpdateStep();
});
connect(upCheck, &QCheckBox::stateChanged, this,
[this, upCheck]() {
_step.mouseUp = upCheck->isChecked();
UpdateStep();
});
connect(countSpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::
NumberVariableChanged),
this,
[this, countSpin](const NumberVariable<int> &) {
_step.clickCount = countSpin->Value();
UpdateStep();
});
} else if (_step.type ==
SourceInteractionStep::Type::MOUSE_WHEEL) {
auto dxSpin =
makeVarSpin(-1200, 1200, _step.wheelDeltaX);
auto dySpin =
makeVarSpin(-1200, 1200, _step.wheelDeltaY);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDx",
dxSpin));
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.wheelDy",
dySpin));
connect(dxSpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::
NumberVariableChanged),
this,
[this, dxSpin](const NumberVariable<int> &) {
_step.wheelDeltaX = dxSpin->Value();
UpdateStep();
});
connect(dySpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::
NumberVariableChanged),
this,
[this, dySpin](const NumberVariable<int> &) {
_step.wheelDeltaY = dySpin->Value();
UpdateStep();
});
}
_fields->addWidget(page);
_fields->setCurrentWidget(page);
break;
}
case SourceInteractionStep::Type::KEY_PRESS: {
auto page = new QWidget;
auto layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
auto vkeySpin = makeVarSpin(0, 0xFFFF, _step.nativeVkey);
auto upCheck = new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.keyUp"));
upCheck->setChecked(_step.keyUp);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.vkey",
vkeySpin));
layout->addWidget(upCheck);
connect(vkeySpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::NumberVariableChanged),
this, [this, vkeySpin](const NumberVariable<int> &) {
_step.nativeVkey = vkeySpin->Value();
UpdateStep();
});
connect(upCheck, &QCheckBox::stateChanged, this,
[this, upCheck]() {
_step.keyUp = upCheck->isChecked();
UpdateStep();
});
_fields->addWidget(page);
_fields->setCurrentWidget(page);
break;
}
case SourceInteractionStep::Type::TYPE_TEXT: {
auto page = new QWidget;
auto layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
auto textEdit = new VariableLineEdit(page);
textEdit->setText(_step.text);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.text",
textEdit));
connect(textEdit, &QLineEdit::editingFinished, this,
[this, textEdit]() {
_step.text = textEdit->text().toStdString();
UpdateStep();
});
_fields->addWidget(page);
_fields->setCurrentWidget(page);
break;
}
case SourceInteractionStep::Type::WAIT: {
auto page = new QWidget;
auto layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
auto msSpin = makeVarSpin(0, 60000, _step.waitMs);
layout->addLayout(labelledRow(
"AdvSceneSwitcher.action.sourceInteraction.step.edit.waitMs",
msSpin));
connect(msSpin,
QOverload<const NumberVariable<int> &>::of(
&GenericVariableSpinbox::NumberVariableChanged),
this, [this, msSpin](const NumberVariable<int> &) {
_step.waitMs = msSpin->Value();
UpdateStep();
});
_fields->addWidget(page);
_fields->setCurrentWidget(page);
break;
}
}
}
} // namespace advss

View File

@@ -0,0 +1,32 @@
#pragma once
#include "source-interaction-step.hpp"
#include <QComboBox>
#include <QStackedWidget>
#include <QWidget>
namespace advss {
class SourceInteractionStepEdit : public QWidget {
Q_OBJECT
public:
SourceInteractionStepEdit(QWidget *parent,
const SourceInteractionStep &step);
SourceInteractionStep GetStep() const { return _step; }
signals:
void StepChanged(const SourceInteractionStep &);
private slots:
void TypeChanged(int);
void UpdateStep();
private:
void RebuildFields();
QComboBox *_typeCombo;
QStackedWidget *_fields;
SourceInteractionStep _step;
};
} // namespace advss

View File

@@ -0,0 +1,99 @@
#include "source-interaction-step-list.hpp"
#include "obs-module-helper.hpp"
#include <QTimer>
namespace advss {
SourceInteractionStepList::SourceInteractionStepList(QWidget *parent)
: ListEditor(parent)
{
_list->setSelectionMode(QAbstractItemView::ExtendedSelection);
connect(_list, &QListWidget::currentRowChanged, this,
&SourceInteractionStepList::RowSelected);
SetPlaceholderText(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.record.placeholder"));
SetMaxListHeight(350);
}
void SourceInteractionStepList::SetSteps(
const std::vector<SourceInteractionStep> &steps)
{
_steps = steps;
_list->clear();
for (const auto &step : _steps) {
_list->addItem(QString::fromStdString(step.ToString()));
}
UpdateListSize();
_list->setCurrentRow(-1);
}
void SourceInteractionStepList::UpdateStep(int row,
const SourceInteractionStep &step)
{
if (row < 0 || row >= (int)_steps.size()) {
return;
}
_steps[row] = step;
_list->item(row)->setText(QString::fromStdString(step.ToString()));
}
void SourceInteractionStepList::Add()
{
_steps.emplace_back();
_list->addItem(QString::fromStdString(_steps.back().ToString()));
QTimer::singleShot(0, this, [this]() { UpdateListSize(); });
emit StepsChanged(_steps);
_list->setCurrentRow((int)_steps.size() - 1);
}
void SourceInteractionStepList::Remove()
{
const QList<QListWidgetItem *> selected = _list->selectedItems();
if (selected.isEmpty()) {
return;
}
std::vector<int> rows;
rows.reserve(selected.size());
for (const auto *item : selected) {
rows.push_back(_list->row(item));
}
std::sort(rows.begin(), rows.end(), std::greater<int>());
for (int row : rows) {
if (row >= 0 && row < (int)_steps.size()) {
_steps.erase(_steps.begin() + row);
delete _list->takeItem(row);
}
}
QTimer::singleShot(0, this, [this]() { UpdateListSize(); });
emit StepsChanged(_steps);
}
void SourceInteractionStepList::Up()
{
int row = _list->currentRow();
if (row <= 0 || row >= (int)_steps.size()) {
return;
}
std::swap(_steps[row], _steps[row - 1]);
_list->insertItem(row - 1, _list->takeItem(row));
_list->setCurrentRow(row - 1);
emit StepsChanged(_steps);
}
void SourceInteractionStepList::Down()
{
int row = _list->currentRow();
if (row < 0 || row >= (int)_steps.size() - 1) {
return;
}
std::swap(_steps[row], _steps[row + 1]);
_list->insertItem(row + 1, _list->takeItem(row));
_list->setCurrentRow(row + 1);
emit StepsChanged(_steps);
}
} // namespace advss

View File

@@ -0,0 +1,38 @@
#pragma once
#include "list-editor.hpp"
#include "source-interaction-step.hpp"
#include <vector>
namespace advss {
class SourceInteractionStepList : public ListEditor {
Q_OBJECT
public:
SourceInteractionStepList(QWidget *parent = nullptr);
void SetSteps(const std::vector<SourceInteractionStep> &steps);
void UpdateStep(int row, const SourceInteractionStep &step);
int CurrentRow() const { return _list->currentRow(); }
void SetCurrentRow(int row) { _list->setCurrentRow(row); }
void Clear() const { _list->clear(); }
void HideControls() const { _controls->hide(); }
void Insert(const QString &value) const { _list->addItem(value); }
void ScrollToBottom() const { _list->scrollToBottom(); }
void AddControlWidget(QWidget *widget) { _controls->AddWidget(widget); }
signals:
void StepsChanged(const std::vector<SourceInteractionStep> &);
void RowSelected(int row);
private slots:
void Add() override;
void Remove() override;
void Up() override;
void Down() override;
private:
std::vector<SourceInteractionStep> _steps;
};
} // namespace advss

View File

@@ -0,0 +1,190 @@
#include "source-interaction-step.hpp"
#include "obs-module-helper.hpp"
#include "variable.hpp"
#include <obs-interaction.h>
#include <chrono>
#include <thread>
#include <QString>
namespace advss {
static QString varOrNum(const NumberVariable<int> &v)
{
if (!v.IsFixedType()) {
return QString::fromStdString(
GetWeakVariableName(v.GetVariable()));
}
return QString::number(v.GetFixedValue());
}
bool SourceInteractionStep::Save(obs_data_t *obj) const
{
obs_data_set_int(obj, "type", static_cast<int>(type));
x.Save(obj, "x");
y.Save(obj, "y");
obs_data_set_int(obj, "button", static_cast<int>(button));
obs_data_set_bool(obj, "mouseUp", mouseUp);
clickCount.Save(obj, "clickCount");
wheelDeltaX.Save(obj, "wheelDeltaX");
wheelDeltaY.Save(obj, "wheelDeltaY");
nativeVkey.Save(obj, "nativeVkey");
obs_data_set_int(obj, "modifiers", modifiers);
obs_data_set_bool(obj, "keyUp", keyUp);
text.Save(obj, "text");
waitMs.Save(obj, "waitMs");
return true;
}
bool SourceInteractionStep::Load(obs_data_t *obj)
{
type = static_cast<Type>(obs_data_get_int(obj, "type"));
x.Load(obj, "x");
y.Load(obj, "y");
button = static_cast<obs_mouse_button_type>(
obs_data_get_int(obj, "button"));
mouseUp = obs_data_get_bool(obj, "mouseUp");
clickCount.Load(obj, "clickCount");
wheelDeltaX.Load(obj, "wheelDeltaX");
wheelDeltaY.Load(obj, "wheelDeltaY");
nativeVkey.Load(obj, "nativeVkey");
modifiers = static_cast<uint32_t>(obs_data_get_int(obj, "modifiers"));
keyUp = obs_data_get_bool(obj, "keyUp");
text.Load(obj, "text");
waitMs.Load(obj, "waitMs");
return true;
}
std::string SourceInteractionStep::ToString() const
{
switch (type) {
case Type::MOUSE_MOVE:
return QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseMove"))
.arg(varOrNum(x))
.arg(varOrNum(y))
.toStdString();
case Type::MOUSE_CLICK: {
const char *btnKey =
(button == MOUSE_LEFT)
? "AdvSceneSwitcher.action.sourceInteraction.button.left"
: (button == MOUSE_MIDDLE)
? "AdvSceneSwitcher.action.sourceInteraction.button.middle"
: "AdvSceneSwitcher.action.sourceInteraction.button.right";
const char *dirKey =
mouseUp ? "AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseUp"
: "AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseDown";
QString result =
QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseClick"))
.arg(obs_module_text(dirKey))
.arg(obs_module_text(btnKey))
.arg(varOrNum(x))
.arg(varOrNum(y));
if (!clickCount.IsFixedType() ||
clickCount.GetFixedValue() > 1) {
result +=
QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseClickCount"))
.arg(varOrNum(clickCount));
}
return result.toStdString();
}
case Type::MOUSE_WHEEL:
return QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.mouseWheel"))
.arg(varOrNum(x))
.arg(varOrNum(y))
.arg(varOrNum(wheelDeltaX))
.arg(varOrNum(wheelDeltaY))
.toStdString();
case Type::KEY_PRESS: {
const char *dirKey =
keyUp ? "AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyUp"
: "AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyDown";
if (text.empty()) {
return QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyPress"))
.arg(obs_module_text(dirKey))
.toStdString();
}
return QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.keyPressWithText"))
.arg(obs_module_text(dirKey))
.arg(QString::fromStdString(text))
.toStdString();
}
case Type::TYPE_TEXT:
return QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.typeText"))
.arg(QString::fromStdString(text))
.toStdString();
case Type::WAIT:
return QString(obs_module_text(
"AdvSceneSwitcher.action.sourceInteraction.step.listEntry.wait"))
.arg(varOrNum(waitMs))
.toStdString();
}
return "Unknown";
}
void PerformInteractionStep(obs_source_t *source,
const SourceInteractionStep &step)
{
switch (step.type) {
case SourceInteractionStep::Type::MOUSE_MOVE: {
obs_mouse_event e{};
e.x = step.x;
e.y = step.y;
obs_source_send_mouse_move(source, &e, false);
break;
}
case SourceInteractionStep::Type::MOUSE_CLICK: {
obs_mouse_event e{};
e.modifiers = step.modifiers;
e.x = step.x;
e.y = step.y;
obs_source_send_mouse_click(source, &e, step.button,
step.mouseUp, step.clickCount);
break;
}
case SourceInteractionStep::Type::MOUSE_WHEEL: {
obs_mouse_event e{};
e.modifiers = step.modifiers;
e.x = step.x;
e.y = step.y;
obs_source_send_mouse_wheel(source, &e, step.wheelDeltaX,
step.wheelDeltaY);
break;
}
case SourceInteractionStep::Type::KEY_PRESS: {
std::string textCopy = step.text;
obs_key_event e{};
e.modifiers = step.modifiers;
e.text = textCopy.data();
e.native_vkey =
static_cast<uint32_t>(step.nativeVkey.GetValue());
obs_source_send_key_click(source, &e, step.keyUp);
break;
}
case SourceInteractionStep::Type::TYPE_TEXT: {
const std::string resolvedText = step.text;
for (const char c : resolvedText) {
std::string ch(1, c);
obs_key_event e{};
e.text = ch.data();
obs_source_send_key_click(source, &e, false);
obs_source_send_key_click(source, &e, true);
}
break;
}
case SourceInteractionStep::Type::WAIT:
std::this_thread::sleep_for(
std::chrono::milliseconds(step.waitMs));
break;
}
}
} // namespace advss

View File

@@ -0,0 +1,48 @@
#pragma once
#include "variable-number.hpp"
#include "variable-string.hpp"
#include <obs.hpp>
#include <obs-interaction.h>
namespace advss {
struct SourceInteractionStep {
enum class Type {
MOUSE_MOVE,
MOUSE_CLICK,
MOUSE_WHEEL,
KEY_PRESS,
TYPE_TEXT,
WAIT,
};
Type type = Type::MOUSE_MOVE;
// Mouse fields
NumberVariable<int> x = 0;
NumberVariable<int> y = 0;
obs_mouse_button_type button = MOUSE_LEFT;
bool mouseUp = false;
NumberVariable<int> clickCount = 1;
NumberVariable<int> wheelDeltaX = 0;
NumberVariable<int> wheelDeltaY = 120;
// Key / text fields
NumberVariable<int> nativeVkey = 0;
uint32_t modifiers = 0; // INTERACT_*
bool keyUp = false;
StringVariable text; // for TYPE_TEXT or key text
// Wait field (ms)
NumberVariable<int> waitMs = 100;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string ToString() const;
};
void PerformInteractionStep(obs_source_t *source,
const SourceInteractionStep &step);
} // namespace advss

View File

@@ -0,0 +1,149 @@
#include "source-preview-widget.hpp"
#include <obs.hpp>
#include <graphics/graphics.h>
#include <QResizeEvent>
namespace advss {
static int32_t clampToSource(int32_t v, uint32_t max)
{
if (v < 0) {
return 0;
}
if (v > (int32_t)max) {
return (int32_t)max;
}
return v;
}
SourcePreviewWidget::SourcePreviewWidget(QWidget *parent,
obs_weak_source_t *source)
: QWidget(parent),
_source(source)
{
setAttribute(Qt::WA_PaintOnScreen);
setAttribute(Qt::WA_NoSystemBackground);
setMinimumSize(320, 240);
}
SourcePreviewWidget::~SourcePreviewWidget()
{
if (_display) {
obs_display_remove_draw_callback(
_display, SourcePreviewWidget::DrawCallback, this);
obs_display_destroy(_display);
_display = nullptr;
}
if (_showing) {
OBSSourceAutoRelease source =
obs_weak_source_get_source(_source);
if (source) {
obs_source_dec_showing(source);
}
_showing = false;
}
}
void SourcePreviewWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
if (!_display) {
WId wid = winId();
if (!wid) {
return;
}
gs_init_data info{};
#if defined(_WIN32)
info.window.hwnd = reinterpret_cast<void *>(wid);
#elif defined(__APPLE__)
info.window.view = reinterpret_cast<id>(wid);
#else
info.window.id = static_cast<uint32_t>(wid);
info.window.display = nullptr;
#endif
info.format = GS_BGRA;
info.zsformat = GS_ZS_NONE;
info.cx = static_cast<uint32_t>(width());
info.cy = static_cast<uint32_t>(height());
_display = obs_display_create(&info, 0x000000);
if (_display) {
obs_display_add_draw_callback(
_display, SourcePreviewWidget::DrawCallback,
this);
OBSSourceAutoRelease source =
obs_weak_source_get_source(_source);
if (source) {
obs_source_inc_showing(source);
_showing = true;
}
}
} else {
obs_display_resize(_display, static_cast<uint32_t>(width()),
static_cast<uint32_t>(height()));
}
}
void SourcePreviewWidget::DrawCallback(void *param, uint32_t cx, uint32_t cy)
{
auto self = static_cast<SourcePreviewWidget *>(param);
OBSSourceAutoRelease source = obs_weak_source_get_source(self->_source);
if (!source) {
return;
}
uint32_t srcW = std::max(obs_source_get_width(source), 1u);
uint32_t srcH = std::max(obs_source_get_height(source), 1u);
float scaleX = static_cast<float>(cx) / srcW;
float scaleY = static_cast<float>(cy) / srcH;
float scale = std::min(scaleX, scaleY);
int newW = static_cast<int>(scale * srcW);
int newH = static_cast<int>(scale * srcH);
int offX = (static_cast<int>(cx) - newW) / 2;
int offY = (static_cast<int>(cy) - newH) / 2;
self->_offsetX = offX;
self->_offsetY = offY;
self->_scale = scale;
gs_viewport_push();
gs_projection_push();
gs_ortho(0.0f, static_cast<float>(srcW), 0.0f, static_cast<float>(srcH),
-100.0f, 100.0f);
gs_set_viewport(offX, offY, newW, newH);
obs_source_video_render(source);
gs_projection_pop();
gs_viewport_pop();
}
bool SourcePreviewWidget::GetSourceRelativeXY(int widgetX, int widgetY,
int &srcX, int &srcY) const
{
OBSSourceAutoRelease source = obs_weak_source_get_source(_source);
if (!source) {
return false;
}
uint32_t srcW = std::max(obs_source_get_width(source), 1u);
uint32_t srcH = std::max(obs_source_get_height(source), 1u);
srcX = static_cast<int>((widgetX - _offsetX) / _scale);
srcY = static_cast<int>((widgetY - _offsetY) / _scale);
srcX = static_cast<int>(
clampToSource(static_cast<int32_t>(srcX), srcW));
srcY = static_cast<int>(
clampToSource(static_cast<int32_t>(srcY), srcH));
return true;
}
} // namespace advss

View File

@@ -0,0 +1,40 @@
#pragma once
#include <obs.h>
#include <graphics/graphics.h>
#include <QWidget>
class QResizeEvent;
namespace advss {
// A native window widget that renders an OBS source via obs_display.
// Stores letterbox geometry for coordinate mapping.
class SourcePreviewWidget : public QWidget {
Q_OBJECT
public:
SourcePreviewWidget(QWidget *parent, obs_weak_source_t *source);
~SourcePreviewWidget();
// Maps a widget-local point to source-space coordinates.
bool GetSourceRelativeXY(int widgetX, int widgetY, int &srcX,
int &srcY) const;
protected:
void resizeEvent(QResizeEvent *) override;
QPaintEngine *paintEngine() const override { return nullptr; }
private:
static void DrawCallback(void *param, uint32_t cx, uint32_t cy);
obs_weak_source_t *_source;
obs_display_t *_display = nullptr;
bool _showing = false;
int _offsetX = 0;
int _offsetY = 0;
float _scale = 1.0f;
};
} // namespace advss

View File

@@ -48,8 +48,17 @@ if(MSVC)
endif()
target_sources(
${PROJECT_NAME} PRIVATE macro-action-http.cpp macro-action-http.hpp
key-value-list.cpp key-value-list.hpp)
${PROJECT_NAME}
PRIVATE macro-action-http.cpp
macro-action-http.hpp
macro-condition-http.cpp
macro-condition-http.hpp
http-server.cpp
http-server.hpp
http-server-tab.cpp
http-server-tab.hpp
key-value-list.cpp
key-value-list.hpp)
setup_advss_plugin(${PROJECT_NAME})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")

View File

@@ -0,0 +1,223 @@
#include "http-server-tab.hpp"
#include "http-server.hpp"
#include "obs-module-helper.hpp"
#include "sync-helpers.hpp"
#include "tab-helpers.hpp"
#include "ui-helpers.hpp"
#include <QTabWidget>
#include <QTimer>
namespace advss {
static bool registerTab();
static void setupTab(QTabWidget *);
static bool registerTabDone = registerTab();
static HttpServersTable *tabWidget = nullptr;
static bool registerTab()
{
AddSetupTabCallback("httpServerTab", HttpServersTable::Create,
setupTab);
return true;
}
static void setTabVisible(QTabWidget *tab, bool visible)
{
SetTabVisibleByName(
tab, visible,
obs_module_text("AdvSceneSwitcher.httpServerTab.title"));
}
HttpServersTable *HttpServersTable::Create()
{
tabWidget = new HttpServersTable();
return tabWidget;
}
void HttpServersTable::Add()
{
auto newServer = std::make_shared<HttpServer>();
bool accepted = HttpServerSettingsDialog::AskForSettings(
GetSettingsWindow(), *newServer);
if (!accepted) {
return;
}
{
auto lock = LockContext();
GetHttpServers().emplace_back(newServer);
}
HttpServerSignalManager::Instance()->Add(
QString::fromStdString(newServer->Name()));
}
void HttpServersTable::Remove()
{
auto selectedRows =
tabWidget->Table()->selectionModel()->selectedRows();
if (selectedRows.empty()) {
return;
}
QStringList serverNames;
for (const auto &row : selectedRows) {
auto cell = tabWidget->Table()->item(row.row(), 0);
if (!cell) {
continue;
}
serverNames << cell->text();
}
const int count = serverNames.size();
if (count == 1) {
const QString msg = obs_module_text(
"AdvSceneSwitcher.httpServerTab.removeSingle.text");
if (!DisplayMessage(msg.arg(serverNames.at(0)), true)) {
return;
}
} else {
const QString msg = obs_module_text(
"AdvSceneSwitcher.httpServerTab.removeMultiple.text");
if (!DisplayMessage(msg.arg(count), true)) {
return;
}
}
{
auto lock = LockContext();
RemoveItemsByName(GetHttpServers(), serverNames);
}
for (const auto &name : serverNames) {
HttpServerSignalManager::Instance()->Remove(name);
}
}
static QStringList getCellLabels(HttpServer *server, bool addName = true)
{
assert(server);
QStringList result;
if (addName) {
result << QString::fromStdString(server->Name());
}
result << QString::number(server->GetPort())
<< obs_module_text(
server->IsListening()
? "AdvSceneSwitcher.httpServer.status.listening"
: "AdvSceneSwitcher.httpServer.status.stopped");
return result;
}
static void updateServerStatus(QTableWidget *table)
{
for (int row = 0; row < table->rowCount(); ++row) {
auto item = table->item(row, 0);
if (!item) {
continue;
}
auto weakServer = GetWeakHttpServerByQString(item->text());
auto server = weakServer.lock();
if (!server) {
continue;
}
UpdateItemTableRow(table, row,
getCellLabels(server.get(), false));
}
}
static void openSettingsDialog()
{
auto selectedRows =
tabWidget->Table()->selectionModel()->selectedRows();
if (selectedRows.empty()) {
return;
}
auto cell = tabWidget->Table()->item(selectedRows.last().row(), 0);
if (!cell) {
return;
}
auto weakServer = GetWeakHttpServerByQString(cell->text());
auto server = weakServer.lock();
if (!server) {
return;
}
const auto oldName = server->Name();
bool accepted = HttpServerSettingsDialog::AskForSettings(
GetSettingsWindow(), *server);
if (accepted && oldName != server->Name()) {
HttpServerSignalManager::Instance()->Rename(
QString::fromStdString(oldName),
QString::fromStdString(server->Name()));
}
updateServerStatus(tabWidget->Table());
}
static const QStringList headers =
QStringList()
<< obs_module_text("AdvSceneSwitcher.httpServerTab.name.header")
<< obs_module_text("AdvSceneSwitcher.httpServerTab.port.header")
<< obs_module_text("AdvSceneSwitcher.httpServerTab.status.header");
HttpServersTable::HttpServersTable(QTabWidget *parent)
: ResourceTable(
parent,
obs_module_text("AdvSceneSwitcher.httpServerTab.help"),
obs_module_text(
"AdvSceneSwitcher.httpServerTab.addButton.tooltip"),
obs_module_text(
"AdvSceneSwitcher.httpServerTab.removeButton.tooltip"),
headers, openSettingsDialog)
{
for (const auto &s : GetHttpServers()) {
auto server = std::static_pointer_cast<HttpServer>(s);
AddItemTableRow(Table(), getCellLabels(server.get()));
}
SetHelpVisible(GetHttpServers().empty());
}
static void setupTab(QTabWidget *tab)
{
if (GetHttpServers().empty()) {
setTabVisible(tab, false);
}
QWidget::connect(HttpServerSignalManager::Instance(),
&HttpServerSignalManager::Rename, tab,
[](const QString &oldName, const QString &newName) {
RenameItemTableRow(tabWidget->Table(), oldName,
newName);
});
QWidget::connect(
HttpServerSignalManager::Instance(),
&HttpServerSignalManager::Add, tab, [tab](const QString &name) {
AddItemTableRow(tabWidget->Table(),
getCellLabels(GetHttpServerByName(
name.toStdString())));
tabWidget->SetHelpVisible(false);
tabWidget->HighlightAddButton(false);
setTabVisible(tab, true);
});
QWidget::connect(HttpServerSignalManager::Instance(),
&HttpServerSignalManager::Remove, tab,
[](const QString &name) {
RemoveItemTableRow(tabWidget->Table(), name);
if (tabWidget->Table()->rowCount() == 0) {
tabWidget->SetHelpVisible(true);
tabWidget->HighlightAddButton(true);
}
});
auto timer = new QTimer(tabWidget);
timer->setInterval(1000);
QWidget::connect(timer, &QTimer::timeout,
[]() { updateServerStatus(tabWidget->Table()); });
timer->start();
}
} // namespace advss

View File

@@ -0,0 +1,20 @@
#pragma once
#include "resource-table.hpp"
namespace advss {
class HttpServersTable final : public ResourceTable {
Q_OBJECT
public:
static HttpServersTable *Create();
private slots:
void Add();
void Remove();
private:
HttpServersTable(QTabWidget *parent = nullptr);
};
} // namespace advss

View File

@@ -0,0 +1,364 @@
#include "http-server.hpp"
#include "layout-helpers.hpp"
#include "log-helper.hpp"
#include "obs-module-helper.hpp"
#include "plugin-state-helpers.hpp"
#include <obs-data.h>
#include <httplib.h>
#undef DispatchMessage
Q_DECLARE_METATYPE(advss::HttpServer *);
namespace advss {
static std::deque<std::shared_ptr<Item>> httpServers;
static void saveHttpServers(obs_data_t *obj);
static void loadHttpServers(obs_data_t *obj);
static bool setup();
static bool setupDone = setup();
bool setup()
{
AddSaveStep(saveHttpServers);
AddLoadStep(loadHttpServers);
AddPluginCleanupStep([]() {
for (auto &s : httpServers) {
auto server = std::dynamic_pointer_cast<HttpServer>(s);
if (server) {
server->Stop();
}
}
httpServers.clear();
});
return true;
}
static void saveHttpServers(obs_data_t *obj)
{
auto array = obs_data_array_create();
for (const auto &s : httpServers) {
auto item = obs_data_create();
s->Save(item);
obs_data_array_push_back(array, item);
obs_data_release(item);
}
obs_data_set_array(obj, "httpServers", array);
obs_data_array_release(array);
}
static void loadHttpServers(obs_data_t *obj)
{
httpServers.clear();
auto array = obs_data_get_array(obj, "httpServers");
const size_t count = obs_data_array_count(array);
for (size_t i = 0; i < count; ++i) {
auto item = obs_data_array_item(array, i);
auto server = HttpServer::Create();
httpServers.emplace_back(server);
httpServers.back()->Load(item);
obs_data_release(item);
}
obs_data_array_release(array);
}
struct HttpServer::Impl {
std::unique_ptr<httplib::Server> server;
std::thread thread;
std::mutex mutex;
std::atomic_bool listening{false};
MessageDispatcher<HttpRequest> dispatcher;
};
HttpServer::HttpServer() : _impl(std::make_unique<Impl>()) {}
HttpServer::HttpServer(const HttpServer &other)
: Item(other),
_impl(std::make_unique<Impl>())
{
_port = other._port;
_startOnLoad = other._startOnLoad;
}
HttpServer &HttpServer::operator=(const HttpServer &other)
{
if (this != &other) {
Stop();
_name = other._name;
_port = other._port;
_startOnLoad = other._startOnLoad;
}
return *this;
}
HttpServer::~HttpServer()
{
Stop();
}
bool HttpServer::IsListening() const
{
return _impl->listening.load();
}
void HttpServer::Start()
{
std::lock_guard<std::mutex> lock(_impl->mutex);
if (_impl->server) {
_impl->server->stop();
}
if (_impl->thread.joinable()) {
_impl->thread.join();
}
_impl->server = std::make_unique<httplib::Server>();
_impl->listening.store(false);
auto handleRequest = [this](const httplib::Request &req,
httplib::Response &res) {
HttpRequest request;
request.method = req.method;
request.path = req.path;
request.body = req.body;
for (const auto &[name, value] : req.headers) {
request.headers.emplace(name, value);
}
_impl->dispatcher.DispatchMessage(request);
res.status = 200;
};
_impl->server->Get(".*", handleRequest);
_impl->server->Post(".*", handleRequest);
_impl->server->Put(".*", handleRequest);
_impl->server->Patch(".*", handleRequest);
_impl->server->Delete(".*", handleRequest);
const int port = _port;
const std::string name = _name;
auto serverPtr = _impl->server.get();
auto listeningFlag = &_impl->listening;
_impl->thread = std::thread([serverPtr, listeningFlag, port, name]() {
if (!serverPtr->bind_to_port("0.0.0.0", port)) {
blog(LOG_WARNING,
"HTTP server \"%s\" failed to bind to port %d",
name.c_str(), port);
return;
}
listeningFlag->store(true);
blog(LOG_INFO, "HTTP server \"%s\" listening on port %d",
name.c_str(), port);
serverPtr->listen_after_bind();
listeningFlag->store(false);
blog(LOG_INFO, "HTTP server \"%s\" stopped", name.c_str());
});
}
void HttpServer::Stop()
{
if (!_impl) {
return;
}
std::lock_guard<std::mutex> lock(_impl->mutex);
if (_impl->server) {
_impl->server->stop();
}
if (_impl->thread.joinable()) {
_impl->thread.join();
}
_impl->server.reset();
_impl->listening.store(false);
}
HttpRequestBuffer HttpServer::RegisterForRequests()
{
return _impl->dispatcher.RegisterClient();
}
void HttpServer::Load(obs_data_t *obj)
{
Item::Load(obj);
_port = (int)obs_data_get_int(obj, "port");
_startOnLoad = obs_data_get_bool(obj, "startOnLoad");
if (_startOnLoad) {
Start();
}
}
void HttpServer::Save(obs_data_t *obj) const
{
Item::Save(obj);
obs_data_set_int(obj, "port", _port);
obs_data_set_bool(obj, "startOnLoad", _startOnLoad);
}
HttpServer *GetHttpServerByName(const std::string &name)
{
for (auto &s : httpServers) {
if (s->Name() == name) {
return dynamic_cast<HttpServer *>(s.get());
}
}
return nullptr;
}
std::weak_ptr<HttpServer> GetWeakHttpServerByName(const std::string &name)
{
for (const auto &s : httpServers) {
if (s->Name() == name) {
return std::dynamic_pointer_cast<HttpServer>(s);
}
}
return {};
}
std::weak_ptr<HttpServer> GetWeakHttpServerByQString(const QString &name)
{
return GetWeakHttpServerByName(name.toStdString());
}
std::string GetWeakHttpServerName(const std::weak_ptr<HttpServer> &server)
{
auto s = server.lock();
if (!s) {
return obs_module_text("AdvSceneSwitcher.httpServer.invalid");
}
return s->Name();
}
std::deque<std::shared_ptr<Item>> &GetHttpServers()
{
return httpServers;
}
// --- HttpServerSettingsDialog ---
static bool ServerNameAvailable(const QString &name)
{
return !GetHttpServerByName(name.toStdString());
}
static bool AskForSettingsWrapper(QWidget *parent, Item &settings)
{
HttpServer &server = dynamic_cast<HttpServer &>(settings);
return HttpServerSettingsDialog::AskForSettings(parent, server);
}
HttpServerSettingsDialog::HttpServerSettingsDialog(QWidget *parent,
const HttpServer &settings)
: ItemSettingsDialog(settings, httpServers,
"AdvSceneSwitcher.httpServer.select",
"AdvSceneSwitcher.httpServer.add",
"AdvSceneSwitcher.item.nameNotAvailable", true,
parent),
_port(new QSpinBox()),
_startOnLoad(new QCheckBox()),
_layout(new QGridLayout())
{
_port->setMinimum(1);
_port->setMaximum(65535);
_port->setValue(settings._port);
_startOnLoad->setChecked(settings._startOnLoad);
int row = 0;
_layout->addWidget(
new QLabel(obs_module_text("AdvSceneSwitcher.httpServer.name")),
row, 0);
auto nameLayout = new QHBoxLayout;
nameLayout->addWidget(_name);
nameLayout->addWidget(_nameHint);
_layout->addLayout(nameLayout, row, 1);
++row;
_layout->addWidget(
new QLabel(obs_module_text("AdvSceneSwitcher.httpServer.port")),
row, 0);
_layout->addWidget(_port, row, 1);
++row;
_layout->addWidget(new QLabel(obs_module_text(
"AdvSceneSwitcher.httpServer.startOnLoad")),
row, 0);
_layout->addWidget(_startOnLoad, row, 1);
++row;
_layout->addWidget(_buttonbox, row, 0, 1, -1);
setLayout(_layout);
MinimizeSizeOfColumn(_layout, 0);
}
bool HttpServerSettingsDialog::AskForSettings(QWidget *parent,
HttpServer &settings)
{
HttpServerSettingsDialog dialog(parent, settings);
dialog.setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle"));
if (dialog.exec() != DialogCode::Accepted) {
return false;
}
settings._name = dialog._name->text().toStdString();
settings._port = dialog._port->value();
settings._startOnLoad = dialog._startOnLoad->isChecked();
settings.Start();
return true;
}
HttpServerSelection::HttpServerSelection(QWidget *parent)
: ItemSelection(httpServers, HttpServer::Create, AskForSettingsWrapper,
"AdvSceneSwitcher.httpServer.select",
"AdvSceneSwitcher.httpServer.add",
"AdvSceneSwitcher.item.nameNotAvailable",
"AdvSceneSwitcher.httpServer.configure", parent)
{
QWidget::connect(HttpServerSignalManager::Instance(),
SIGNAL(Rename(const QString &, const QString &)), this,
SLOT(RenameItem(const QString &, const QString &)));
QWidget::connect(HttpServerSignalManager::Instance(),
SIGNAL(Add(const QString &)), this,
SLOT(AddItem(const QString &)));
QWidget::connect(HttpServerSignalManager::Instance(),
SIGNAL(Remove(const QString &)), this,
SLOT(RemoveItem(const QString &)));
QWidget::connect(this,
SIGNAL(ItemRenamed(const QString &, const QString &)),
HttpServerSignalManager::Instance(),
SIGNAL(Rename(const QString &, const QString &)));
QWidget::connect(this, SIGNAL(ItemAdded(const QString &)),
HttpServerSignalManager::Instance(),
SIGNAL(Add(const QString &)));
QWidget::connect(this, SIGNAL(ItemRemoved(const QString &)),
HttpServerSignalManager::Instance(),
SIGNAL(Remove(const QString &)));
}
void HttpServerSelection::SetServer(const std::string &name)
{
if (GetHttpServerByName(name)) {
SetItem(name);
} else {
SetItem("");
}
}
void HttpServerSelection::SetServer(const std::weak_ptr<HttpServer> &server)
{
auto s = server.lock();
if (s) {
SetItem(s->Name());
} else {
SetItem("");
}
}
HttpServerSignalManager::HttpServerSignalManager(QObject *parent)
: QObject(parent)
{
}
HttpServerSignalManager *HttpServerSignalManager::Instance()
{
static HttpServerSignalManager manager;
return &manager;
}
} // namespace advss

View File

@@ -0,0 +1,101 @@
#pragma once
#include "item-selection-helpers.hpp"
#include "message-buffer.hpp"
#include "message-dispatcher.hpp"
#include <map>
#include <memory>
#include <string>
#include <QCheckBox>
#include <QGridLayout>
#include <QSpinBox>
namespace advss {
struct HttpRequest {
std::string method;
std::string path;
std::string body;
std::map<std::string, std::string> headers;
};
using HttpRequestBuffer = std::shared_ptr<MessageBuffer<HttpRequest>>;
class HttpServerSelection;
class HttpServerSettingsDialog;
class HttpServer : public Item {
public:
HttpServer();
HttpServer(const HttpServer &);
HttpServer &operator=(const HttpServer &);
~HttpServer();
static std::shared_ptr<Item> Create()
{
return std::make_shared<HttpServer>();
}
void Load(obs_data_t *obj);
void Save(obs_data_t *obj) const;
HttpRequestBuffer RegisterForRequests();
int GetPort() const { return _port; }
bool IsListening() const;
void Start();
void Stop();
private:
int _port = 16384;
bool _startOnLoad = true;
struct Impl;
std::unique_ptr<Impl> _impl;
friend HttpServerSelection;
friend HttpServerSettingsDialog;
};
class HttpServerSettingsDialog : public ItemSettingsDialog {
Q_OBJECT
public:
HttpServerSettingsDialog(QWidget *parent, const HttpServer &);
static bool AskForSettings(QWidget *parent, HttpServer &settings);
private:
QSpinBox *_port;
QCheckBox *_startOnLoad;
QGridLayout *_layout;
};
class HttpServerSelection : public ItemSelection {
Q_OBJECT
public:
HttpServerSelection(QWidget *parent = nullptr);
void SetServer(const std::string &);
void SetServer(const std::weak_ptr<HttpServer> &);
};
class HttpServerSignalManager : public QObject {
Q_OBJECT
public:
HttpServerSignalManager(QObject *parent = nullptr);
static HttpServerSignalManager *Instance();
signals:
void Rename(const QString &, const QString &);
void Add(const QString &);
void Remove(const QString &);
};
HttpServer *GetHttpServerByName(const std::string &);
std::weak_ptr<HttpServer> GetWeakHttpServerByName(const std::string &);
std::weak_ptr<HttpServer> GetWeakHttpServerByQString(const QString &);
std::string GetWeakHttpServerName(const std::weak_ptr<HttpServer> &);
std::deque<std::shared_ptr<Item>> &GetHttpServers();
} // namespace advss

View File

@@ -112,48 +112,56 @@ static URLInfo getURLInfo(const std::string &input, bool keepParams)
bool MacroActionHttp::PerformAction()
{
// Capture all config while holding the segment lock
const auto [host, path] = getURLInfo(_url, !_setParams);
httplib::Client cli(host);
setTimeout(cli, _timeout);
const auto params = _setParams ? getParams(_params) : httplib::Params();
const auto headers = _setHeaders ? getHeaders(_headers)
: httplib::Headers();
const auto method = _method;
const std::string body = _body;
const std::string contentType = _contentType;
// Release the segment lock for the blocking network call
httplib::Result response;
switch (_method) {
case MacroActionHttp::Method::GET:
response = cli.Get(path, params, headers);
break;
case MacroActionHttp::Method::POST: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response =
cli.Post(pathWithParam, headers, _body, _contentType);
break;
}
case MacroActionHttp::Method::PUT: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response = cli.Put(pathWithParam, headers, _body, _contentType);
break;
}
case MacroActionHttp::Method::PATCH: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response =
cli.Patch(pathWithParam, headers, _body, _contentType);
break;
}
case MacroActionHttp::Method::DELETE: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response =
cli.Delete(pathWithParam, headers, _body, _contentType);
break;
}
default:
break;
{
SuspendLock suspendLock(*this);
switch (method) {
case MacroActionHttp::Method::GET:
response = cli.Get(path, params, headers);
break;
case MacroActionHttp::Method::POST: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response = cli.Post(pathWithParam, headers, body,
contentType);
break;
}
case MacroActionHttp::Method::PUT: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response = cli.Put(pathWithParam, headers, body,
contentType);
break;
}
case MacroActionHttp::Method::PATCH: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response = cli.Patch(pathWithParam, headers, body,
contentType);
break;
}
case MacroActionHttp::Method::DELETE: {
const auto pathWithParam =
httplib::append_query_params(path, params);
response = cli.Delete(pathWithParam, headers, body,
contentType);
break;
}
default:
break;
}
}
if (VerboseLoggingEnabled() && !response) {
@@ -485,10 +493,10 @@ void MacroActionHttpEdit::SetWidgetLayout()
auto layout = new QVBoxLayout;
layout->addLayout(actionLayout);
layout->addWidget(_setHeaders);
layout->addLayout(_headerListLayout);
layout->addWidget(_setParams);
layout->addLayout(_paramListLayout);
layout->addWidget(_setHeaders);
layout->addLayout(_headerListLayout);
layout->addLayout(_contentTypeLayout);
layout->addLayout(_bodyLayout);
layout->addLayout(timeoutLayout);

View File

@@ -0,0 +1,313 @@
#include "macro-condition-http.hpp"
#include "layout-helpers.hpp"
#include "macro-helpers.hpp"
#undef DELETE
namespace advss {
const std::string MacroConditionHttp::id = "http";
bool MacroConditionHttp::_registered = MacroConditionFactory::Register(
MacroConditionHttp::id,
{MacroConditionHttp::Create, MacroConditionHttpEdit::Create,
"AdvSceneSwitcher.condition.http"});
static const std::map<MacroConditionHttp::Method, std::string> methodLabels = {
{MacroConditionHttp::Method::ANY,
"AdvSceneSwitcher.condition.http.method.any"},
{MacroConditionHttp::Method::GET,
"AdvSceneSwitcher.condition.http.method.get"},
{MacroConditionHttp::Method::POST,
"AdvSceneSwitcher.condition.http.method.post"},
{MacroConditionHttp::Method::PUT,
"AdvSceneSwitcher.condition.http.method.put"},
{MacroConditionHttp::Method::PATCH,
"AdvSceneSwitcher.condition.http.method.patch"},
{MacroConditionHttp::Method::DELETE,
"AdvSceneSwitcher.condition.http.method.delete"},
};
static std::string_view methodToString(MacroConditionHttp::Method method)
{
switch (method) {
case MacroConditionHttp::Method::GET:
return "GET";
case MacroConditionHttp::Method::POST:
return "POST";
case MacroConditionHttp::Method::PUT:
return "PUT";
case MacroConditionHttp::Method::PATCH:
return "PATCH";
case MacroConditionHttp::Method::DELETE:
return "DELETE";
default:
break;
}
return "";
}
void MacroConditionHttp::SetServer(const std::string &name)
{
_server = GetWeakHttpServerByName(name);
auto server = _server.lock();
if (!server) {
_requestBuffer.reset();
return;
}
_requestBuffer = server->RegisterForRequests();
}
bool MacroConditionHttp::CheckCondition()
{
if (!_requestBuffer) {
return false;
}
const bool macroWasPausedSinceLastCheck =
MacroWasPausedSince(GetMacro(), _lastCheck);
_lastCheck = std::chrono::high_resolution_clock::now();
if (macroWasPausedSinceLastCheck) {
_requestBuffer->Clear();
return false;
}
while (!_requestBuffer->Empty()) {
auto request = _requestBuffer->ConsumeMessage();
if (!request) {
continue;
}
SetTempVarValue("method", request->method);
SetTempVarValue("path", request->path);
SetTempVarValue("body", request->body);
if (_method != Method::ANY &&
request->method != methodToString(_method)) {
continue;
}
const std::string pathPattern = std::string(_path);
if (_pathRegex.Enabled()) {
if (!_pathRegex.Matches(request->path, _path)) {
continue;
}
} else {
if (request->path != pathPattern) {
continue;
}
}
const std::string bodyPattern = std::string(_body);
if (_bodyRegex.Enabled()) {
if (!_bodyRegex.Matches(request->body, _body)) {
continue;
}
} else {
if (request->body != bodyPattern) {
continue;
}
}
if (_clearBufferOnMatch) {
_requestBuffer->Clear();
}
return true;
}
return false;
}
bool MacroConditionHttp::Save(obs_data_t *obj) const
{
MacroCondition::Save(obj);
obs_data_set_int(obj, "method", static_cast<int>(_method));
_path.Save(obj, "path");
_pathRegex.Save(obj, "pathRegex");
_body.Save(obj, "body");
_bodyRegex.Save(obj, "bodyRegex");
obs_data_set_bool(obj, "clearBufferOnMatch", _clearBufferOnMatch);
auto server = _server.lock();
obs_data_set_string(obj, "server",
server ? server->Name().c_str() : "");
return true;
}
bool MacroConditionHttp::Load(obs_data_t *obj)
{
MacroCondition::Load(obj);
_method = static_cast<Method>(obs_data_get_int(obj, "method"));
_path.Load(obj, "path");
_pathRegex.Load(obj, "pathRegex");
_body.Load(obj, "body");
_bodyRegex.Load(obj, "bodyRegex");
_clearBufferOnMatch = obs_data_get_bool(obj, "clearBufferOnMatch");
if (!obs_data_has_user_value(obj, "clearBufferOnMatch")) {
_clearBufferOnMatch = true;
}
SetServer(obs_data_get_string(obj, "server"));
return true;
}
std::string MacroConditionHttp::GetShortDesc() const
{
auto server = _server.lock();
return server ? server->Name() : "";
}
void MacroConditionHttp::SetupTempVars()
{
MacroCondition::SetupTempVars();
AddTempvar("method",
obs_module_text("AdvSceneSwitcher.tempVar.http.method"),
obs_module_text(
"AdvSceneSwitcher.tempVar.http.method.description"));
AddTempvar("path",
obs_module_text("AdvSceneSwitcher.tempVar.http.path"),
obs_module_text(
"AdvSceneSwitcher.tempVar.http.path.description"));
AddTempvar("body",
obs_module_text("AdvSceneSwitcher.tempVar.http.body"),
obs_module_text(
"AdvSceneSwitcher.tempVar.http.body.description"));
}
static void populateMethodSelection(QComboBox *list)
{
for (const auto &[value, name] : methodLabels) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(value));
}
}
MacroConditionHttpEdit::MacroConditionHttpEdit(
QWidget *parent, std::shared_ptr<MacroConditionHttp> entryData)
: QWidget(parent),
_method(new QComboBox(this)),
_server(new HttpServerSelection(this)),
_path(new VariableLineEdit(this)),
_pathRegex(new RegexConfigWidget(this)),
_body(new VariableTextEdit(this)),
_bodyRegex(new RegexConfigWidget(this)),
_clearBufferOnMatch(new QCheckBox(
obs_module_text("AdvSceneSwitcher.clearBufferOnMatch")))
{
populateMethodSelection(_method);
QWidget::connect(_method, SIGNAL(currentIndexChanged(int)), this,
SLOT(MethodChanged(int)));
QWidget::connect(_server, SIGNAL(SelectionChanged(const QString &)),
this, SLOT(ServerChanged(const QString &)));
QWidget::connect(_path, SIGNAL(editingFinished()), this,
SLOT(PathChanged()));
QWidget::connect(_pathRegex,
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
SLOT(PathRegexChanged(const RegexConfig &)));
QWidget::connect(_body, SIGNAL(textChanged()), this,
SLOT(BodyChanged()));
QWidget::connect(_bodyRegex,
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
SLOT(BodyRegexChanged(const RegexConfig &)));
QWidget::connect(_clearBufferOnMatch, SIGNAL(stateChanged(int)), this,
SLOT(ClearBufferOnMatchChanged(int)));
auto topLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.http.layout"),
topLayout,
{{"{{method}}", _method}, {"{{server}}", _server}});
auto pathLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.http.layout.path"),
pathLayout, {{"{{path}}", _path}, {"{{regex}}", _pathRegex}},
false);
auto bodyLayout = new QHBoxLayout;
PlaceWidgets(
obs_module_text("AdvSceneSwitcher.condition.http.layout.body"),
bodyLayout, {{"{{body}}", _body}, {"{{regex}}", _bodyRegex}},
false);
auto mainLayout = new QVBoxLayout;
mainLayout->addLayout(topLayout);
mainLayout->addLayout(pathLayout);
mainLayout->addLayout(bodyLayout);
mainLayout->addWidget(_clearBufferOnMatch);
setLayout(mainLayout);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroConditionHttpEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_method->setCurrentIndex(
_method->findData(static_cast<int>(_entryData->_method)));
_server->SetServer(_entryData->GetServer());
_path->setText(_entryData->_path);
_pathRegex->SetRegexConfig(_entryData->_pathRegex);
_body->setPlainText(_entryData->_body);
_bodyRegex->SetRegexConfig(_entryData->_bodyRegex);
_clearBufferOnMatch->setChecked(_entryData->_clearBufferOnMatch);
adjustSize();
updateGeometry();
}
void MacroConditionHttpEdit::MethodChanged(int idx)
{
GUARD_LOADING_AND_LOCK();
_entryData->_method = static_cast<MacroConditionHttp::Method>(
_method->itemData(idx).toInt());
}
void MacroConditionHttpEdit::ServerChanged(const QString &name)
{
GUARD_LOADING_AND_LOCK();
_entryData->SetServer(name.toStdString());
emit HeaderInfoChanged(
QString::fromStdString(_entryData->GetShortDesc()));
}
void MacroConditionHttpEdit::PathChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_path = _path->text().toStdString();
}
void MacroConditionHttpEdit::PathRegexChanged(const RegexConfig &conf)
{
GUARD_LOADING_AND_LOCK();
_entryData->_pathRegex = conf;
adjustSize();
updateGeometry();
}
void MacroConditionHttpEdit::BodyChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_body = _body->toPlainText().toUtf8().constData();
adjustSize();
updateGeometry();
}
void MacroConditionHttpEdit::BodyRegexChanged(const RegexConfig &conf)
{
GUARD_LOADING_AND_LOCK();
_entryData->_bodyRegex = conf;
adjustSize();
updateGeometry();
}
void MacroConditionHttpEdit::ClearBufferOnMatchChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_clearBufferOnMatch = value;
}
} // namespace advss

View File

@@ -0,0 +1,95 @@
#pragma once
#include "macro-condition-edit.hpp"
#include "http-server.hpp"
#include "variable-line-edit.hpp"
#include "variable-text-edit.hpp"
#include "regex-config.hpp"
#include <QCheckBox>
#include <QComboBox>
namespace advss {
class MacroConditionHttp : public MacroCondition {
public:
MacroConditionHttp(Macro *m) : MacroCondition(m, true) {}
bool CheckCondition();
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetShortDesc() const;
std::string GetId() const { return id; }
static std::shared_ptr<MacroCondition> Create(Macro *m)
{
return std::make_shared<MacroConditionHttp>(m);
}
enum class Method {
ANY = 0,
GET,
POST,
PUT,
PATCH,
DELETE,
};
void SetServer(const std::string &name);
std::weak_ptr<HttpServer> GetServer() const { return _server; }
StringVariable _path = ".*";
StringVariable _body = ".*";
RegexConfig _pathRegex = RegexConfig::PartialMatchRegexConfig(true);
RegexConfig _bodyRegex = RegexConfig::PartialMatchRegexConfig(true);
Method _method = Method::ANY;
bool _clearBufferOnMatch = true;
private:
void SetupTempVars();
std::weak_ptr<HttpServer> _server;
HttpRequestBuffer _requestBuffer;
std::chrono::high_resolution_clock::time_point _lastCheck{};
static bool _registered;
static const std::string id;
};
class MacroConditionHttpEdit : public QWidget {
Q_OBJECT
public:
MacroConditionHttpEdit(
QWidget *parent,
std::shared_ptr<MacroConditionHttp> cond = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroCondition> cond)
{
return new MacroConditionHttpEdit(
parent,
std::dynamic_pointer_cast<MacroConditionHttp>(cond));
}
private slots:
void MethodChanged(int);
void ServerChanged(const QString &);
void PathChanged();
void PathRegexChanged(const RegexConfig &);
void BodyChanged();
void BodyRegexChanged(const RegexConfig &);
void ClearBufferOnMatchChanged(int);
signals:
void HeaderInfoChanged(const QString &);
private:
QComboBox *_method;
HttpServerSelection *_server;
VariableLineEdit *_path;
RegexConfigWidget *_pathRegex;
VariableTextEdit *_body;
RegexConfigWidget *_bodyRegex;
QCheckBox *_clearBufferOnMatch;
std::shared_ptr<MacroConditionHttp> _entryData;
bool _loading = true;
};
} // namespace advss

View File

@@ -35,6 +35,7 @@ bool MacroActionScript::PerformAction()
return true;
}
SuspendLock suspendLock(static_cast<MacroAction &>(*this));
(void)SendTriggerSignal();
return true;
}

View File

@@ -36,6 +36,7 @@ bool MacroConditionScript::CheckCondition()
return false;
}
SuspendLock suspendLock(static_cast<MacroCondition &>(*this));
return SendTriggerSignal();
}

View File

@@ -54,7 +54,9 @@ MacroSegmentScriptInlineEdit::MacroSegmentScriptInlineEdit(
_scriptType(new QComboBox(this)),
_language(new QComboBox(this)),
_script(new ScriptEditor(this)),
_path(new FileSelection(FileSelection::Type::WRITE, this)),
_path(new FileSelection(
FileSelection::Type::WRITE, this,
obs_module_text("AdvSceneSwitcher.script.file.select"))),
_openFile(new QPushButton(
obs_module_text("AdvSceneSwitcher.script.file.open"), this)),
_fileLayout(new QHBoxLayout()),

View File

@@ -68,10 +68,15 @@ void ContentClassification::SetContentClassification(
data = obs_data_create();
obs_data_set_array(data, "content_classification_labels", ccls);
const auto id = token.GetUserID();
if (!id) {
vblog(LOG_INFO, "%s skip - invalid user id", __func__);
return;
}
auto result = SendPatchRequest(token, "https://api.twitch.tv",
"/helix/channels",
{{"broadcaster_id", token.GetUserID()}},
data.Get());
{{"broadcaster_id", *id}}, data.Get());
if (result.status != 204) {
blog(LOG_INFO,

View File

@@ -207,6 +207,26 @@ static obs_data_t *copyData(const OBSData &data)
return obs_data_create_from_json(json);
}
void EventSub::LogActiveSubscriptions() const
{
if (!VerboseLoggingEnabled()) {
return;
}
if (_activeSubscriptions.empty()) {
blog(LOG_INFO, "Twitch EventSub active subscriptions: none");
return;
}
blog(LOG_INFO, "Twitch EventSub active subscriptions: %zu",
_activeSubscriptions.size());
for (const auto &sub : _activeSubscriptions) {
const char *type = obs_data_get_string(sub.data, "type");
blog(LOG_INFO, " id=%s type=%s", sub.id.c_str(),
type ? type : "");
}
}
std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
Subscription subscription)
{
@@ -218,7 +238,12 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
std::unique_lock<std::mutex> lock(eventSub->_subscriptionMtx);
if (!eventSub->_connected) {
vblog(LOG_INFO, "Twitch EventSub connect started for %s",
if (eventSub->_reconnecting) {
return "";
}
vblog(LOG_INFO,
"new Twitch EventSub connect attempt started for %s",
token->GetName().c_str());
lock.unlock();
eventSub->Connect();
@@ -226,9 +251,17 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
}
if (isAlreadySubscribed(eventSub->_activeSubscriptions, subscription)) {
eventSub->LogActiveSubscriptions();
return eventSub->_activeSubscriptions.find(subscription)->id;
}
if (!eventSub->IsValidID(eventSub->_sessionID)) {
vblog(LOG_INFO,
"session ID of Twitch event sub invalid - skip %s",
__func__);
return "";
}
OBSDataAutoRelease postData = copyData(subscription.data);
setTransportData(postData.Get(), eventSub->_sessionID);
auto result = SendPostRequest(*token, registerSubscriptionURL.data(),
@@ -236,8 +269,14 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
postData.Get());
if (result.status != 202) {
vblog(LOG_INFO, "failed to register Twitch EventSub (%d)",
result.status);
const char *error = obs_data_get_string(result.data, "error");
const char *message =
obs_data_get_string(result.data, "message");
vblog(LOG_INFO,
"failed to register Twitch EventSub (%d): %s - %s",
result.status, error ? error : "no error",
message ? message : "no message");
eventSub->LogActiveSubscriptions();
return "";
}
@@ -246,6 +285,7 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
OBSDataAutoRelease replyData = obs_data_array_item(replyArray, 0);
subscription.id = obs_data_get_string(replyData, "id");
eventSub->_activeSubscriptions.emplace(subscription);
eventSub->LogActiveSubscriptions();
return subscription.id;
}
@@ -402,7 +442,9 @@ void EventSub::HandleWelcome(obs_data_t *data)
{
OBSDataAutoRelease session = obs_data_get_obj(data, "session");
_sessionID = obs_data_get_string(session, "id");
ClearActiveSubscriptions();
blog(LOG_INFO, "Twitch EventSub connected");
vblog(LOG_INFO, "Twitch EventSub session id: %s", _sessionID.c_str());
}
void EventSub::HandleKeepAlive() const
@@ -430,11 +472,14 @@ void EventSub::OnServerMigrationWelcome(
// Disable reconnect handling for old connection which will be closed
_client->set_close_handler([](connection_hdl) {});
_client->set_fail_handler([](connection_hdl) {});
auto connection = _client->get_con_from_hdl(_connection);
connection->set_close_handler([](connection_hdl) {
vblog(LOG_INFO, "previous Twitch EventSub connection closed");
});
connection->set_fail_handler([](connection_hdl) {});
if (!_connection.expired()) {
auto connection = _client->get_con_from_hdl(_connection);
connection->set_close_handler([](connection_hdl) {
vblog(LOG_INFO,
"previous Twitch EventSub connection closed");
});
connection->set_fail_handler([](connection_hdl) {});
}
websocketpp::lib::error_code ec;
_client->close(_connection, websocketpp::close::status::normal,
@@ -442,6 +487,7 @@ void EventSub::OnServerMigrationWelcome(
_client.swap(newClient);
_sessionID = _migrationSessionID;
vblog(LOG_INFO, "Twitch EventSub session id: %s", _sessionID.c_str());
_connection = newHdl;
_client->set_open_handler(bind(&EventSub::OnOpen, this, _1));
@@ -461,17 +507,16 @@ void EventSub::OnServerMigrationWelcome(
void EventSub::StartServerMigrationClient(const std::string &url)
{
auto client = std::make_unique<EventSubWSClient>();
SetupClient(*client);
_migrationClient = std::make_unique<EventSubWSClient>();
SetupClient(*_migrationClient);
client->set_open_handler([this](connection_hdl) {
_migrationClient->set_open_handler([this](connection_hdl) {
vblog(LOG_INFO, "Twitch EventSub migration client opened");
});
client->set_message_handler([this,
&client](connection_hdl hdl,
EventSubWSClient::message_ptr
message) {
_migrationClient->set_message_handler([this](connection_hdl hdl,
EventSubWSClient::message_ptr
message) {
const auto msg = ParseWebSocketMessage(message);
if (!msg) {
return;
@@ -487,14 +532,14 @@ void EventSub::StartServerMigrationClient(const std::string &url)
obs_data_get_obj(data, "session");
_migrationSessionID =
obs_data_get_string(session, "id");
OnServerMigrationWelcome(hdl, client);
OnServerMigrationWelcome(hdl, _migrationClient);
} else {
OnMessage(hdl, message);
}
});
websocketpp::lib::error_code ec;
auto con = client->get_connection(url, ec);
auto con = _migrationClient->get_connection(url, ec);
if (ec) {
blog(LOG_ERROR,
"Twitch EventSub migration connection failed: %s",
@@ -504,8 +549,8 @@ void EventSub::StartServerMigrationClient(const std::string &url)
}
_migrationConnection = con;
client->connect(con);
client->run();
_migrationClient->connect(con);
_migrationClient->run();
}
void EventSub::HandleServerMigration(obs_data_t *data)
@@ -563,7 +608,7 @@ void EventSub::HandleRevocation(obs_data_t *data)
std::lock_guard<std::mutex> lock(_subscriptionMtx);
for (auto it = _activeSubscriptions.begin();
it != _activeSubscriptions.begin();) {
it != _activeSubscriptions.end();) {
if (it->id == id) {
it = _activeSubscriptions.erase(it);
} else {

View File

@@ -81,6 +81,8 @@ private:
void HandleServerMigration(obs_data_t *);
void HandleRevocation(obs_data_t *);
void LogActiveSubscriptions() const;
void RegisterInstance();
void UnregisterInstance();

View File

@@ -189,12 +189,17 @@ void LanguageSelection::Save(obs_data_t *obj) const
void LanguageSelection::SetStreamLanguage(const TwitchToken &token) const
{
const auto id = token.GetUserID();
if (!id) {
vblog(LOG_INFO, "%s skip - invalid user id", __func__);
return;
}
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "broadcaster_language", _language.c_str());
auto result = SendPatchRequest(token, "https://api.twitch.tv",
"/helix/channels",
{{"broadcaster_id", token.GetUserID()}},
data.Get());
{{"broadcaster_id", *id}}, data.Get());
if (result.status != 204) {
blog(LOG_INFO, "Failed to set stream language! (%d)",

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@
#include <variable-line-edit.hpp>
#include <variable-text-edit.hpp>
#include <duration-control.hpp>
#include <duration.hpp>
namespace advss {
@@ -182,11 +183,13 @@ public:
StringVariable _announcementMessage = obs_module_text(
"AdvSceneSwitcher.action.twitch.announcement.message");
AnnouncementColor _announcementColor = AnnouncementColor::PRIMARY;
int _nonModDelayDuration = 2;
TwitchChannel _channel;
StringVariable _chatMessage;
UserInfoQueryType _userInfoQueryType = UserInfoQueryType::LOGIN;
StringVariable _userLogin = "user login";
DoubleVariable _userId = 0;
StringVariable _banReason = "";
TwitchPointsReward _pointsReward;
std::weak_ptr<Variable> _rewardVariable;
bool _useVariableForRewardSelection = false;
@@ -198,14 +201,14 @@ private:
void CreateStreamClip(const std::shared_ptr<TwitchToken> &) const;
void StartCommercial(const std::shared_ptr<TwitchToken> &) const;
void SendChatAnnouncement(const std::shared_ptr<TwitchToken> &) const;
void SetChatEmoteOnlyMode(const std::shared_ptr<TwitchToken> &,
bool enable) const;
void StartRaid(const std::shared_ptr<TwitchToken> &);
void SendChatMessage(const std::shared_ptr<TwitchToken> &);
void GetUserInfo(const std::shared_ptr<TwitchToken> &);
void GetRewardInfo(const std::shared_ptr<TwitchToken> &);
void GetChannelInfo(const std::shared_ptr<TwitchToken> &token);
std::optional<std::string>
GetTargetUserID(const std::shared_ptr<TwitchToken> &) const;
bool ResolveVariableSelectionToRewardId(
const std::shared_ptr<TwitchToken> &);
@@ -251,11 +254,13 @@ private slots:
void DurationChanged(const Duration &);
void AnnouncementMessageChanged();
void AnnouncementColorChanged(int index);
void NonModDelayDurationChanged(int index);
void ChannelChanged(const TwitchChannel &);
void ChatMessageChanged();
void UserInfoQueryTypeChanged(int);
void UserLoginChanged();
void UserIdChanged(const NumberVariable<double> &);
void BanReasonChanged();
void PointsRewardChanged(const TwitchPointsReward &);
void RewardVariableChanged(const QString &);
void ToggleRewardSelection(bool);
@@ -274,6 +279,8 @@ private:
void SetTokenWarning(bool visible, const QString &text = "");
QHBoxLayout *_layout;
QWidget *_userModerationRow;
QHBoxLayout *_layout2;
FilterComboBox *_actions;
TwitchConnectionSelection *_tokens;
QLabel *_tokenWarning;
@@ -288,6 +295,7 @@ private:
DurationSelection *_duration;
VariableTextEdit *_announcementMessage;
QComboBox *_announcementColor;
QComboBox *_nonModDelayDuration;
TwitchChannelSelection *_channel;
VariableTextEdit *_chatMessage;
QComboBox *_userInfoQueryType;
@@ -295,6 +303,7 @@ private:
// QSpinBox uses int internally, which is too small for Twitch IDs, so
// we use QDoubleSpinBox instead
VariableDoubleSpinBox *_userId;
VariableLineEdit *_banReason;
TwitchPointsRewardWidget *_pointsReward;
VariableSelection *_rewardVariable;
QPushButton *_toggleRewardSelection;

View File

@@ -1096,6 +1096,12 @@ void MacroConditionTwitch::AddChannelGenericEventSubscription(
return;
}
const auto id = token->GetUserID();
if (!id) {
vblog(LOG_INFO, "%s skip - invalid user id", __func__);
return;
}
const auto channelID = _channel.GetUserID(*token);
if (!TwitchChannel::IsValid(channelID)) {
vblog(LOG_INFO, "skip %s because of invalid channel selection",
@@ -1114,7 +1120,7 @@ void MacroConditionTwitch::AddChannelGenericEventSubscription(
if (includeModeratorId) {
obs_data_set_string(condition, "moderator_user_id",
token->GetUserID().c_str());
id->c_str());
}
obs_data_apply(condition, extraConditions);

View File

@@ -26,6 +26,12 @@ void TwitchTagList::Save(obs_data_t *obj) const
void TwitchTagList::SetStreamTags(const TwitchToken &token) const
{
const auto id = token.GetUserID();
if (!id) {
vblog(LOG_INFO, "%s skip - invalid user id", __func__);
return;
}
nlohmann::json j;
j["tags"] = toVector();
@@ -47,8 +53,7 @@ void TwitchTagList::SetStreamTags(const TwitchToken &token) const
auto result = SendPatchRequest(token, "https://api.twitch.tv",
"/helix/channels",
{{"broadcaster_id", token.GetUserID()}},
j.dump());
{{"broadcaster_id", *id}}, j.dump());
if (result.status != 204) {
blog(LOG_INFO, "Failed to set stream tags! (%d)",

View File

@@ -206,7 +206,9 @@ void TwitchToken::Save(obs_data_t *obj) const
{
Item::Save(obj);
obs_data_set_string(obj, "token", _token.c_str());
obs_data_set_string(obj, "userID", _userID.c_str());
if (_userID) {
obs_data_set_string(obj, "userID", _userID->c_str());
}
obs_data_set_bool(obj, "validateEventSubTimestamps",
_validateEventSubTimestamps);
obs_data_set_bool(obj, "warnIfInvalid", _warnIfInvalid);
@@ -266,7 +268,7 @@ void TwitchToken::SetToken(const std::string &value)
SendGetRequest(*this, "https://api.twitch.tv", "/helix/users");
if (res.status != 200) {
blog(LOG_WARNING, "failed to get Twitch user id from token!");
_userID = -1;
_userID = {};
return;
}
@@ -318,12 +320,35 @@ bool TwitchToken::IsValid(bool forceUpdate) const
cli.Get("/oauth2/validate", httplib::Params{}, headers);
_lastValidityCheckTime = std::chrono::system_clock::now();
_lastValidityCheckValue = _token;
_lastValidityCheckResult = response && response->status == 200;
if (!_lastValidityCheckResult) {
if (!response || response->status != 200) {
blog(LOG_INFO, "Twitch token %s is not valid!",
_name.c_str());
_lastValidityCheckResult = false;
return false;
}
return _lastValidityCheckResult;
OBSDataAutoRelease replyData =
obs_data_create_from_json(response->body.c_str());
const char *id = obs_data_get_string(replyData, "user_id");
if (!id) {
blog(LOG_INFO,
"Twitch token %s does validity check did not report user_id! Assume invalid!",
_name.c_str());
_lastValidityCheckResult = false;
return false;
}
if (_userID && _userID != id) {
blog(LOG_INFO,
"Twitch token %s does not match expected user (got %s, expected %s)!",
_name.c_str(), id, _userID->c_str());
_lastValidityCheckResult = false;
return false;
}
_lastValidityCheckResult = true;
return true;
};
const bool tokenChanged = _lastValidityCheckValue != _token;
@@ -729,6 +754,7 @@ void TwitchTokenSettingsDialog::RequestToken()
auto scope = QString::fromStdString(
generateScopeString(GetEnabledOptions()));
_validationTimer.stop();
_tokenGrabber.SetTokenScope(scope);
_tokenGrabber.start();
_tokenStatus->setText(obs_module_text(
@@ -766,6 +792,7 @@ void TwitchTokenSettingsDialog::GotToken(const std::optional<QString> &value)
Q_ARG(const QString &, name));
SetTokenInfoVisible(true);
_requestToken->setEnabled(true);
_validationTimer.start();
}
std::set<TokenOption> TwitchTokenSettingsDialog::GetEnabledOptions()

View File

@@ -51,7 +51,7 @@ public:
void SetToken(const std::string &);
bool IsEmpty() const { return _token.empty(); }
std::optional<std::string> GetToken() const;
std::string GetUserID() const { return _userID; }
std::optional<std::string> GetUserID() const { return _userID; }
std::shared_ptr<EventSub> GetEventSub();
bool ValidateTimestamps() const { return _validateEventSubTimestamps; }
bool IsValid(bool forceUpdate = false) const;
@@ -65,7 +65,7 @@ private:
mutable std::string _lastValidityCheckValue;
mutable bool _lastValidityCheckResult = false;
mutable std::chrono::system_clock::time_point _lastValidityCheckTime;
std::string _userID;
std::optional<std::string> _userID;
std::set<TokenOption> _tokenOptions = TokenOption::GetAllTokenOptions();
std::shared_ptr<EventSub> _eventSub;
bool _validateEventSubTimestamps = false;

View File

@@ -185,6 +185,11 @@ static RequestResult processResult(const httplib::Result &response,
return result;
}
if (response->status < 200 || response->status >= 300) {
vblog(LOG_INFO, "Twitch API error response: %s",
response->body.c_str());
}
OBSDataAutoRelease replyData =
obs_data_create_from_json(response->body.c_str());
result.data = replyData;
@@ -518,6 +523,13 @@ static RequestResult sendDeleteRequest(const TwitchToken &token,
return processResult(response, __func__);
}
RequestResult SendDeleteRequest(const TwitchToken &token,
const std::string &uri, const std::string &path,
const httplib::Params &params)
{
return sendDeleteRequest(token, uri, path, params);
}
void SetJsonTempVars(const std::string &jsonStr,
std::function<void(const char *, const char *)> setVarFunc)
{

View File

@@ -48,6 +48,9 @@ RequestResult SendPatchRequest(const TwitchToken &token, const std::string &uri,
const httplib::Params &params = {},
const std::string &data = "",
bool useCache = false);
RequestResult SendDeleteRequest(const TwitchToken &token,
const std::string &uri, const std::string &path,
const httplib::Params &params = {});
// Helper functions to set temp var values

View File

@@ -153,6 +153,25 @@ static QStringList getCellLabels(TwitchToken *token, bool addName = true)
return result;
}
static void updateConnectionStatus(QTableWidget *table)
{
for (int row = 0; row < table->rowCount(); row++) {
auto item = table->item(row, 0);
if (!item) {
continue;
}
auto weakToken = GetWeakTwitchTokenByQString(item->text());
auto token = weakToken.lock();
if (!token) {
continue;
}
UpdateItemTableRow(table, row,
getCellLabels(token.get(), false));
}
}
static void openSettingsDialog()
{
auto selectedRows =
@@ -174,6 +193,7 @@ static void openSettingsDialog()
TwitchTokenSettingsDialog::AskForSettings(GetSettingsWindow(),
*token.get());
updateConnectionStatus(tabWidget->Table());
}
static const QStringList headers =
@@ -202,25 +222,6 @@ TwitchConnectionsTable::TwitchConnectionsTable(QTabWidget *parent)
SetHelpVisible(GetTwitchTokens().empty());
}
static void updateConnectionStatus(QTableWidget *table)
{
for (int row = 0; row < table->rowCount(); row++) {
auto item = table->item(row, 0);
if (!item) {
continue;
}
auto weakToken = GetWeakTwitchTokenByQString(item->text());
auto token = weakToken.lock();
if (!token) {
continue;
}
UpdateItemTableRow(table, row,
getCellLabels(token.get(), false));
}
}
static QStringList getInvalidTokens()
{
QStringList tokens;

View File

@@ -283,10 +283,18 @@ bool MacroConditionVideo::ScreenshotContainsPattern()
SetTempVarValue("patternCount", "0");
return false;
}
const auto count = countNonZero(result);
SetTempVarValue("similarity", std::to_string(bestMatchValue));
SetTempVarValue("patternCount", std::to_string(count));
return count > 0;
if (IsTempVarInUse("patternCount")) {
const auto count = CountPatternMatches(
result, {_patternImageData.rgbaPattern.cols,
_patternImageData.rgbaPattern.rows});
SetTempVarValue("patternCount", std::to_string(count));
return count > 0;
}
return countNonZero(result) > 0;
}
bool MacroConditionVideo::FileInputIsUpToDate() const
@@ -369,13 +377,19 @@ bool MacroConditionVideo::CheckColor()
_screenshotData.GetImage(), _colorParameters.color,
_colorParameters.colorThreshold,
_colorParameters.matchThreshold);
// Way too slow for now
//SetTempVarValue("dominantColor", GetDominantColor(_screenshotData.image, 3)
// .name(QColor::HexArgb)
// .toStdString());
SetTempVarValue("color", GetAverageColor(_screenshotData.GetImage())
.name(QColor::HexArgb)
.toStdString());
SetTempVarValue("color", [&]() {
return GetAverageColor(_screenshotData.GetImage())
.name(QColor::HexArgb)
.toStdString();
});
SetTempVarValue("dominantColor", [&]() {
return GetDominantColor(_screenshotData.GetImage(), 3)
.name(QColor::HexArgb)
.toStdString();
});
return ret;
}
@@ -471,6 +485,12 @@ void MacroConditionVideo::SetupTempVars()
obs_module_text("AdvSceneSwitcher.tempVar.video.color"),
obs_module_text(
"AdvSceneSwitcher.tempVar.video.color.description"));
AddTempvar(
"dominantColor",
obs_module_text(
"AdvSceneSwitcher.tempVar.video.dominantColor"),
obs_module_text(
"AdvSceneSwitcher.tempVar.video.dominantColor.description"));
break;
case VideoCondition::MATCH:
case VideoCondition::DIFFER:

View File

@@ -97,6 +97,35 @@ double MatchPattern(QImage &img, const PatternImageData &patternData,
return bestFitValue;
}
int CountPatternMatches(const cv::Mat &result, const cv::Size &patternSize)
{
if (result.empty()) {
return 0;
}
cv::Mat work = result.clone();
int count = 0;
while (true) {
double maxVal;
cv::Point maxLoc;
cv::minMaxLoc(work, nullptr, &maxVal, nullptr, &maxLoc);
if (maxVal <= 0.0) {
break;
}
count++;
// Suppress the template-sized region around this match so
// overlapping high-scoring positions are not counted separately
int x = std::max(0, maxLoc.x - patternSize.width / 2);
int y = std::max(0, maxLoc.y - patternSize.height / 2);
int w = std::min(patternSize.width, work.cols - x);
int h = std::min(patternSize.height, work.rows - y);
work(cv::Rect(x, y, w, h)) = 0.0f;
}
return count;
}
double MatchPattern(QImage &img, QImage &pattern, double threshold,
cv::Mat &result, bool useAlphaAsMask,
cv::TemplateMatchModes matchColor)
@@ -257,9 +286,9 @@ QColor GetAverageColor(const QImage &img)
auto image = QImageToMat(img);
cv::Scalar meanColor = cv::mean(image);
int averageBlue = cvRound(meanColor[0]);
int averageRed = cvRound(meanColor[0]);
int averageGreen = cvRound(meanColor[1]);
int averageRed = cvRound(meanColor[2]);
int averageBlue = cvRound(meanColor[2]);
return QColor(averageRed, averageGreen, averageBlue);
}
@@ -280,8 +309,8 @@ QColor GetDominantColor(const QImage &img, int k)
cv::TermCriteria criteria(
cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 100, 0.2);
cv::Mat labels, centers;
cv::kmeans(reshapedImage, k, labels, criteria, 1,
cv::KMEANS_RANDOM_CENTERS, centers);
cv::kmeans(reshapedImage, k, labels, criteria, 3, cv::KMEANS_PP_CENTERS,
centers);
// Find the dominant color
// Center of the cluster with the largest number of pixels
@@ -292,16 +321,16 @@ QColor GetDominantColor(const QImage &img, int k)
cv::Point max_loc;
cv::minMaxLoc(counts, nullptr, nullptr, nullptr, &max_loc);
try {
cv::Scalar dominantColor = centers.at<cv::Scalar>(max_loc.y);
const int blue = cv::saturate_cast<int>(dominantColor.val[0]);
const int green = cv::saturate_cast<int>(dominantColor.val[1]);
const int red = cv::saturate_cast<int>(dominantColor.val[2]);
const int alpha = cv::saturate_cast<int>(dominantColor.val[3]);
return QColor(red, green, blue, alpha);
} catch (...) {
}
return QColor();
const int clusterIdx = max_loc.x;
const int red =
cv::saturate_cast<int>(centers.at<float>(clusterIdx, 0));
const int green =
cv::saturate_cast<int>(centers.at<float>(clusterIdx, 1));
const int blue =
cv::saturate_cast<int>(centers.at<float>(clusterIdx, 2));
const int alpha =
cv::saturate_cast<int>(centers.at<float>(clusterIdx, 3));
return QColor(red, green, blue, alpha);
}
// Assumption is that QImage uses Format_RGBA8888.

Some files were not shown because too many files have changed in this diff Show More