Managing StreamYard using the Elgato Stream Deck

Introduction

At the start of the global pandemic, it was soon apparent that we would be working and speaking from home for quite some time. As such, I decided early on to do a full upgrade of my workplace at home. One of the improvements I did was to get an Elgato Stream Deck, which allows us to have physical programmable buttons. I use these to switch lights, set scenes, enable and disable audio and video, and more.

One of the streaming platforms which has emerged over the recent months is StreamYard. This platform was a pleasant surprise to me, both as a speaker and as an organizer. It is just straightforward to use as a speaker, follow a link, give access to your microphone and camera, and done. No installation of additional software, no need for specific accounts, it just works. Moreover, as an organizer, it provides me with an effortless experience. It allows us to set custom logos and backgrounds and comes with various layouts where speakers and content can be highlighted in different ways, all for just a small fee.

StreamYard layout

However, there is one downside to StreamYard, which is the lack of support for (global) hotkeys. They are working on adding these; however, for now, the support is not yet there. As a speaker, I use my Stream Deck extensively for global hotkeys, as it allows me, for example, to mute myself quickly. Additionally, as a producer, it would be great if I can rapidly switch between different layouts. This post explains how to set this up by using Tampermonkey combined with AutoHotkey. You can find all of the used resources in the last paragraph.

StreamDeck

Creating hotkeys with Tampermonkey

The first step is to create hotkeys within StreamYard. The ones I wanted to have were to toggle my audio and video and switch between layouts. As StreamYard does not support hotkeys yet, we need to find a way to implement this ourselves. Here is where Tampermonkey comes in, as it allows us to insert user scripts into our web pages and has plugins for all major browsers. After a quick search, I found this script from Justin Garrison, which allows the audio and video toggling. As this script uses the m and v buttons to do the toggling, I started making a couple of changes. Consequently, this ensures I do not trigger the hotkeys by accident, such as when typing in the chat.

if (e.key == "b" && !e.shiftKey && e.ctrlKey && e.altKey && !e.metaKey) {
    var unmuteButton = document.querySelector('[aria-label="Unmute microphone"]');
    var muteButton = document.querySelector('[aria-label="Mute microphone"]');
    if (unmuteButton !== null) {
        unmuteButton.click();
    } else {
        muteButton.click();
    }
} else if (e.key == "v" && !e.shiftKey && e.ctrlKey && e.altKey && !e.metaKey) {
    var faceUnmuteButton = document.querySelector('[aria-label="turn on camera"]');
    var faceMuteButton = document.querySelector('[aria-label="turn off camera"]');
    if (faceUnmuteButton !== null) {
        faceUnmuteButton.click();
    } else {
        faceMuteButton.click();
    }
}

Accordingly, the hotkeys are now set to CTRL+ALT+B and CTRL+ALT+V, respectively, which is already much better. The reason CTRL+ALT+B was used for the audio hotkey is that I found that using CTRL+ALT+M gave some unexpected behavior when combined with AutoHotkey. Now that the initial setup is done, the next step was adding the various layout options’ hotkeys. To do this, I just needed to find the aria labels for the different options, which can be found using the developer tools.

Aria labels in Developer Tools

The aria labels for the different layouts are as following, which are all set up with a hotkey combination of ALT+#.

  1. Solo layout. The host camera fills all the space. If no host camera, the first guest that was added is used.
  2. Thin layout. All cameras are visible and squished to fill up all the space.
  3. Group layout. All cameras are visible and spaced out.
  4. Leader layout. All cameras are visible. One is larger than the others.
  5. Small screen layout. One camera and the shared screen are visible. If no screen, it behaves like the leader layout.
  6. Large screen layout. The shared screen is large, all cameras are visible but small. If no screen, it behaves like the group layout.
  7. Full screen layout. Only the shared screen is visible. If no screen, it behaves like the group layout.
else if (e.key == "1" && !e.shiftKey && !e.ctrlKey && e.altKey && !e.metaKey) {
            var soloLayoutButton = document.querySelector('[aria-label="Solo layout. The host camera fills all the space. If no host camera, the first guest that was added is used."]');
            if (soloLayoutButton !== null) {
                soloLayoutButton.click();
            }
        }

After introducing all the hotkeys I ended up with the script below, which could then be loaded into Tampermonkey.

