Skip to content

Commit

Permalink
win32: add Media Control support
Browse files Browse the repository at this point in the history
Add support for SystemMediaTransportControls interface. This allows to
control mpv from Windows media control ui.
  • Loading branch information
kasper93 committed Jun 10, 2024
1 parent 9b0e3b3 commit 3d4dc8e
Show file tree
Hide file tree
Showing 6 changed files with 381 additions and 1 deletion.
8 changes: 7 additions & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ project('mpv',
'buildtype=debugoptimized',
'b_lundef=false',
'c_std=c11',
'cpp_std=c++20',
'cpp_eh=default',
'warning_level=2',
]
)
Expand Down Expand Up @@ -383,7 +385,7 @@ if get_option('fuzzers')
link_flags += ['-fsanitize=address,undefined,fuzzer', '-fno-omit-frame-pointer']
endif

add_project_arguments(flags, language: 'c')
add_project_arguments(flags, language: ['c', 'cpp', 'objc'])
add_project_arguments(['-Wno-unused-parameter'], language: 'objc')
add_project_link_arguments(link_flags, language: ['c', 'cpp', 'objc'])

Expand Down Expand Up @@ -524,6 +526,10 @@ if not posix and not features['win32-desktop']
'osdep/terminal-dummy.c')
endif

if win32
subdir('osdep/win32')
endif

features += {'glob-posix': cc.has_function('glob', prefix: '#include <glob.h>')}

