Daniel Marschall
Daniel Marschall

Reputation: 3879

How to stop the systems music? (VK_MEDIA_STOP not working everywhere)

I am developing an application which runs in the background, and when a specific event is happening, the music which is played by media players should be stopped.

The major music players like Spotify and Windows Media Player support being controlled via special keyboard keys "Stop", "Start", "Next track" etc.

So I would like to use this feature to control all running media players and tell them to stop playing.

Media players which do not implement these keys won't be targeted by my application; they will be incompatible.

Currently, I have following function, which works so far:

procedure StopMusic;
const
  KEYEVENTF_KEYDOWN = 0;
  KEYEVENTF_KEYUP = 2;
begin
  keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYDOWN, 0);
  keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYUP, 0);
end;

I don't know exactly how this mechanism works. The operating system messages all applications, no matter in which application I am currently working in, but only if keybd_event is called. (SendMessage WM_KEYDOWN won't work)

The problem I am having is that some applications like OwnCloud swallow these keys if their window is focused (also if pressed on a real keyboard). I will contact them to report this bug. But in the meantime I want to try to make a workaround for it.

My idea for a workaround is to quickly switch the focus to a different window, then perform the virtual key press, then switch back to the window which was active:

procedure StopMusic;
const
  KEYEVENTF_KEYDOWN = 0;
  KEYEVENTF_KEYUP = 2;
var
  hBak: HWND;
begin
  hBak := GetForegroundWindow();
  try
    SetForegroundWindow(GetDesktopWindow());
    keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYDOWN, 0);
    keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYUP, 0);
  finally
    SetForegroundWindow(hBak);
  end;
end;

But this function does not work at all. It has no effect, no matter which Window is currently focused. I guess it is because the Desktop can't gain the focus.

Therefore my question is:

UPDATE: Solution 1 (Dirty)

The first workaround is to switch the focus to the taskbar. But obviously, this is a dirty solution, since it is an implementation detail of Windows.

procedure StopMusic;
const
  KEYEVENTF_KEYDOWN = 0;
  KEYEVENTF_KEYUP = 2;
var
  hBak: HWND;
begin
  hBak := GetForegroundWindow();
  try
    SetForegroundWindow(FindWindow('Shell_TrayWnd', nil));
    keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYDOWN, 0);
    keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYUP, 0);
  finally
    SetForegroundWindow(hBak);
  end;
end;

UPDATE: Solution 2 (Dirty)

This solution also works. It is not a hardcoded string like Shell_TrayWnd, but still using an undocumented API call.

function TaskmanWindow: HWND;
type
  TGetTaskmanWindow = function(): HWND; stdcall;
var
  hUser32: THandle;
  GetTaskmanWindow: TGetTaskmanWindow;
begin
  Result := 0;
  hUser32 := GetModuleHandle('user32.dll');
  if (hUser32 > 0) then
  begin
    @GetTaskmanWindow := GetProcAddress(hUser32, 'GetTaskmanWindow'); // Undocumented
    if Assigned(GetTaskmanWindow) then
    begin
      Result := GetTaskmanWindow;
    end;
  end;
end;

procedure StopMusic;
const
  KEYEVENTF_KEYDOWN = 0;
  KEYEVENTF_KEYUP = 2;
var
  hBak: HWND;
begin
  hBak := GetForegroundWindow();
  try
    SetForegroundWindow(TaskmanWindow);
    keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYDOWN, 0);
    keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYUP, 0);
  finally
    SetForegroundWindow(hBak);
  end;
end;

UPDATE: Solution 3 (Good)

The clean solution of the problem is to send an AppCommand to all applications

procedure StopMusic;
begin
  SendMessage(HWND_BROADCAST, WM_APPCOMMAND, 0, MAKELONG(0, APPCOMMAND_MEDIA_STOP));
end;

UPDATE: Bugreport

A friend with an github account posted the bugreport for me.

Upvotes: 2

Views: 406

Answers (1)

Sertac Akyuz
Sertac Akyuz

Reputation: 54792

...
keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYDOWN, 0);
keybd_event(VK_MEDIA_STOP, 0, KEYEVENTF_KEYUP, 0);
... 

I don't know exactly how this mechanism works...


This code simulates pressing the VK_MEDIA_STOP key which generates a WM_APPCOMMAND message with the APPCOMMAND_MEDIA_STOP application command. It's possible to learn what happens then from the message's documentation:

If a child window does not process this message and instead calls DefWindowProc, DefWindowProc will send the message to its parent window. If a top level window does not process this message and instead calls DefWindowProc, DefWindowProc will call a shell hook with the hook code equal to HSHELL_APPCOMMAND.

So, when a window of an application is in the foreground when you press the key, or simulate the input, if the focused control or its parent chain does not halt the processing of the message and the top level window calls the default window procedure, then a shell event is generated. The well behaving media players you mention are the ones that have registered to receive shell hook messages. This mechanism is also explained in this MSDN blog post.

What OwnCloud does is to, obviously, not calling the default window procedure after it receives a WM_APPCOMMAND. When it is in the foreground this happens to cause your problem; other media applications cannot respond to the message since no shell event is generated.

Hence, your workaround of trying to activate a non-interfering window and generating keyboard input to that window. This is pointless, your own application's window is as good as the taskbar's or any other application's window as long as you don't do anything with the message. When your application's window is in the foreground and you generate input, if Windows Media Player is not responding, there is something else and we'd need a reproduction case for that.

In any case, generating a shell event will not be helpful to OwnCloud or similar applications since they don't have a registered window for shell hook notifications. To these applications, you can send the WM_APPCOMMAND yourself. The linked MSDN blog post has an example of doing that. Although not suggested, you can also broadcast the message if you have a possibility of running an indefinite number of media applications. For a number of considerations why this is not suggested, see this MSDN blog post. If you don't see any problem broadcasting the message, consider using SendMessageTimeout instead of SendMessage.

Upvotes: 2

Related Questions