// ==UserScript==
// @name         Streamyard Keyboard Shortcuts
// @namespace    http://streamyard.com
// @version      1.0
// @description  Keyboard shortcuts for streamyard
// @author       [email protected]
// @match        https://streamyard.com/*
// @grant        none
// @run-at       document-end
// ==/UserScript==
(function () {
    'use strict';
    document.addEventListener('keydown', function (e) {
        if (e.key == "b" && !e.shiftKey && e.ctrlKey && e.altKey && !e.metaKey) {
            var unmuteButton = document.querySelector('[aria-label="Unmute microphone"]');
            var muteButton = document.querySelector('[aria-label="Mute microphone"]');
            if (unmuteButton !== null) {
                unmuteButton.click();
            } else {
                muteButton.click();
            }
        } else if (e.key == "v" && !e.shiftKey && e.ctrlKey && e.altKey && !e.metaKey) {
            var faceUnmuteButton = document.querySelector('[aria-label="turn on camera"]');
            var faceMuteButton = document.querySelector('[aria-label="turn off camera"]');
            if (faceUnmuteButton !== null) {
                faceUnmuteButton.click();
            } else {
                faceMuteButton.click();
            }
        } else if (e.key == "1" && !e.shiftKey && !e.ctrlKey && e.altKey && !e.metaKey) {
            var soloLayoutButton = document.querySelector('[aria-label="Solo layout. The host camera fills all the space. If no host camera, the first guest that was added is used."]');
            if (soloLayoutButton !== null) {
                soloLayoutButton.click();
            }
        } else if (e.key == "2" && !e.shiftKey && !e.ctrlKey && e.altKey && !e.metaKey) {
            var thinLayoutButton = document.querySelector('[aria-label="Thin layout. All cameras are visible and squished to fill up all the space."]');
            if (thinLayoutButton !== null) {
                thinLayoutButton.click();
            }
        } else if (e.key == "3" && !e.shiftKey && !e.ctrlKey && e.altKey && !e.metaKey) {
            var groupLayoutButton = document.querySelector('[aria-label="Group layout. All cameras are visible and spaced out."]');
            if (groupLayoutButton !== null) {
                groupLayoutButton.click();
            }
        } else if (e.key == "4" && !e.shiftKey && !e.ctrlKey && e.altKey && !e.metaKey) {
            var leaderLayoutButton = document.querySelector('[aria-label="Leader layout. All cameras are visible. One is larger than the others."]');
            if (leaderLayoutButton !== null) {
                leaderLayoutButton.click();
            }
        } else if (e.key == "5" && !e.shiftKey && !e.ctrlKey && e.altKey && !e.metaKey) {
            var smallScreenLayoutButton = document.querySelector('[aria-label="Small screen layout. One camera and the shared screen are visible. If no screen, it behaves like the leader layout."]');
            if (smallScreenLayoutButton !== null) {
                smallScreenLayoutButton.click();
            }
        } else if (e.key == "6" && !e.shiftKey && !e.ctrlKey && e.altKey && !e.metaKey) {
            var largeScreenLayoutButton = document.querySelector('[aria-label="Large screen layout. The shared screen is large, all cameras are visible but small. If no screen, it behaves like the group layout."]');
            if (largeScreenLayoutButton !== null) {
                largeScreenLayoutButton.click();
            }
        } else if (e.key == "7" && !e.shiftKey && !e.ctrlKey && e.altKey && !e.metaKey) {
            var fullScreenLayoutButton = document.querySelector('[aria-label="Full screen layout. Only the shared screen is visible. If no screen, it behaves like the group layout."]');
            if (fullScreenLayoutButton !== null) {
                fullScreenLayoutButton.click();
            }
        }
    }, false);
})();

Tampermonkey user scripts

Creating global hotkeys using AutoHotkey

Now that the hotkeys are configured in StreamYard, the next step is to create global hotkeys. A global hotkey allows us to execute our hotkeys, even when the StreamYard window does not have focus. To implement this, I decided to use AutoHotkey, which allows us to register global hotkeys and send commands to specific windows.

First, let us register a global hotkey of SHIFT+CTRL+ALT+B to toggle the mute button. Once the hotkey is triggered, the current window’s ID is retrieved by using the WinGet function of AHK. This ID allows us to come back to the current window after executing the hotkey. Next, use the IfWinExist function to retrieve the StreamYard window. Subsequently, if the window was found, activate the window, and execute the hotkey CTRL+ALT+B, toggling the mute button. Finally, switch back to the initial window.

; StreamYard - Toggle Mute
+^!B::
WinGet, winid, ID, A    ; Save the current window ID
IfWinExist, StreamYard ahk_class Chrome_WidgetWin_1
{
    WinActivate
    Send, ^!b   
    WinActivate ahk_id %winid% ; Restore previous window focus
}
return

To ensure this is always running, I created a script called global-hotkeys.ahk, and placed a shortcut to the script into my startup folder, which can be found by running shell:startup.

Run shell:startup
Windows startup folder

Whenever I use the global hotkey, AutoHotkey captures it, checks for a StreamYard window, and executes the corresponding hotkey in that window. With this in place, it was now easy to add the other global hotkeys to the script, which ended up as below.

SetTitleMatchMode, 2 ; 2 = a partial match on the title