features += {'glob-win32': win32 and not features['glob-posix']}
Expand Down
3 changes: 3 additions & 0 deletions meson_options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ option('macos-touchbar', type: 'feature', value: 'auto', description: 'macOS Tou
option('swift-build', type: 'feature', value: 'auto', description: 'macOS Swift build tools')
option('swift-flags', type: 'string', description: 'Optional Swift compiler flags')

# Windows features
option('win32-smtc', type: 'feature', value: 'auto', description: 'Enable Media Control support')

# manpages
option('html-build', type: 'feature', value: 'disabled', description: 'HTML manual generation')
option('manpage-build', type: 'feature', value: 'auto', description: 'manpage generation')
Expand Down
9 changes: 9 additions & 0 deletions osdep/win32/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
add_languages('cpp')
cpp = meson.get_compiler('cpp')

smtc = cpp.has_header('winrt/base.h', required: get_option('win32-smtc'))
features += {'smtc': smtc}
if features['smtc']
dependencies += cpp.find_library('runtimeobject')
sources += meson.current_source_dir() / 'smtc.cc'
endif
326 changes: 326 additions & 0 deletions osdep/win32/smtc.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
/*
* User language lookup for generic POSIX platforms
*
* This file is part of mpv.
*
* mpv is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* mpv is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with mpv. If not, see <http://www.gnu.org/licenses/>.
*/

#include "smtc.h"

#include <chrono>
#include <format>

#include <Windows.h>
#include <SystemMediaTransportControlsInterop.h>
#include <winrt/windows.foundation.h>
#include <winrt/windows.media.h>

extern "C" {
#include "common/msg.h"
#include "osdep/threads.h"
#include "player/client.h"
}

EXTERN_C IMAGE_DOS_HEADER __ImageBase;
#define HINST_THISCOMPONENT (reinterpret_cast<HINSTANCE>(&__ImageBase))
#define WM_MP_EVENT (WM_USER + 1)

using namespace std::chrono_literals;
using namespace winrt::Windows::Media;
using winrt::Windows::Foundation::TimeSpan;

struct mp_string_deleter {
void operator()(char *ptr) const {
mpv_free(ptr);
}
};
using mp_string = std::unique_ptr<char, mp_string_deleter>;

struct smtc_ctx {
mp_log *log;
mpv_handle *mpv;
std::atomic_bool close = false;
std::atomic<HWND> hwnd = nullptr;
};

static void update_timeline(SystemMediaTransportControls &smtc, mpv_handle &mpv)
{
int closed = 0;
mpv_get_property(&mpv, "idle-active", MPV_FORMAT_FLAG, &closed);
if (!closed) {
int paused = 1;
mpv_get_property(&mpv, "pause", MPV_FORMAT_FLAG, &paused);
smtc.PlaybackStatus(paused ? MediaPlaybackStatus::Paused : MediaPlaybackStatus::Playing);
smtc.IsPlayEnabled(true);
smtc.IsPauseEnabled(true);
int64_t ch_index = -1, ch_count = -1, pl_count = -1;
mpv_get_property(&mpv, "chapter", MPV_FORMAT_INT64, &ch_index);
mpv_get_property(&mpv, "chapter-list/count", MPV_FORMAT_INT64, &ch_count);
mpv_get_property(&mpv, "playlist-count", MPV_FORMAT_INT64, &pl_count);
smtc.IsNextEnabled(pl_count > 1 || ch_index < ch_count - 1);
smtc.IsPreviousEnabled(pl_count > 1 || ch_index > 0);
} else {
smtc.PlaybackStatus(MediaPlaybackStatus::Closed);
smtc.IsPlayEnabled(false);
smtc.IsPauseEnabled(false);
smtc.IsNextEnabled(false);
smtc.IsPreviousEnabled(false);
}

double pos = 0, duration = 0;
mpv_get_property(&mpv, "time-pos", MPV_FORMAT_DOUBLE, &pos);
mpv_get_property(&mpv, "duration", MPV_FORMAT_DOUBLE, &duration);

SystemMediaTransportControlsTimelineProperties tl;
tl.StartTime(0s);
tl.MinSeekTime(0s);
tl.Position(std::chrono::duration_cast<TimeSpan>(std::chrono::duration<double>(pos)));
tl.MaxSeekTime(std::chrono::duration_cast<TimeSpan>(std::chrono::duration<double>(duration)));
tl.EndTime(std::chrono::duration_cast<TimeSpan>(std::chrono::duration<double>(duration)));
smtc.UpdateTimelineProperties(tl);
}

static void update_metadata(SystemMediaTransportControls &smtc, mpv_handle &mpv)
{
auto updater = smtc.DisplayUpdater();
updater.ClearAll();

int image = 0;
bool video = !mpv_get_property(&mpv, "current-tracks/video/image", MPV_FORMAT_FLAG, &image);
int audio = 0;
mpv_get_property(&mpv, "current-tracks/audio/selected", MPV_FORMAT_FLAG, &audio);

mp_string title{ mpv_get_property_osd_string(&mpv, "media-title") };
if (video && !image) {
updater.Type(MediaPlaybackType::Video);
const auto &props = updater.VideoProperties();
if (title)
props.Title(winrt::to_hstring(title.get()));
int64_t ch_index = -1;
mpv_get_property(&mpv, "chapter", MPV_FORMAT_INT64, &ch_index);
if (ch_index >= 0) {
int64_t ch_count = -1;
mpv_get_property(&mpv, "chapter-list/count", MPV_FORMAT_INT64, &ch_count);
mp_string ch_title {
mpv_get_property_string(&mpv, std::format(("chapter-list/{}/title"), ch_index).c_str())
};
if (ch_title)
props.Subtitle(winrt::to_hstring(std::format("{} ({}/{})", ch_title.get(), ch_index + 1, ch_count)));
}
} else if (image && !audio) {
updater.Type(MediaPlaybackType::Image);
const auto &props = updater.ImageProperties();
if (title)
props.Title(winrt::to_hstring(title.get()));
} else {
updater.Type(MediaPlaybackType::Music);
const auto &props = updater.MusicProperties();
if (title)
props.Title(winrt::to_hstring(title.get()));
if (mp_string str{ mpv_get_property_string(&mpv, "metadata/by-key/Album_Artist") })
props.AlbumArtist(winrt::to_hstring(str.get()));
if (mp_string str{ mpv_get_property_string(&mpv, "metadata/by-key/Album") })
props.AlbumTitle(winrt::to_hstring(str.get()));
if (mp_string str{ mpv_get_property_string(&mpv, "metadata/by-key/Album_Track_Count") })
props.AlbumTrackCount(std::atoi(str.get()));
if (mp_string str{ mpv_get_property_string(&mpv, "metadata/by-key/Artist") })
props.Artist(winrt::to_hstring(str.get()));
if (mp_string str{ mpv_get_property_string(&mpv, "metadata/by-key/Track") })
props.TrackNumber(std::atoi(str.get()));
}

updater.Update();
}

static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
LONG_PTR user_data;
if (uMsg == WM_MP_EVENT && (user_data = GetWindowLongPtrW(hWnd, GWLP_USERDATA))) {
auto &smtc = *reinterpret_cast<SystemMediaTransportControls*>(user_data);
auto &mpv = *reinterpret_cast<mpv_handle*>(lParam);
auto &event = *reinterpret_cast<mpv_event*>(wParam);

update_timeline(smtc, mpv);
if (event.event_id == MPV_EVENT_PROPERTY_CHANGE) {
auto &prop = *static_cast<mpv_event_property *>(event.data);
if (!strcmp(prop.name, "time-pos"))
return 0;
}
update_metadata(smtc, mpv);

return 0;
}

if (uMsg == WM_DESTROY)
PostQuitMessage(0);

return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

static MP_THREAD_VOID win_event_loop_fn(void *arg)
{
auto &ctx = *static_cast<smtc_ctx*>(arg);

WNDCLASS wc = {
.lpfnWndProc = WindowProc,
.hInstance = HINST_THISCOMPONENT,
.hIcon = LoadIconW(HINST_THISCOMPONENT, L"IDI_ICON1"),
.lpszClassName = L"mpv-smtc"
};
RegisterClassW(&wc);

try {
ctx.hwnd = CreateWindowExW(0, wc.lpszClassName, L"mpv smtc",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
nullptr, nullptr, wc.hInstance, nullptr);
if (!ctx.hwnd)
winrt::throw_last_error();

SystemMediaTransportControls smtc{ nullptr };
auto interop = winrt::get_activation_factory<SystemMediaTransportControls,
ISystemMediaTransportControlsInterop>();
HRESULT hr = interop->GetForWindow(ctx.hwnd,
winrt::guid_of<SystemMediaTransportControls>(),
winrt::put_abi(smtc));
if (FAILED(hr))
winrt::throw_hresult(hr);
SetWindowLongPtrW(ctx.hwnd, GWLP_USERDATA, (LONG_PTR)&smtc);

smtc.IsEnabled(true);

smtc.ButtonPressed([&](const SystemMediaTransportControls &sender,
const SystemMediaTransportControlsButtonPressedEventArgs &args) {
const auto pause = [&](int pause) {
mpv_set_property(ctx.mpv, "pause", MPV_FORMAT_FLAG, &pause);
};
switch (args.Button()) {
case SystemMediaTransportControlsButton::Play:
pause(false);
break;
case SystemMediaTransportControlsButton::Pause:
pause(true);
break;
case SystemMediaTransportControlsButton::Stop:
mpv_command_string(ctx.mpv, "stop");
break;
case SystemMediaTransportControlsButton::Next: {
int64_t ch_index = -1, ch_count = -1;
mpv_get_property(ctx.mpv, "chapter", MPV_FORMAT_INT64, &ch_index);
mpv_get_property(ctx.mpv, "chapter-list/count", MPV_FORMAT_INT64, &ch_count);
mpv_command_string(ctx.mpv, ch_index < ch_count ? "add chapter 1" : "playlist-next");
break;
}
case SystemMediaTransportControlsButton::Previous: {
int64_t ch_index = -1;
mpv_get_property(ctx.mpv, "chapter", MPV_FORMAT_INT64, &ch_index);
mpv_command_string(ctx.mpv, ch_index > 0 ? "add chapter -1" : "playlist-prev");
break;
}
default:
break;
}
});

MSG msg;
while(BOOL ret = GetMessageW(&msg, nullptr, 0, 0)) {
if (ret == -1)
winrt::throw_last_error();
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
} catch (const winrt::hresult_error& e) {
MP_ERR(&ctx, "%s: %ls\n", __func__, e.message().c_str());
}

ctx.close = true;
mpv_wakeup(ctx.mpv);
HWND hwnd = ctx.hwnd;
ctx.hwnd = nullptr;
DestroyWindow(hwnd);
UnregisterClassW(wc.lpszClassName, HINST_THISCOMPONENT);

MP_THREAD_RETURN();
}

static MP_THREAD_VOID mpv_event_loop_fn(void *arg)
{
auto mpv = static_cast<mpv_handle *>(arg);
int64_t last_pos_update = 0;
smtc_ctx ctx = {
.log = mp_client_get_log(mpv),
.mpv = mpv
};

// Create a dedicated window and event loop. We could use the mpv main window,
// but it is not always available, especially in audio-only/console mode.
mp_thread win_event_loop;
if (mp_thread_create(&win_event_loop, win_event_loop_fn, &ctx)) {
MP_ERR(&ctx, "Failed to create window event thread!\n");
goto error;
}

mpv_observe_property(mpv, 0, "current-tracks", MPV_FORMAT_NODE_MAP);
mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE);
mpv_observe_property(mpv, 0, "idle-active", MPV_FORMAT_FLAG);
mpv_observe_property(mpv, 0, "media-title", MPV_FORMAT_STRING);
mpv_observe_property(mpv, 0, "metadata", MPV_FORMAT_NODE_MAP);
mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_FLAG);
mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE);

