Something went wrong. Try again.
🎥 Command line media player
Something went wrong. Try again.
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544/* * 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 <utility>
#include <windows.h>#include <systemmediatransportcontrolsinterop.h>#include <winrt/Windows.Foundation.h>#include <winrt/Windows.Graphics.Imaging.h>#include <winrt/Windows.Media.h>#include <winrt/Windows.Storage.h>#include <winrt/Windows.Storage.Streams.h>
extern "C" {#include "common/msg.h"#include "misc/node.h"#include "misc/path_utils.h"#include "osdep/threads.h"#include "player/client.h"}
EXTERN_C IMAGE_DOS_HEADER __ImageBase;#define WM_MP_EVENT (WM_USER + 1)
using namespace std::chrono_literals;using namespace winrt::Windows::Foundation;using namespace winrt::Windows::Graphics::Imaging;using namespace winrt::Windows::Media;using namespace winrt::Windows::Storage;using namespace winrt::Windows::Storage::Streams;
struct mpv_deleter { void operator()(void *ptr) const { mpv_free(ptr); }};using mp_string = std::unique_ptr<char, mpv_deleter>;
struct mp_node : mpv_node { mp_node() = default; ~mp_node() { if (valid) mpv_free_node_contents(static_cast<mpv_node *>(this)); } mp_node(const mp_node &) = delete; mp_node& operator=(const mp_node &) = delete; mp_node& operator=(mp_node &&) = delete; mp_node(mp_node &&o) noexcept : mpv_node(static_cast<mpv_node &>(o)), valid(std::exchange(o.valid, false)) {}
bool valid = false;};
template<mpv_format F> struct mp_fmt;template<> struct mp_fmt<MPV_FORMAT_FLAG> { using type = int; };template<> struct mp_fmt<MPV_FORMAT_INT64> { using type = int64_t; };template<> struct mp_fmt<MPV_FORMAT_DOUBLE> { using type = double; };template<> struct mp_fmt<MPV_FORMAT_NODE> { using type = mp_node; };
template<mpv_format F>static inline std::optional<typename mp_fmt<F>::type>mp_get_property(mpv_handle *mpv, const char *name){ typename mp_fmt<F>::type val; if (mpv_get_property(mpv, name, F, &val) != MPV_ERROR_SUCCESS) return std::nullopt; if constexpr (F == MPV_FORMAT_NODE) val.valid = true; return val;}
template<typename T> struct mp_fmt_e;template<> struct mp_fmt_e<int> { static constexpr mpv_format value = MPV_FORMAT_FLAG; };template<> struct mp_fmt_e<bool> { static constexpr mpv_format value = MPV_FORMAT_FLAG; };template<> struct mp_fmt_e<int64_t> { static constexpr mpv_format value = MPV_FORMAT_INT64; };template<> struct mp_fmt_e<double> { static constexpr mpv_format value = MPV_FORMAT_DOUBLE; };
template<typename T>static inline int mp_set_property(mpv_handle *mpv, const char *name, T &&val){ using val_t = std::remove_reference_t<T>; typename mp_fmt<mp_fmt_e<val_t>::value>::type mpv_val = std::forward<T>(val); return mpv_set_property(mpv, name, mp_fmt_e<val_t>::value, &mpv_val);}
struct smtc_ctx { mp_log *log; mpv_handle *mpv; SystemMediaTransportControls smtc{ nullptr }; IAsyncOperation<FileProperties::StorageItemThumbnail> thumb_async{ nullptr }; std::atomic_bool close{ false }; std::atomic<HWND> hwnd{ nullptr };};
static void update_state(SystemMediaTransportControls &smtc, mpv_handle *mpv){ auto closed = mp_get_property<MPV_FORMAT_FLAG>(mpv, "idle-active"); if (!closed.value_or(false)) { auto paused = mp_get_property<MPV_FORMAT_FLAG>(mpv, "pause"); smtc.PlaybackStatus(paused.value_or(true) ? MediaPlaybackStatus::Paused : MediaPlaybackStatus::Playing); smtc.IsPlayEnabled(true); smtc.IsPauseEnabled(true); smtc.IsStopEnabled(true); auto ch_index = mp_get_property<MPV_FORMAT_INT64>(mpv, "chapter"); auto ch_count = mp_get_property<MPV_FORMAT_INT64>(mpv, "chapter-list/count"); auto pl_count = mp_get_property<MPV_FORMAT_INT64>(mpv, "playlist-count"); smtc.IsNextEnabled(pl_count > 1 || ch_count > ch_index.value_or(0)); 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); }
auto shuffle = mp_get_property<MPV_FORMAT_FLAG>(mpv, "shuffle"); smtc.ShuffleEnabled(shuffle.value_or(false));
auto speed = mp_get_property<MPV_FORMAT_DOUBLE>(mpv, "speed"); smtc.PlaybackRate(speed.value_or(1.0));
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); }
auto pos = mp_get_property<MPV_FORMAT_DOUBLE>(mpv, "time-pos"); auto duration = mp_get_property<MPV_FORMAT_DOUBLE>(mpv, "duration");
if (!pos || !duration) return;
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_thumbnail(SystemMediaTransportControls &smtc, smtc_ctx &ctx){ auto track_list = mp_get_property<MPV_FORMAT_NODE>(ctx.mpv, "track-list"); if (!track_list) return; assert(track_list->format == MPV_FORMAT_NODE_ARRAY); auto list = track_list->u.list; const char *thumbnail = nullptr; mp_string filepath{ mpv_get_property_string(ctx.mpv, "path") }; // Get first albumart or image from tracks for (int i = 0; i < list->num; ++i) { mpv_node *img = node_map_get(list->values + i, "image"); if (!img || img->format != MPV_FORMAT_FLAG || !img->u.flag) continue;
// If this is the only track selected and image, try to use it directly if (list->num == 1) thumbnail = filepath.get();
mpv_node *file = node_map_get(list->values + i, "external-filename"); if (!file || file->format != MPV_FORMAT_STRING || !file->u.string) continue;
// Select first image found if (!thumbnail) thumbnail = file->u.string;
mpv_node *art = node_map_get(list->values + i, "albumart"); if (!art || art->format != MPV_FORMAT_FLAG || !art->u.flag) continue;
// Select first albumart found thumbnail = file->u.string; break; }
try { if (thumbnail) { mp_string normalized_path{ mp_normalize_path(nullptr, thumbnail) }; MP_TRACE(&ctx, "Using thumbnail: %s\n", normalized_path.get()); auto hstr = winrt::to_hstring(normalized_path.get()); RandomAccessStreamReference stream_ref{ nullptr }; if (mp_is_url(bstr0(thumbnail))) { stream_ref = RandomAccessStreamReference::CreateFromUri(Uri(hstr)); } else { auto storage_file = StorageFile::GetFileFromPathAsync(hstr); stream_ref = RandomAccessStreamReference::CreateFromFile(storage_file.get()); } if (!stream_ref) return; auto updater = smtc.DisplayUpdater(); updater.Thumbnail(stream_ref); } else { if (filepath && !mp_is_url(bstr0(filepath.get()))) { mp_string path{ mp_normalize_path(nullptr, filepath.get()) }; MP_TRACE(&ctx, "Generating thumbnail for: %s\n", path.get()); auto storage_file = StorageFile::GetFileFromPathAsync(winrt::to_hstring(path.get()));
if (ctx.thumb_async) ctx.thumb_async.Cancel(); ctx.thumb_async = storage_file.get().GetThumbnailAsync(FileProperties::ThumbnailMode::SingleItem, 240);
ctx.thumb_async.Completed([&ctx](auto &&async, const AsyncStatus &status) { if (status != AsyncStatus::Completed || ctx.close) return; auto stream_ref = RandomAccessStreamReference::CreateFromStream(async.GetResults()); auto updater = ctx.smtc.DisplayUpdater(); updater.Thumbnail(stream_ref); updater.Update(); }); } } } catch (const winrt::hresult_error& e) { MP_VERBOSE(&ctx, "%s: 0x%x - %ls\n", __func__, int32_t(e.code()), e.message().c_str()); }}
static void update_metadata(SystemMediaTransportControls &smtc, smtc_ctx &ctx){ auto *mpv = ctx.mpv; auto updater = smtc.DisplayUpdater(); updater.ClearAll();
auto image_opt = mp_get_property<MPV_FORMAT_FLAG>(mpv, "current-tracks/video/image"); bool video = bool(image_opt); bool image = image_opt.value_or(false); auto audio = mp_get_property<MPV_FORMAT_FLAG>(mpv, "current-tracks/audio/selected");
if (!video && !image && !audio) return;
update_thumbnail(smtc, ctx);
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())); auto ch_index = mp_get_property<MPV_FORMAT_INT64>(mpv, "chapter").value_or(-1); if (ch_index >= 0) { mp_string ch_title { mpv_get_property_string(mpv, std::format("chapter-list/{}/title", ch_index).c_str()) }; if (ch_title) { auto ch_count = mp_get_property<MPV_FORMAT_INT64>(mpv, "chapter-list/count").value_or(0); 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 void handle_mp_event(smtc_ctx *ctx, mpv_event *event){ if (!ctx || !ctx->smtc || !ctx->mpv || ctx->close) return;
try { update_state(ctx->smtc, ctx->mpv); if (event->event_id == MPV_EVENT_PROPERTY_CHANGE) { auto &prop = *static_cast<mpv_event_property *>(event->data); if (!strcmp(prop.name, "time-pos") || !strcmp(prop.name, "duration")) return; } update_metadata(ctx->smtc, *ctx); } catch (const winrt::hresult_error& e) { MP_VERBOSE(ctx, "%s: 0x%x - %ls\n", __func__, int32_t(e.code()), e.message().c_str()); }}
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){ if (uMsg == WM_DESTROY) PostQuitMessage(0);
smtc_ctx *ctx = reinterpret_cast<smtc_ctx *>(GetWindowLongPtrW(hWnd, GWLP_USERDATA));
switch (uMsg) { case WM_MP_EVENT: handle_mp_event(ctx, reinterpret_cast<mpv_event *>(lParam)); return 0; case WM_SETFOCUS: if (!ctx) return 0; if (auto wid { mp_get_property<MPV_FORMAT_INT64>(ctx->mpv, "window-id") }) SetFocus(HWND(*wid)); return 0; case WM_ACTIVATE: if (!ctx) return 0; if (auto wid { mp_get_property<MPV_FORMAT_INT64>(ctx->mpv, "window-id") }) { if (IsIconic(HWND(*wid))) ShowWindow(HWND(*wid), SW_RESTORE); BringWindowToTop(HWND(*wid)); } return 0; }
return DefWindowProc(hWnd, uMsg, wParam, lParam);}
static MP_THREAD_VOID win_event_loop_fn(void *arg){ mp_thread_set_name("smtc/win"); auto &ctx = *static_cast<smtc_ctx *>(arg); auto *mpv = ctx.mpv;
WNDCLASS wc = { .lpfnWndProc = WindowProc, .hInstance = HINSTANCE(&__ImageBase), .hIcon = LoadIconW(HINSTANCE(&__ImageBase), L"IDI_ICON1"), .lpszClassName = L"mpv-smtc" }; RegisterClassW(&wc);
try { // Dummy window is used to allow SMTC to work also in audio only mode, // where VO may not be created. 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 = ctx.smtc; 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(&ctx));
smtc.IsEnabled(true);
smtc.ButtonPressed([&](const SystemMediaTransportControls &, const SystemMediaTransportControlsButtonPressedEventArgs &args) { switch (args.Button()) { case SystemMediaTransportControlsButton::Play: mp_set_property(mpv, "pause", false); break; case SystemMediaTransportControlsButton::Pause: mp_set_property(mpv, "pause", true); break; case SystemMediaTransportControlsButton::Stop: mpv_command_string(mpv, "stop"); break; case SystemMediaTransportControlsButton::Next: { auto ch_index = mp_get_property<MPV_FORMAT_INT64>(mpv, "chapter").value_or(0); auto ch_count = mp_get_property<MPV_FORMAT_INT64>(mpv, "chapter-list/count"); // mpv allows to jump past last chapter mpv_command_string(mpv, ch_index < ch_count ? "add chapter 1" : "playlist-next"); break; } case SystemMediaTransportControlsButton::Previous: { auto ch_index = mp_get_property<MPV_FORMAT_INT64>(mpv, "chapter"); mpv_command_string(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<std::chrono::duration<double>>(position).count(); mp_set_property(mpv, "time-pos", pos); }); smtc.PlaybackRateChangeRequested([&](const SystemMediaTransportControls &, const PlaybackRateChangeRequestedEventArgs &args) { mp_set_property(mpv, "speed", args.RequestedPlaybackRate()); }); smtc.ShuffleEnabledChangeRequested([&](const SystemMediaTransportControls &, const ShuffleEnabledChangeRequestedEventArgs &args) { mp_set_property(mpv, "shuffle", args.RequestedShuffleEnabled()); }); 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; } mp_set_property(mpv, "loop-file", loop_file); mp_set_property(mpv, "loop-playlist", loop_playlist); });
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: 0x%x - %ls\n", __func__, int32_t(e.code()), e.message().c_str()); }
ctx.close = true; mpv_wakeup(mpv); HWND hwnd = ctx.hwnd; ctx.hwnd = nullptr; if (ctx.thumb_async) ctx.thumb_async.Cancel(); DestroyWindow(hwnd); UnregisterClassW(wc.lpszClassName, HINSTANCE(&__ImageBase));
MP_THREAD_RETURN();}
static MP_THREAD_VOID mpv_event_loop_fn(void *arg){ mp_thread_set_name("smtc/mpv"); auto mpv = static_cast<mpv_handle *>(arg); 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; }
// 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 // For simplicity we observe time-pos and duration as integers, so we get // update every second, faster than recommended, but should be fine.
mpv_observe_property(mpv, 0, "current-tracks", MPV_FORMAT_NODE_MAP); mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_INT64); 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_INT64); mpv_observe_property(mpv, 0, "track-list", MPV_FORMAT_NODE); // 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 (ctx.close) break; if (event->event_id == MPV_EVENT_SHUTDOWN) { HWND hwnd = ctx.hwnd; if (hwnd) PostMessageW(hwnd, WM_CLOSE, 0, 0); break; } if (event->event_id == MPV_EVENT_PROPERTY_CHANGE || event->event_id == MPV_EVENT_PLAYBACK_RESTART) { HWND hwnd = ctx.hwnd; if (hwnd) SendMessageW(hwnd, WM_MP_EVENT, 0, LPARAM(event)); } } 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);}