;shift: +
;control: ^
;alt: !

; StreamYard - Toggle Mute
+^!B::
WinGet, winid, ID, A    ; Save the current window ID
IfWinExist, StreamYard ahk_class Chrome_WidgetWin_1
{
    WinActivate
    Send, ^!b   
    WinActivate ahk_id %winid% ; Restore previous window focus
}
return
 
; StreamYard - Toggle Video
+^!V::
WinGet, winid, ID, A    ; Save the current window ID
IfWinExist, StreamYard ahk_class Chrome_WidgetWin_1
{
    WinActivate
    Send, ^!v   
    WinActivate ahk_id %winid% ; Restore previous window focus
}
return
 
; StreamYard - Set scene 1
+^!1::
WinGet, winid, ID, A    ; Save the current window ID
IfWinExist, StreamYard ahk_class Chrome_WidgetWin_1
{
    WinActivate
    Send, !1   
    WinActivate ahk_id %winid% ; Restore previous window focus
}
return
 
; StreamYard - Set scene 2
+^!2::
WinGet, winid, ID, A    ; Save the current window ID
IfWinExist, StreamYard ahk_class Chrome_WidgetWin_1
{
    WinActivate
    Send, !2   
    WinActivate ahk_id %winid% ; Restore previous window focus
}
return
 
; StreamYard - Set scene 3
+^!3::
WinGet, winid, ID, A    ; Save the current window ID
IfWinExist, StreamYard ahk_class Chrome_WidgetWin_1
{
    WinActivate
    Send, !3   
    WinActivate ahk_id %winid% ; Restore previous window focus
}
return
 
; StreamYard - Set scene 4
+^!4::
WinGet, winid, ID, A    ; Save the current window ID
IfWinExist, StreamYard ahk_class Chrome_WidgetWin_1
{
    WinActivate
    Send, !4   
    WinActivate ahk_id %winid% ; Restore previous window focus
}
return
 
; StreamYard - Set scene 5
+^!5::
WinGet, winid, ID, A    ; Save the current window ID
IfWinExist, StreamYard ahk_class Chrome_WidgetWin_1
{
    WinActivate
    Send, !5   
    WinActivate ahk_id %winid% ; Restore previous window focus
}
return
 
; StreamYard - Set scene 6
+^!6::
WinGet, winid, ID, A    ; Save the current window ID
IfWinExist, StreamYard ahk_class Chrome_WidgetWin_1
{
    WinActivate
    Send, !6   
    WinActivate ahk_id %winid% ; Restore previous window focus
}
return
 
; StreamYard - Set scene 7
+^!7::
WinGet, winid, ID, A    ; Save the current window ID
IfWinExist, StreamYard ahk_class Chrome_WidgetWin_1
{
    WinActivate
    Send, !7   
    WinActivate ahk_id %winid% ; Restore previous window focus
}
return

Stream Deck configuration

The last step was to configure my Stream Deck to execute the various global hotkeys. I used the Hotkey action for switching the profiles and configured these to send the global hotkeys we registered in AHK.

Elgato Hotkey action for StreamYard layout

For toggling the audio and video, I decided to use the Hotkey Switch action, as this allows to send different commands and have different icons when pressing the button. I used the same global hotkeys for both; however, I did use two different icons. This way, I can immediately see on my Stream Deck if I am muted and if the webcam is turned on.

Elgato Hotkey Switch action for StreamYard mute
Elgato Hotkey Switch action for StreamYard unmute

Resources

I hope this post helped you set up your StreamYard global hotkeys and maybe also gave inspiration for additional possibilities. I have made all the resources, such as the various scripts and images available on my GitHub. Feel free to use these to get started quickly.

Create and retrieve Azure Functions function keys in ARM template

One of my favorite services in Azure is Functions, which allow you to create serverless micro-services. Triggered by events, after which they run their code, Functions are perfect for the event-driven architectures we strive for these days. These events can come from various sources, like when a message is available in Service Bus, timers, an event sent from Event Grid, etc. However, the one we still use a lot is an HTTP trigger, where we expose the Function as a REST endpoint, available for consumers to call into.

Often we will have an architectural guideline, that every REST endpoint needs to be exposed through Azure API Management. Therefor, we expose these HTTP triggered Functions via API Management as well. We also have guidelines that everything is deployed as Infrastructure as Code, so we do this through ARM templates. In this post we dive into the security side of this, and how to set this up in ARM.

Continue reading

Implementing smart caching of secrets in Azure API Management policies

In a previous blog post, we have seen how to retrieve secrets from Azure Key Vault from an API Management policy. This works great, however, we might start to run into throttling due to the limitations which Key Vault imposes.

This might be due to having an API exposed which we need to call frequently, or because we retrieve secrets from Key Vault in multiple implementations, all of which adds to the restrictions. Luckily, API Management has another policy expression which helps us out here, namely the caching policy.

