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