History

win32: add Media Control support

Add support for SystemMediaTransportControls interface. This allows to control mpv from Windows media control ui.

Kacper Michajłow committed 2 years ago

Commit 5fed12e1 parent 06da68011 files · +497 −0Browse files

Unified · Split

11 files
  1. DOCS/interface-changes/media-controls.txt +1 −0
  2. DOCS/man/options.rst +7 −0
  3. meson.build +4 −0
  4. meson_options.txt +3 −0
  5. misc/path_utils.c +2 −0
  6. options/options.c +3 −0
  7. options/options.h +1 −0
  8. osdep/win32/meson.build +8 −0
  9. osdep/win32/smtc.cpp +433 −0
  10. osdep/win32/smtc.h +29 −0
  11. player/main.c +6 −0
DOCS/interface-changes/media-controls.txt+1−0
@@ -0,0 +1 @@
1+add `--media-controls` option
DOCS/man/options.rst+7−0
@@ -7420,6 +7420,13 @@ Miscellaneous · 7419 unmodified lines
74207420
74217421 .. warning:: Using realtime priority can cause system lockup.
74227422
7423+``--media-controls=<yes|player|no>``
7424+ (Windows only)
7425+ Enable integration of media control interface SystemMediaTransportControls.
7426+ If set to ``player``, only the player will use the controls. Setting it to
7427+ ``yes`` will also enable the controls for libmpv integrations.
7428+ (default: ``player``)
7429+
74237430 ``--force-media-title=<string>``
74247431 Force the contents of the ``media-title`` property to this value. Useful
74257432 for scripts which want to set a title, without overriding the user's
meson.build+4−0
@@ -530,6 +530,10 @@ if not posix and not features['win32-desktop'] · 529 unmodified lines
530530 'osdep/terminal-dummy.c')
531531 endif
532532
533+if win32
534+ subdir('osdep/win32')
535+endif
536+
533537 features += {'glob-posix': cc.has_function('glob', prefix: '#include <glob.h>')}
534538
535539 features += {'glob-win32': win32 and not features['glob-posix']}
meson_options.txt+3−0
@@ -116,6 +116,9 @@ option('macos-touchbar', type: 'feature', value: 'auto', description: 'macOS Tou · 115 unmodified lines
116116 option('swift-build', type: 'feature', value: 'auto', description: 'macOS Swift build tools')
117117 option('swift-flags', type: 'string', description: 'Optional Swift compiler flags')
118118
119+# Windows features
120+option('win32-smtc', type: 'feature', value: 'auto', description: 'Enable Media Control support')
121+
119122 # manpages
120123 option('html-build', type: 'feature', value: 'disabled', description: 'HTML manual generation')
121124 option('manpage-build', type: 'feature', value: 'auto', description: 'manpage generation')
misc/path_utils.c+2−0
@@ -156,6 +156,8 @@ char *mp_getcwd(void *talloc_ctx) · 155 unmodified lines
156156
157157 char *mp_normalize_path(void *talloc_ctx, const char *path)
158158 {
159+ assert(talloc_ctx && "mp_normalize_path requires talloc_ctx!");
160+
159161 if (!path)
160162 return NULL;
161163
options/options.c+3−0
@@ -543,6 +543,8 @@ static const m_option_t mp_opts[] = { · 542 unmodified lines
543543 {"idle", IDLE_PRIORITY_CLASS}),
544544 .flags = UPDATE_PRIORITY},
545545 #endif
546+ {"media-controls", OPT_CHOICE(media_controls,
547+ {"no", 0}, {"player", 1}, {"yes", 2})},
546548 {"config", OPT_BOOL(load_config), .flags = CONF_PRE_PARSE},
547549 {"config-dir", OPT_STRING(force_configdir),
548550 .flags = CONF_NOCFG | CONF_PRE_PARSE | M_OPT_FILE},
@@ -1039,6 +1041,7 @@ static const struct MPOpts mp_default_opts = { · 490 unmodified lines
10391041 .osd_bar_visible = true,
10401042 .screenshot_template = "mpv-shot%n",
10411043 .play_dir = 1,
1044+ .media_controls = 1,
10421045
10431046 .audiofile_auto_exts = (char *[]){
10441047 "aac",
options/options.h+1−0
@@ -344,6 +344,7 @@ typedef struct MPOpts { · 343 unmodified lines
344344 bool osd_bar_visible;
345345
346346 int w32_priority;
347+ int media_controls;
347348
348349 struct bluray_opts *stream_bluray_opts;
349350 struct cdda_opts *stream_cdda_opts;
osdep/win32/meson.build+8−0
@@ -0,0 +1,8 @@
1+add_languages('cpp', native: false)
2+cpp = meson.get_compiler('cpp')
3+
4+features += {'win32-smtc': cpp.has_header('winrt/base.h', required: get_option('win32-smtc'))}
5+if features['win32-smtc']
6+ dependencies += cpp.find_library('runtimeobject')
7+ sources += meson.current_source_dir() / 'smtc.cpp'
8+endif
osdep/win32/smtc.cpp+433−0
@@ -0,0 +1,433 @@
1+/*
2+ * This file is part of mpv.
3+ *
4+ * mpv is free software; you can redistribute it and/or
5+ * modify it under the terms of the GNU Lesser General Public
6+ * License as published by the Free Software Foundation; either
7+ * version 2.1 of the License, or (at your option) any later version.
8+ *
9+ * mpv is distributed in the hope that it will be useful,
10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+ * GNU Lesser General Public License for more details.
13+ *
14+ * You should have received a copy of the GNU Lesser General Public
15+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
16+ */
17+
18+#include "smtc.h"
19+
20+#include <chrono>
21+#include <format>
22+#include <utility>
23+
24+#include <windows.h>
25+#include <systemmediatransportcontrolsinterop.h>
26+#include <winrt/Windows.Foundation.h>
27+#include <winrt/Windows.Media.h>
28+
29+extern "C" {
30+#include "common/msg.h"
31+#include "osdep/threads.h"
32+#include "player/client.h"
33+}
34+
35+EXTERN_C IMAGE_DOS_HEADER __ImageBase;
36+#define WM_MP_EVENT (WM_USER + 1)
37+
38+using namespace std::chrono_literals;
39+using namespace winrt::Windows::Media;
40+using winrt::Windows::Foundation::TimeSpan;
41+
42+struct mpv_deleter {
43+ void operator()(void *ptr) const {
44+ mpv_free(ptr);
45+ }
46+};
47+using mp_string = std::unique_ptr<char, mpv_deleter>;
48+
49+template<mpv_format F> struct mp_fmt;
50+template<> struct mp_fmt<MPV_FORMAT_FLAG> { using type = int; };
51+template<> struct mp_fmt<MPV_FORMAT_INT64> { using type = int64_t; };
52+template<> struct mp_fmt<MPV_FORMAT_DOUBLE> { using type = double; };
53+
54+template<mpv_format F>
55+static inline std::optional<typename mp_fmt<F>::type>
56+mp_get_property(mpv_handle *mpv, const char *name)
57+{
58+ typename mp_fmt<F>::type val;
59+ if (mpv_get_property(mpv, name, F, &val) != MPV_ERROR_SUCCESS)
60+ return std::nullopt;
61+ return val;
62+}
63+
64+template<typename T> struct mp_fmt_e;
65+template<> struct mp_fmt_e<int> { static constexpr mpv_format value = MPV_FORMAT_FLAG; };
66+template<> struct mp_fmt_e<bool> { static constexpr mpv_format value = MPV_FORMAT_FLAG; };
67+template<> struct mp_fmt_e<int64_t> { static constexpr mpv_format value = MPV_FORMAT_INT64; };
68+template<> struct mp_fmt_e<double> { static constexpr mpv_format value = MPV_FORMAT_DOUBLE; };
69+
70+template<typename T>
71+static inline int mp_set_property(mpv_handle *mpv, const char *name, T &&val)
72+{
73+ using val_t = std::remove_reference_t<T>;
74+ typename mp_fmt<mp_fmt_e<val_t>::value>::type mpv_val = std::forward<T>(val);
75+ return mpv_set_property(mpv, name, mp_fmt_e<val_t>::value, &mpv_val);
76+}
77+
78+struct smtc_ctx {
79+ mp_log *log;
80+ mpv_handle *mpv;
81+ SystemMediaTransportControls smtc{ nullptr };
82+ std::atomic_bool close{ false };
83+ std::atomic<HWND> hwnd{ nullptr };
84+};
85+
86+static void update_state(SystemMediaTransportControls &smtc, mpv_handle *mpv)
87+{
88+ auto closed = mp_get_property<MPV_FORMAT_FLAG>(mpv, "idle-active");
89+ if (!closed.value_or(false)) {
90+ auto paused = mp_get_property<MPV_FORMAT_FLAG>(mpv, "pause");
91+ smtc.PlaybackStatus(paused.value_or(true) ? MediaPlaybackStatus::Paused : MediaPlaybackStatus::Playing);
92+ smtc.IsPlayEnabled(true);
93+ smtc.IsPauseEnabled(true);
94+ smtc.IsStopEnabled(true);
95+ auto ch_index = mp_get_property<MPV_FORMAT_INT64>(mpv, "chapter");
96+ auto ch_count = mp_get_property<MPV_FORMAT_INT64>(mpv, "chapter-list/count");
97+ auto pl_count = mp_get_property<MPV_FORMAT_INT64>(mpv, "playlist-count");
98+ smtc.IsNextEnabled(pl_count > 1 || ch_count > ch_index.value_or(0));
99+ smtc.IsPreviousEnabled(pl_count > 1 || ch_index > 0);
100+ smtc.IsRewindEnabled(true);
101+ } else {
102+ smtc.PlaybackStatus(MediaPlaybackStatus::Closed);
103+ smtc.IsPlayEnabled(false);
104+ smtc.IsPauseEnabled(false);
105+ smtc.IsStopEnabled(false);
106+ smtc.IsNextEnabled(false);
107+ smtc.IsPreviousEnabled(false);
108+ smtc.IsRewindEnabled(false);
109+ }
110+
111+ auto shuffle = mp_get_property<MPV_FORMAT_FLAG>(mpv, "shuffle");
112+ smtc.ShuffleEnabled(shuffle.value_or(false));
113+
114+ auto speed = mp_get_property<MPV_FORMAT_DOUBLE>(mpv, "speed");
115+ smtc.PlaybackRate(speed.value_or(1.0));
116+
117+ mp_string loop_file_opt{ mpv_get_property_string(mpv, "loop-file") };
118+ bool loop_file = loop_file_opt && strcmp(loop_file_opt.get(), "no");
119+ mp_string loop_playlist_opt{ mpv_get_property_string(mpv, "loop-playlist") };
120+ bool loop_playlist = loop_playlist_opt && strcmp(loop_playlist_opt.get(), "no");
121+ if (loop_file) {
122+ smtc.AutoRepeatMode(MediaPlaybackAutoRepeatMode::Track);
123+ } else if (loop_playlist) {
124+ smtc.AutoRepeatMode(MediaPlaybackAutoRepeatMode::List);
125+ } else {
126+ smtc.AutoRepeatMode(MediaPlaybackAutoRepeatMode::None);
127+ }
128+
129+ auto pos = mp_get_property<MPV_FORMAT_DOUBLE>(mpv, "time-pos");
130+ auto duration = mp_get_property<MPV_FORMAT_DOUBLE>(mpv, "duration");
131+
132+ if (!pos || !duration)
133+ return;
134+
135+ SystemMediaTransportControlsTimelineProperties tl;
136+ tl.StartTime(0s);
137+ tl.MinSeekTime(0s);
138+ tl.Position(std::chrono::duration_cast<TimeSpan>(std::chrono::duration<double>(*pos)));
139+ tl.MaxSeekTime(std::chrono::duration_cast<TimeSpan>(std::chrono::duration<double>(*duration)));
140+ tl.EndTime(std::chrono::duration_cast<TimeSpan>(std::chrono::duration<double>(*duration)));
141+ smtc.UpdateTimelineProperties(tl);
142+}
143+
144+static void update_metadata(SystemMediaTransportControls &smtc, smtc_ctx &ctx)
145+{
146+ auto *mpv = ctx.mpv;
147+ auto updater = smtc.DisplayUpdater();
148+ updater.ClearAll();
149+
150+ auto image_opt = mp_get_property<MPV_FORMAT_FLAG>(mpv, "current-tracks/video/image");
151+ bool video = bool(image_opt);
152+ bool image = image_opt.value_or(false);
153+ auto audio = mp_get_property<MPV_FORMAT_FLAG>(mpv, "current-tracks/audio/selected");
154+
155+ if (!video && !image && !audio)
156+ return;
157+
158+ mp_string title{ mpv_get_property_osd_string(mpv, "media-title") };
159+ if (video && !image) {
160+ updater.Type(MediaPlaybackType::Video);
161+ const auto &props = updater.VideoProperties();
162+ if (title)
163+ props.Title(winrt::to_hstring(title.get()));
164+ auto ch_index = mp_get_property<MPV_FORMAT_INT64>(mpv, "chapter").value_or(-1);
165+ if (ch_index >= 0) {
166+ mp_string ch_title {
167+ mpv_get_property_string(mpv, std::format("chapter-list/{}/title", ch_index).c_str())
168+ };
169+ if (ch_title) {
170+ auto ch_count = mp_get_property<MPV_FORMAT_INT64>(mpv, "chapter-list/count").value_or(0);
171+ props.Subtitle(winrt::to_hstring(std::format("{} ({}/{})", ch_title.get(), ch_index + 1, ch_count)));
172+ }
173+ }
174+ } else if (image && !audio) {
175+ updater.Type(MediaPlaybackType::Image);
176+ const auto &props = updater.ImageProperties();
177+ if (title)
178+ props.Title(winrt::to_hstring(title.get()));
179+ } else {
180+ updater.Type(MediaPlaybackType::Music);
181+ const auto &props = updater.MusicProperties();
182+ if (title)
183+ props.Title(winrt::to_hstring(title.get()));
184+ if (mp_string str{ mpv_get_property_string(mpv, "metadata/by-key/Album_Artist") })
185+ props.AlbumArtist(winrt::to_hstring(str.get()));
186+ if (mp_string str{ mpv_get_property_string(mpv, "metadata/by-key/Album") })
187+ props.AlbumTitle(winrt::to_hstring(str.get()));
188+ if (mp_string str{ mpv_get_property_string(mpv, "metadata/by-key/Album_Track_Count") })
189+ props.AlbumTrackCount(std::atoi(str.get()));
190+ if (mp_string str{ mpv_get_property_string(mpv, "metadata/by-key/Artist") })
191+ props.Artist(winrt::to_hstring(str.get()));
192+ if (mp_string str{ mpv_get_property_string(mpv, "metadata/by-key/Track") })
193+ props.TrackNumber(std::atoi(str.get()));
194+ }
195+
196+ updater.Update();
197+}
198+
199+static void handle_mp_event(smtc_ctx *ctx, mpv_event *event)
200+{
201+ if (!ctx || !ctx->smtc || !ctx->mpv || ctx->close)
202+ return;
203+
204+ try {
205+ update_state(ctx->smtc, ctx->mpv);
206+ if (event->event_id == MPV_EVENT_PROPERTY_CHANGE) {
207+ auto &prop = *static_cast<mpv_event_property *>(event->data);
208+ if (!strcmp(prop.name, "time-pos") || !strcmp(prop.name, "duration"))
209+ return;
210+ }
211+ update_metadata(ctx->smtc, *ctx);
212+ } catch (const winrt::hresult_error& e) {
213+ MP_VERBOSE(ctx, "%s: 0x%x - %ls\n", __func__, int32_t(e.code()), e.message().c_str());
214+ }
215+}
216+
217+static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
218+{
219+ if (uMsg == WM_DESTROY)
220+ PostQuitMessage(0);
221+
222+ smtc_ctx *ctx = reinterpret_cast<smtc_ctx *>(GetWindowLongPtrW(hWnd, GWLP_USERDATA));
223+
224+ switch (uMsg)
225+ {
226+ case WM_MP_EVENT:
227+ handle_mp_event(ctx, reinterpret_cast<mpv_event *>(lParam));
228+ return 0;
229+ case WM_SETFOCUS:
230+ if (!ctx)
231+ return 0;
232+ if (auto wid { mp_get_property<MPV_FORMAT_INT64>(ctx->mpv, "window-id") })
233+ SetFocus(HWND(*wid));
234+ return 0;
235+ case WM_ACTIVATE:
236+ if (!ctx)
237+ return 0;
238+ if (auto wid { mp_get_property<MPV_FORMAT_INT64>(ctx->mpv, "window-id") }) {
239+ if (IsIconic(HWND(*wid)))
240+ ShowWindow(HWND(*wid), SW_RESTORE);
241+ BringWindowToTop(HWND(*wid));
242+ }
243+ return 0;
244+ }
245+
246+ return DefWindowProc(hWnd, uMsg, wParam, lParam);
247+}
248+
249+static MP_THREAD_VOID win_event_loop_fn(void *arg)
250+{
251+ mp_thread_set_name("smtc/win");
252+ auto &ctx = *static_cast<smtc_ctx *>(arg);
253+ auto *mpv = ctx.mpv;
254+
255+ WNDCLASS wc = {
256+ .lpfnWndProc = WindowProc,
257+ .hInstance = HINSTANCE(&__ImageBase),
258+ .hIcon = LoadIconW(HINSTANCE(&__ImageBase), L"IDI_ICON1"),
259+ .lpszClassName = L"mpv-smtc"
260+ };
261+ RegisterClassW(&wc);
262+
263+ try {
264+ // Dummy window is used to allow SMTC to work also in audio only mode,
265+ // where VO may not be created.
266+ ctx.hwnd = CreateWindowExW(0, wc.lpszClassName, L"mpv smtc",
267+ WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
268+ CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
269+ nullptr, nullptr, wc.hInstance, nullptr);
270+ if (!ctx.hwnd)
271+ winrt::throw_last_error();
272+
273+ SystemMediaTransportControls &smtc = ctx.smtc;
274+ auto interop = winrt::get_activation_factory<SystemMediaTransportControls,
275+ ISystemMediaTransportControlsInterop>();
276+ HRESULT hr = interop->GetForWindow(ctx.hwnd,
277+ winrt::guid_of<SystemMediaTransportControls>(),
278+ winrt::put_abi(smtc));
279+ if (FAILED(hr))
280+ winrt::throw_hresult(hr);
281+ SetWindowLongPtrW(ctx.hwnd, GWLP_USERDATA, LONG_PTR(&ctx));
282+
283+ smtc.IsEnabled(true);
284+
285+ smtc.ButtonPressed([&](const SystemMediaTransportControls &,
286+ const SystemMediaTransportControlsButtonPressedEventArgs &args) {
287+ switch (args.Button()) {
288+ case SystemMediaTransportControlsButton::Play:
289+ mp_set_property(mpv, "pause", false);
290+ break;
291+ case SystemMediaTransportControlsButton::Pause:
292+ mp_set_property(mpv, "pause", true);
293+ break;
294+ case SystemMediaTransportControlsButton::Stop:
295+ mpv_command_string(mpv, "stop");
296+ break;
297+ case SystemMediaTransportControlsButton::Next: {
298+ auto ch_index = mp_get_property<MPV_FORMAT_INT64>(mpv, "chapter").value_or(0);
299+ auto ch_count = mp_get_property<MPV_FORMAT_INT64>(mpv, "chapter-list/count");
300+ // mpv allows to jump past last chapter
301+ mpv_command_string(mpv, ch_index < ch_count ? "add chapter 1" : "playlist-next");
302+ break;
303+ }
304+ case SystemMediaTransportControlsButton::Previous: {
305+ auto ch_index = mp_get_property<MPV_FORMAT_INT64>(mpv, "chapter");
306+ mpv_command_string(mpv, ch_index > 0 ? "add chapter -1" : "playlist-prev");
307+ break;
308+ }
309+ default:
310+ break;
311+ }
312+ });
313+ smtc.PlaybackPositionChangeRequested([&](const SystemMediaTransportControls &,
314+ const PlaybackPositionChangeRequestedEventArgs &args) {
315+ auto position = args.RequestedPlaybackPosition();
316+ auto pos = std::chrono::duration_cast<std::chrono::duration<double>>(position).count();
317+ mp_set_property(mpv, "time-pos", pos);
318+ });
319+ smtc.PlaybackRateChangeRequested([&](const SystemMediaTransportControls &,
320+ const PlaybackRateChangeRequestedEventArgs &args) {
321+ mp_set_property(mpv, "speed", args.RequestedPlaybackRate());
322+ });
323+ smtc.ShuffleEnabledChangeRequested([&](const SystemMediaTransportControls &,
324+ const ShuffleEnabledChangeRequestedEventArgs &args) {
325+ mp_set_property(mpv, "shuffle", args.RequestedShuffleEnabled());
326+ });
327+ smtc.AutoRepeatModeChangeRequested([&](const SystemMediaTransportControls &,
328+ const AutoRepeatModeChangeRequestedEventArgs &args) {
329+ bool loop_file = false, loop_playlist = false;
330+ switch (args.RequestedAutoRepeatMode()) {
331+ case MediaPlaybackAutoRepeatMode::Track:
332+ loop_file = true;
333+ break;
334+ case MediaPlaybackAutoRepeatMode::List:
335+ loop_playlist = true;
336+ break;
337+ case MediaPlaybackAutoRepeatMode::None:
338+ break;
339+ }
340+ mp_set_property(mpv, "loop-file", loop_file);
341+ mp_set_property(mpv, "loop-playlist", loop_playlist);
342+ });
343+
344+ MSG msg;
345+ while(BOOL ret = GetMessageW(&msg, nullptr, 0, 0)) {
346+ if (ret == -1)
347+ winrt::throw_last_error();
348+ TranslateMessage(&msg);
349+ DispatchMessageW(&msg);
350+ }
351+ } catch (const winrt::hresult_error& e) {
352+ MP_ERR(&ctx, "%s: 0x%x - %ls\n", __func__, int32_t(e.code()), e.message().c_str());
353+ }
354+
355+ ctx.close = true;
356+ mpv_wakeup(mpv);
357+ HWND hwnd = ctx.hwnd;
358+ ctx.hwnd = nullptr;
359+ DestroyWindow(hwnd);
360+ UnregisterClassW(wc.lpszClassName, HINSTANCE(&__ImageBase));
361+
362+ MP_THREAD_RETURN();
363+}
364+
365+static MP_THREAD_VOID mpv_event_loop_fn(void *arg)
366+{
367+ mp_thread_set_name("smtc/mpv");
368+ auto mpv = static_cast<mpv_handle *>(arg);
369+ smtc_ctx ctx = {
370+ .log = mp_client_get_log(mpv),
371+ .mpv = mpv
372+ };
373+
374+ // Create a dedicated window and event loop. We could use the mpv main window,
375+ // but it is not always available, especially in audio-only/console mode.
376+ mp_thread win_event_loop;
377+ if (mp_thread_create(&win_event_loop, win_event_loop_fn, &ctx)) {
378+ MP_ERR(&ctx, "Failed to create window event thread!\n");
379+ goto error;
380+ }
381+
382+ // It is recommended that you keep the system controls in sync with your
383+ // media playback by updating these properties approximately every 5 seconds
384+ // during playback and again whenever the state of playback changes, such as
385+ // pausing or seeking to a new position.
386+ // https://learn.microsoft.com/windows/uwp/audio-video-camera/system-media-transport-controls
387+ // For simplicity we observe time-pos and duration as integers, so we get
388+ // update every second, faster than recommended, but should be fine.
389+
390+ mpv_observe_property(mpv, 0, "current-tracks", MPV_FORMAT_NODE_MAP);
391+ mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_INT64);
392+ mpv_observe_property(mpv, 0, "idle-active", MPV_FORMAT_FLAG);
393+ mpv_observe_property(mpv, 0, "media-title", MPV_FORMAT_STRING);
394+ mpv_observe_property(mpv, 0, "metadata", MPV_FORMAT_NODE_MAP);
395+ mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_FLAG);
396+ mpv_observe_property(mpv, 0, "shuffle", MPV_FORMAT_DOUBLE);
397+ mpv_observe_property(mpv, 0, "speed", MPV_FORMAT_DOUBLE);
398+ mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_INT64);
399+ // TODO: Options are not observable, fix me!
400+ mpv_observe_property(mpv, 0, "loop-file", MPV_FORMAT_DOUBLE);
401+ mpv_observe_property(mpv, 0, "loop-playlist", MPV_FORMAT_DOUBLE);
402+
403+ while (!ctx.close) {
404+ mpv_event *event = mpv_wait_event(mpv, -1);
405+ if (ctx.close)
406+ break;
407+ if (event->event_id == MPV_EVENT_SHUTDOWN) {
408+ HWND hwnd = ctx.hwnd;
409+ if (hwnd)
410+ PostMessageW(hwnd, WM_CLOSE, 0, 0);
411+ break;
412+ }
413+ if (event->event_id == MPV_EVENT_PROPERTY_CHANGE ||
414+ event->event_id == MPV_EVENT_PLAYBACK_RESTART)
415+ {
416+ HWND hwnd = ctx.hwnd;
417+ if (hwnd)
418+ SendMessageW(hwnd, WM_MP_EVENT, 0, LPARAM(event));
419+ }
420+ }
421+ mp_thread_join(win_event_loop);
422+
423+error:
424+ mpv_destroy(mpv);
425+ MP_THREAD_RETURN();
426+}
427+
428+void mp_smtc_init(mpv_handle *mpv)
429+{
430+ mp_thread mpv_event_loop;
431+ if (!mp_thread_create(&mpv_event_loop, mpv_event_loop_fn, mpv))
432+ mp_thread_detach(mpv_event_loop);
433+}
osdep/win32/smtc.h+29−0
@@ -0,0 +1,29 @@
1+/*
2+ * This file is part of mpv.
3+ *
4+ * mpv is free software; you can redistribute it and/or
5+ * modify it under the terms of the GNU Lesser General Public
6+ * License as published by the Free Software Foundation; either
7+ * version 2.1 of the License, or (at your option) any later version.
8+ *
9+ * mpv is distributed in the hope that it will be useful,
10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+ * GNU Lesser General Public License for more details.
13+ *
14+ * You should have received a copy of the GNU Lesser General Public
15+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
16+ */
17+
18+#pragma once
19+
20+#ifdef __cplusplus
21+extern "C" {
22+#endif
23+
24+typedef struct mpv_handle mpv_handle;
25+void mp_smtc_init(mpv_handle *client);
26+
27+#ifdef __cplusplus
28+}
29+#endif
player/main.c+6−0
@@ -36,6 +36,7 @@ · 35 unmodified lines
3636 #include "osdep/threads.h"
3737 #include "osdep/timer.h"
3838 #include "osdep/main-fn.h"
39+#include "osdep/win32/smtc.h"
3940
4041 #include "common/av_log.h"
4142 #include "common/codecs.h"
@@ -399,6 +400,11 @@ int mp_initialize(struct MPContext *mpctx, char **options) · 357 unmodified lines
399400 cocoa_set_mpv_handle(ctx);
400401 #endif
401402
403+#if defined(HAVE_WIN32_SMTC) && HAVE_WIN32_SMTC
404+ if (opts->media_controls == 2 || (mpctx->is_cli && opts->media_controls == 1))
405+ mp_smtc_init(mp_new_client(mpctx->clients, "SystemMediaTransportControls"));
406+#endif
407+
402408 if (opts->encode_opts->file && opts->encode_opts->file[0]) {
403409 mpctx->encode_lavc_ctx = encode_lavc_init(mpctx->global);
404410 if(!mpctx->encode_lavc_ctx) {

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t