Continue reading

Retrieve Azure Key Vault secrets from API Management policies

When working with Azure API Management, often we need to include secrets in our policies. For example, we may need to send a password in our authentication header, or to validate a key in a JWT token. There are several options to store these secrets. We could hardcode them into our policy, however this would mean anyone with access to our API Management instance could read them. An not just them, but also everyone who can look into our source control. because we deploy our policies as Infrastructure as Code.

The second option is to place the secret in a named value. This even provides us with the option to set the value as a secret, meaning it will not show the actual value in the overview.

Image result for azure api management named values secret"

However, anyone with access to API Management can still come into the instance, and untick the secret option, and grab the secret. Consequently, this is still not a good option, as we want the management of our secrets to be separate from our API Management administration. Therefor, we will instead store the secret in Azure Key Vault, and retrieve it in our policy.

Continue reading

Calling a versioned API in API Management from Logic Apps

We use Azure API Management quite extensively at our clients, where we use this service whenever our services (APIs) go across application boundaries. Basically, API Management implements a facade in front of all our services. Any consumer, whether they are internal or external, uses this gateway to communicate with our services.

Image result for api management azure
Continue reading

Retrieve Azure Key Vault secrets from Logic Apps using Managed Identity

When working with Azure, we should always put our secrets into a secure store, such as Key Vault. This ensures that we can limit who can see the values of our secrets, while still being able to work with them. How we work with these secrets is different over the various services, and in this article we will focus on Logic Apps, while other services will be explained in their own posts later on.

In Logic Apps we often will need some sort of secret, for example a subscription key for API Management, or a SAS key for Event Grid. In the scenario for this blog post we are going to send our secret to a RequestBin endpoint, so we can see that we indeed get the correct value.

Continue reading

Reference Key Vault secret latest version

NOTE: The explanation below is not officially supported by Microsoft! That said, we have been using this ourselves on several projects, and have seen no issues.

We use Key Vault extensively in our solutions, to store any secrets we might need. For example in an API through code, in Azure Functions via the application settings, or in a Logic App through a REST call. If you go to your secrets in Key Vault, you will notice that the link to the secret includes a version number, in the format of https://kv-we-retrieve-kv-secret.vault.azure.net/secrets/MySecretValue/80df3e46ffcd4f1cb187f79905e9a1e8.

Of course, this is great if we want to reference a specific version of a secret. However, often we will just want to reference the latest version, so we stay up to date even when the secret has been changed, for example because it is a rotating password.

It turns out, this is very easy, without the need to update the version number in all our applications whenever a new version is created. This is done by just omitting the version number from our link! So the will instead look like https://kv-we-retrieve-kv-secret.vault.azure.net/secrets/MySecretValue/.

Important to notice is the trailing slash ( / ), which needs to be included, otherwise you will just get a 404 error.

Retrieve Azure Storage access keys in ARM template

Being a big fan of the Infrastructure as Code (IaC) paradigm, I create all my Azure resources as Azure Resource Manager (ARM) templates. Using an IaC approach, our Azure environments are described as code, allowing us to deploy services in an automated and consistent manner. Often different resources reference other resources, which can introduce the need for authentication. Sometimes we can use Azure Active Directory identities for this (explained more detailed in a later post), however at other times this may require some other form of secrets to set up communication.

Of course, we could create a resource, get the secrets out manually, and then pass them into the referencing resource’s template. However, the idea of Infrastructure as Code, especially when combined with a CI/CD strategy, is to have a minimal amount of manual steps. So instead, let’s have a look at how we can retrieve these secrets from our ARM templates. Consequently, this allows us to set these secrets at deployment time, without any manual interference needed. This article is the first in a series of blog posts, each focusing on a different service, starting with Azure Storage access keys in this post.

Azure Storage Connection String
The connection string which we are going to retrieve
Continue reading

API Management CI/CD using ARM Templates – Linked template

This is the fifth and final post in my series around setting up CI/CD for Azure API Management using Azure Resource Manager templates. We already created our API Management instance, added products, users and groups to the instance, and created unversioned and versioned APIs. In this final post, we will see how we can use linked ARM templates in combination with VSTS to deploy our solution all at once, and how this allows us to re-use existing templates to build up our API Management.

image

The posts in this series are the following, this list will be updated as the posts are being published.

Continue reading

API Management CI/CD using ARM Templates – Versioned API

This is the fourth post in my series around setting up CI/CD for Azure API Management using Azure Resource Manager templates. So far we have created our API Management instance, added the products, users and groups for Contoso, and created an unversioned API. In this post we will create an versioned API, allowing us to run multiple versions of an API side by side.

image

The posts in this series are the following, this list will be updated as the posts are being published.

Continue reading