forked from mpv-player/mpv
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for SystemMediaTransportControls interface. This allows to control mpv from Windows media control ui.
- Loading branch information
Showing
6 changed files
with
381 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.