From 4f96afbc818b8327dc29abccd6aa0fbe3395b6da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 10 Jun 2024 20:41:31 +0200 Subject: [PATCH] win32: add Media Control support Add support for SystemMediaTransportControls interface. This allows to control mpv from Windows media control ui. --- meson.build | 8 +- meson_options.txt | 3 + osdep/win32/meson.build | 9 + osdep/win32/smtc.cc | 389 ++++++++++++++++++++++++++++++++++++++++ osdep/win32/smtc.h | 31 ++++ player/main.c | 5 + 6 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 osdep/win32/meson.build create mode 100644 osdep/win32/smtc.cc create mode 100644 osdep/win32/smtc.h diff --git a/meson.build b/meson.build index 0e3eeeeb2f644..9bbf1273897d5 100644 --- a/meson.build +++ b/meson.build @@ -8,6 +8,8 @@ project('mpv', 'buildtype=debugoptimized', 'b_lundef=false', 'c_std=c11', + 'cpp_std=c++20', + 'cpp_eh=default', 'warning_level=2', ] ) @@ -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']) @@ -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 ')} features += {'glob-win32': win32 and not features['glob-posix']} diff --git a/meson_options.txt b/meson_options.txt index 788231ad60b3f..8b0e04d171338 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -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') diff --git a/osdep/win32/meson.build b/osdep/win32/meson.build new file mode 100644 index 0000000000000..9d308f6533045 --- /dev/null +++ b/osdep/win32/meson.build @@ -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 diff --git a/osdep/win32/smtc.cc b/osdep/win32/smtc.cc new file mode 100644 index 0000000000000..2055a020c193a --- /dev/null +++ b/osdep/win32/smtc.cc @@ -0,0 +1,389 @@ +/* + * 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 . + */ + +#include "smtc.h" + +#include +#include + +#include +#include +#include +#include + +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(&__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; + +struct smtc_ctx { + mp_log *log; + mpv_handle *mpv; + std::atomic_bool close = false; + std::atomic hwnd = nullptr; +}; + +static void update_state(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); + smtc.IsStopEnabled(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); + smtc.IsRewindEnabled(true); + } else { + smtc.PlaybackStatus(MediaPlaybackStatus::Closed); + smtc.IsPlayEnabled(false); + smtc.IsPauseEnabled(false); + smtc.IsStopEnabled(false); + smtc.IsNextEnabled(false); + smtc.IsPreviousEnabled(false); + smtc.IsRewindEnabled(false); + } + + int shuffle = false; + mpv_get_property(&mpv, "shuffle", MPV_FORMAT_FLAG, &shuffle); + smtc.ShuffleEnabled(shuffle); + + double speed = 0; + mpv_get_property(&mpv, "speed", MPV_FORMAT_DOUBLE, &speed); + smtc.PlaybackRate(speed); + + mp_string loop_file_opt{ mpv_get_property_string(&mpv, "loop-file") }; + bool loop_file = loop_file_opt && strcmp(loop_file_opt.get(), "no"); + mp_string loop_playlist_opt{ mpv_get_property_string(&mpv, "loop-playlist") }; + bool loop_playlist = loop_playlist_opt && strcmp(loop_playlist_opt.get(), "no"); + if (loop_file) { + smtc.AutoRepeatMode(MediaPlaybackAutoRepeatMode::Track); + } else if (loop_playlist) { + smtc.AutoRepeatMode(MediaPlaybackAutoRepeatMode::List); + } else { + smtc.AutoRepeatMode(MediaPlaybackAutoRepeatMode::None); + } + + 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(std::chrono::duration(pos))); + tl.MaxSeekTime(std::chrono::duration_cast(std::chrono::duration(duration))); + tl.EndTime(std::chrono::duration_cast(std::chrono::duration(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(user_data); + auto &mpv = *reinterpret_cast(lParam); + auto &event = *reinterpret_cast(wParam); + + update_state(smtc, mpv); + if (event.event_id == MPV_EVENT_PROPERTY_CHANGE) { + auto &prop = *static_cast(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(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(); + HRESULT hr = interop->GetForWindow(ctx.hwnd, + winrt::guid_of(), + 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 &, + 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; + } + }); + smtc.PlaybackPositionChangeRequested([&](const SystemMediaTransportControls &, + const PlaybackPositionChangeRequestedEventArgs &args) { + auto position = args.RequestedPlaybackPosition(); + auto pos = std::chrono::duration_cast>(position).count(); + mpv_command(ctx.mpv, (const char*[]){"set", "time-pos", std::to_string(pos).c_str(), NULL}); + }); + smtc.PlaybackRateChangeRequested([&](const SystemMediaTransportControls &, + const PlaybackRateChangeRequestedEventArgs &args) { + auto rate = args.RequestedPlaybackRate(); + mpv_command(ctx.mpv, (const char*[]){"set", "speed", std::to_string(rate).c_str(), NULL}); + }); + smtc.ShuffleEnabledChangeRequested([&](const SystemMediaTransportControls &, + const ShuffleEnabledChangeRequestedEventArgs &args) { + auto shuffle = args.RequestedShuffleEnabled(); + mpv_command(ctx.mpv, (const char*[]){"set", "shuffle", shuffle ? "yes" : "no", NULL}); + }); + smtc.AutoRepeatModeChangeRequested([&](const SystemMediaTransportControls &, + const AutoRepeatModeChangeRequestedEventArgs &args) { + bool loop_file = false, loop_playlist = false; + switch (args.RequestedAutoRepeatMode()) { + case MediaPlaybackAutoRepeatMode::Track: + loop_file = true; + break; + case MediaPlaybackAutoRepeatMode::List: + loop_playlist = true; + break; + case MediaPlaybackAutoRepeatMode::None: + break; + } + mpv_command(ctx.mpv, (const char*[]){"set", "loop-file", + loop_file ? "yes" : "no", NULL}); + mpv_command(ctx.mpv, (const char*[]){"set", "loop-playlist", + loop_playlist ? "yes" : "no", NULL}); + }); + + 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(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, "shuffle", MPV_FORMAT_DOUBLE); + mpv_observe_property(mpv, 0, "speed", MPV_FORMAT_DOUBLE); + mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE); + // TODO: Options are not observable, fix me! + mpv_observe_property(mpv, 0, "loop-file", MPV_FORMAT_DOUBLE); + mpv_observe_property(mpv, 0, "loop-playlist", 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(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(event), + reinterpret_cast(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); +} diff --git a/osdep/win32/smtc.h b/osdep/win32/smtc.h new file mode 100644 index 0000000000000..5e6c0f2a986a8 --- /dev/null +++ b/osdep/win32/smtc.h @@ -0,0 +1,31 @@ +/* + * 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 . + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct mpv_handle mpv_handle; +void mp_smtc_init(mpv_handle *client); + +#ifdef __cplusplus +} +#endif diff --git a/player/main.c b/player/main.c index db3beb554ea98..cfd8b902f068d 100644 --- a/player/main.c +++ b/player/main.c @@ -36,6 +36,7 @@ #include "osdep/threads.h" #include "osdep/timer.h" #include "osdep/main-fn.h" +#include "osdep/win32/smtc.h" #include "common/av_log.h" #include "common/codecs.h" @@ -398,6 +399,10 @@ int mp_initialize(struct MPContext *mpctx, char **options) cocoa_set_mpv_handle(ctx); #endif +#if defined(HAVE_SMTC) && HAVE_SMTC + mp_smtc_init(mp_new_client(mpctx->clients, "SystemMediaTransportControls")); +#endif + if (opts->encode_opts->file && opts->encode_opts->file[0]) { mpctx->encode_lavc_ctx = encode_lavc_init(mpctx->global); if(!mpctx->encode_lavc_ctx) {