| @@ -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 | +} |