UE4: Controlling Spotify in-game

One thing our users have told us about Beach Ball Valley is that it's really fun to play with music. We tried it, and it is a lot of fun – the problem is, when you're actually in-game, you have no control over your music. To make the music experience any good, we had to let you control your music without taking the headset off, and ideally without switching to SteamVR's desktop view.

We considered coming up with our own media-player code, and scanning the user's My Music directory for songs to play (AudioShield-style), but that sounded like a mess, and frankly, it's hard to have fine-grained control over file selection when you have giant pizza paddles for hands.

So we came up with a better solution: controlling the user's favorite media player from in-game.

Simulating Windows media keys

Lots of keyboards have special keys on the top row that let the user play and pause their music, skip forward or backward in their playlists, and so on. Turns out, those keys are well-defined in the Windows APIs and we can simulate pressing them with a little bit of C++.

Here is the most basic example of simulating a media key press, specifically the play/pause key:

#include "Windows.h"

void SimulatePlayPauseKeyPress()
{
    INPUT ip;
    ip.type = INPUT_KEYBOARD;
    ip.ki.wVk = VK_MEDIA_PLAY_PAUSE;
    ip.ki.wScan = 0;
    ip.ki.dwFlags = 0;
    ip.ki.time = 0;
    ip.ki.dwExtraInfo = 0;
    SendInput(1, &ip, sizeof(INPUT));

    ip.ki.dwFlags = KEYEVENTF_KEYUP;
    SendInput(1, &ip, sizeof(INPUT));
}

At the core is the Windows SendInput function, which simulates user input. We pass it an INPUT struct to define the input event we are simulating. The INPUT struct itself is mostly composed of a KEYBDINPUT struct, which defines a keyboard input specifically. (By the way, if the structure of an INPUT struct is confusing to you, you should go learn what unions are.)

You can find the full documentation for each parameter in the MSDN documentation, but the important parameters for the INPUT are:

  • type = INPUT_KEYBOARD, which specifies that we are simulating a key press.
  • ki.wVk, which determines the actual key to press. The value for this field is a virtual-key code.
  • ki.dwFlags, which determines the key action. 0 means key press, KEYEVENTF_KEYUP means key up.

The rest of the parameters are simply appropriate default values for our situation.

We call SendInput twice because we have to send both key press and key up events. Actually calling SendInput looks a little goofy, because in each case we have to provide:

  • The number of input structs we are sending along (1 in our case.)
  • A reference to an array of input structs (or a single one, in our case.)
  • The size of an INPUT struct, for some reason (this makes no sense to me, but hey.)

And that's it! Not a lot of code and the end result is quite nice.

All the code

Here's a fuller example of how this can be integrated into an Unreal Engine 4 project. This includes some setup to expose this functionality to Blueprints. Also note the use of #if PLATFORM_WINDOWS to make sure we don't get build errors on other platforms.

MediaFunctionLibrary.h

#pragma once

#include "Kismet/BlueprintFunctionLibrary.h"
#include "MediaFunctionLibrary.generated.h"

UENUM(BlueprintType)
enum class EMediaKey : uint8
{
    VE_PlayPause        UMETA(DisplayName = "Play/Pause"),
    VE_NextTrack        UMETA(DisplayName = "Next Track"),
    VE_PreviousTrack    UMETA(DisplayName = "Previous Track"),
    VE_Stop             UMETA(DisplayName = "Stop")
};

UCLASS()
class MYPROJECT_API UMediaFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
    
public:

    UFUNCTION(BlueprintCallable, Category = "Utilities|Media")
    static void SimulateMediaKeyPress(EMediaKey key);
    
};

MediaFunctionLibrary.cpp

#include "MyProject.h"

#if PLATFORM_WINDOWS
#include "Windows.h"
#endif

#include "MediaFunctionLibrary.h"

void UMediaFunctionLibrary::SimulateMediaKeyPress(EMediaKey key)
{
#if PLATFORM_WINDOWS
    WORD keyCode = 0;
    switch (key) {
        case EMediaKey::VE_PlayPause: {
            keyCode = VK_MEDIA_PLAY_PAUSE;
        } break;
        case EMediaKey::VE_NextTrack: {
            keyCode = VK_MEDIA_NEXT_TRACK;
        } break;
        case EMediaKey::VE_PreviousTrack: {
            keyCode = VK_MEDIA_PREV_TRACK;
        } break;
        case EMediaKey::VE_Stop: {
            keyCode = VK_MEDIA_STOP;
        } break;
    }

    INPUT ip;
    ip.type = INPUT_KEYBOARD;
    ip.ki.wVk = keyCode;
    ip.ki.wScan = 0;
    ip.ki.dwFlags = 0; // 0 for key press
    ip.ki.time = 0;
    ip.ki.dwExtraInfo = 0;
    SendInput(1, &ip, sizeof(INPUT));

    ip.ki.dwFlags = KEYEVENTF_KEYUP;
    SendInput(1, &ip, sizeof(INPUT));
#endif
}