while (!ctx.close) {
mpv_event *event = mpv_wait_event(mpv, -1);
if (event->event_id == MPV_EVENT_SHUTDOWN) {
if (ctx.hwnd)
PostMessageW(ctx.hwnd, WM_CLOSE, 0, 0);
break;
}
if (event->event_id == MPV_EVENT_PROPERTY_CHANGE)
{
// It is recommended that you keep the system controls in sync with
// your media playback by updating these properties approximately
// every 5 seconds during playback and again whenever the state of
// playback changes, such as pausing or seeking to a new position.
// https://learn.microsoft.com/windows/uwp/audio-video-camera/system-media-transport-controls
if (!strcmp(static_cast<mpv_event_property *>(event->data)->name, "time-pos")) {
auto now = mp_time_ns();
if (now - last_pos_update < MP_TIME_S_TO_NS(5))
continue;
last_pos_update = now;
}
}
if (event->event_id == MPV_EVENT_PROPERTY_CHANGE ||
event->event_id == MPV_EVENT_PLAYBACK_RESTART)
{
if (ctx.hwnd) {
SendMessageW(ctx.hwnd, WM_MP_EVENT,
reinterpret_cast<WPARAM>(event),
reinterpret_cast<LPARAM>(mpv));
}
}
}
mp_thread_join(win_event_loop);

error:
mpv_destroy(mpv);
MP_THREAD_RETURN();
}

void mp_smtc_init(mpv_handle *mpv)
{
mp_thread mpv_event_loop;
if (!mp_thread_create(&mpv_event_loop, mpv_event_loop_fn, mpv))
mp_thread_detach(mpv_event_loop);
}
Loading

0 comments on commit 3d4dc8e

Please sign in to comment.