Merge branch 'alvin/smtc' into 'master'

win32: Add SystemMediaTransportControls support

Closes #25258

See merge request videolan/vlc!3010
This commit is contained in:
alvinhochun 2024-04-28 07:10:56 +00:00
commit a6bf9b6060
10 changed files with 903 additions and 0 deletions

View File

@ -4607,6 +4607,26 @@ AC_ARG_WITH([doxygen-stylesheet],
[ doxygen_stylesheet="$with_doxygen_stylesheet" ], [])
AC_SUBST([doxygen_stylesheet])
dnl
dnl Check for C++/WinRT
dnl
have_cppwinrt="no"
AM_COND_IF([HAVE_WIN32_DESKTOP], [
PKG_CHECK_MODULES([CPPWINRT], [CppWinRT], [
AC_LANG_PUSH([C++])
AX_CHECK_COMPILE_FLAG([-std=c++20], [
AX_APPEND_FLAG([-std=c++20],[CPPWINRT_CFLAGS])
have_cppwinrt="yes"
], [
AC_MSG_WARN([CppWinRT disabled because compiler does not support C++20.])
])
AC_LANG_POP([C++])
], [
AC_MSG_WARN([${CPPWINRT_PKG_ERRORS}.])
])
])
AM_CONDITIONAL([HAVE_CPPWINRT], [test "${have_cppwinrt}" = "yes"])
dnl

View File

@ -0,0 +1 @@
9bb28cf84fe9f7be27bfe6e93f2abc199be7671b80db8234d632983d47edc6d526a416fa2980bc4def712ac1f498815470d2565f42ad44b6d4a693f83e2b1bcb mingw-w64-cppwinrt-2.0.221221.0-beta.1-headers.tar.gz

View File

@ -0,0 +1,28 @@
CPPWINRT_VERSION := 2.0.221221.0
CPPWINRT_VERSION_EXTRA := -beta.1
CPPWINRT_VERSION_FULL := $(CPPWINRT_VERSION)$(CPPWINRT_VERSION_EXTRA)
CPPWINRT_URL := $(GITHUB)/alvinhochun/mingw-w64-cppwinrt/releases/download/$(CPPWINRT_VERSION_FULL)/mingw-w64-cppwinrt-$(CPPWINRT_VERSION_FULL)-headers.tar.gz
ifdef HAVE_WIN32
ifndef HAVE_WINSTORE
PKGS += cppwinrt-headers
endif
endif
$(TARBALLS)/mingw-w64-cppwinrt-$(CPPWINRT_VERSION_FULL)-headers.tar.gz:
$(call download_pkg,$(CPPWINRT_URL),cppwinrt)
.sum-cppwinrt-headers: mingw-w64-cppwinrt-$(CPPWINRT_VERSION_FULL)-headers.tar.gz
cppwinrt-headers: mingw-w64-cppwinrt-$(CPPWINRT_VERSION_FULL)-headers.tar.gz .sum-cppwinrt-headers
$(UNPACK)
$(APPLY) $(SRC)/cppwinrt-headers/workaround-llvm13-libc++.patch
$(MOVE)
.cppwinrt-headers: cppwinrt-headers
@mkdir -p $(PREFIX)/include
cp -R cppwinrt-headers/include/cppwinrt-$(CPPWINRT_VERSION) $(PREFIX)/include
cp cppwinrt-headers/share/licenses/cppwinrt/cppwinrt/LICENSE $(PREFIX)/include/cppwinrt-$(CPPWINRT_VERSION)
@mkdir -p $(PREFIX)/lib/pkgconfig
sed 's,^prefix=.*,prefix=$(PREFIX),' cppwinrt-headers/share/pkgconfig/CppWinRT.pc > $(PREFIX)/lib/pkgconfig/CppWinRT.pc
touch $@

View File

@ -0,0 +1,24 @@
--- cppwinrt-headers/include/cppwinrt-2.0.221221.0/winrt/base.h.orig 2022-12-25 00:48:20.218588000 +0800
+++ cppwinrt-headers/include/cppwinrt-2.0.221221.0/winrt/base.h 2022-12-25 00:49:59.475143753 +0800
@@ -3309,19 +3309,21 @@
inline hstring to_hstring(int64_t value)
{
return impl::hstring_convert(value);
}
+#if !defined(_LIBCPP_VERSION) || _LIBCPP_VERSION >= 14000
inline hstring to_hstring(float value)
{
return impl::hstring_convert(value);
}
inline hstring to_hstring(double value)
{
return impl::hstring_convert(value);
}
+#endif
inline hstring to_hstring(char16_t value)
{
wchar_t buffer[2] = { value, 0 };
return hstring{ std::wstring_view{ buffer, 1 } };

View File

@ -53,7 +53,13 @@ endif
libntservice_plugin_la_SOURCES = control/ntservice.c
libwin_hotkeys_plugin_la_SOURCES = control/globalhotkeys/win32.c
libwin_msg_plugin_la_SOURCES = control/win_msg.c
libwin_smtc_plugin_la_SOURCES = control/win_smtc.cpp
libwin_smtc_plugin_la_CXXFLAGS = $(AM_CXXFLAGS) $(LIBCOMCXXFLAGS) $(CPPWINRT_CFLAGS)
libwin_smtc_plugin_la_LIBADD = $(LIBCOM)
if HAVE_WIN32_DESKTOP
control_LTLIBRARIES += libntservice_plugin.la
control_LTLIBRARIES += libwin_hotkeys_plugin.la libwin_msg_plugin.la
if HAVE_CPPWINRT
control_LTLIBRARIES += libwin_smtc_plugin.la
endif
endif

View File

@ -0,0 +1,798 @@
/*****************************************************************************
* win_smtc.cpp : Windows SystemMediaTransportControls integration for VLC
*****************************************************************************
* Copyright (c) 2022 Alvin Wong
*
* Authors: Alvin Wong
*
* The MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
/* VLC core API headers */
#include <vlc_common.h>
#include <vlc_cxx_helpers.hpp>
#include <vlc_es.h>
#include <vlc_interface.h>
#include <vlc_input_item.h>
#include <vlc_messages.h>
#include <vlc_player.h>
#include <vlc_playlist.h>
#include <vlc_plugin.h>
#include <vlc_tick.h>
#include <vlc_url.h>
/* C++ standard library headers */
#include <algorithm>
#include <charconv>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <exception>
#include <memory>
#include <new>
#include <span>
#include <string_view>
#include <type_traits>
/* Windows and COM headers */
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#if __has_include(<systemmediatransportcontrolsinterop.h>)
# include <systemmediatransportcontrolsinterop.h>
#else
# include <inspectable.h>
// The following are definitions for SystemMediaTransportControlsInterop
// taken from bleeding-edge mingw-w64 (commit 0538a47), and stripped down
// to the minimal required.
extern "C" {
MIDL_INTERFACE("ddb0472d-c911-4a1f-86d9-dc3d71a95f5a")
ISystemMediaTransportControlsInterop : public IInspectable
{
virtual HRESULT STDMETHODCALLTYPE GetForWindow(
HWND appWindow,
REFIID riid,
void **mediaTransportControl) = 0;
};
# ifdef __CRT_UUID_DECL
__CRT_UUID_DECL(ISystemMediaTransportControlsInterop, 0xddb0472d, 0xc911, 0x4a1f, 0x86,0xd9, 0xdc,0x3d,0x71,0xa9,0x5f,0x5a)
# endif
}
#endif
/* C++/WinRT headers */
#include <winrt/base.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Media.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Windows.Storage.Streams.h>
using winrt::Windows::Foundation::TimeSpan;
using winrt::Windows::Media::MediaPlaybackStatus;
using winrt::Windows::Media::PlaybackPositionChangeRequestedEventArgs;
using winrt::Windows::Media::SystemMediaTransportControls;
using winrt::Windows::Media::SystemMediaTransportControlsButton;
using winrt::Windows::Media::SystemMediaTransportControlsButtonPressedEventArgs;
using winrt::Windows::Media::SystemMediaTransportControlsTimelineProperties;
/**
* Define how often the SMTC timeline should be updated. MS docs recommends
* updating every 5 secs.
*/
constexpr vlc_tick_t SMTC_TIMELINE_UPDATE_INTERVAL = VLC_TICK_FROM_MS(5000);
/* Message numbers for private use */
enum {
WM_MY_QUIT_MSG = WM_USER + 333,
WM_MY_ARTWORK_UPDATE_MSG,
};
/* Internal state for an instance of the module */
struct intf_sys_t
{
vlc_thread_t thread{};
HANDLE ready_event{};
bool thread_errored{};
HWND hwnd{};
vlc_tick_t last_timeline_update_time{};
SystemMediaTransportControls smtc{ nullptr };
SystemMediaTransportControlsTimelineProperties timeline_props{ nullptr };
vlc_playlist_t *playlist{};
vlc_player_listener_id *player_listener{};
vlc_playlist_listener_id *playlist_listener{};
};
/* Helper class for handling C strings with RAII */
class owned_cstr
{
struct deleter {
void operator()(char *psz_str) { free(psz_str); }
};
std::unique_ptr<char, deleter> m_psz_str;
std::string_view m_string_view;
public:
owned_cstr() noexcept {}
owned_cstr(const owned_cstr &) noexcept = delete;
owned_cstr(owned_cstr &&) noexcept = default;
explicit owned_cstr(char *psz_str) noexcept
: m_psz_str{psz_str}
{
if (psz_str)
m_string_view = { psz_str };
}
owned_cstr &operator=(const owned_cstr &) noexcept = delete;
owned_cstr &operator=(owned_cstr &&) noexcept = default;
operator bool() const noexcept {
return m_psz_str.get();
}
operator const std::string_view &() const noexcept {
return m_string_view;
}
const char *get() const noexcept {
return m_psz_str.get();
}
std::string_view* operator->() noexcept {
return &m_string_view;
}
const char& operator[](size_t pos) noexcept {
return m_string_view[pos];
}
char *release() noexcept {
m_string_view = {};
return m_psz_str.release();
}
operator winrt::hstring() const {
if (m_string_view.empty())
return {};
// Note: winrt::to_hstring takes UTF-8 strings.
return winrt::to_hstring(m_string_view);
}
};
static inline winrt::hstring hstr(const owned_cstr &cstr)
{
return static_cast<winrt::hstring>(cstr);
}
/**
* Helper function to log a caught exception. Must be called only from inside
* a `catch` block, or otherwise will call `std::terminate()`.
*/
static void log_exception(intf_thread_t *intf, const char *func) noexcept try
{
throw;
} catch (const winrt::hresult_error &ex) {
msg_Err(intf, "unhandled exception in %s "
"(hresult %x)", func, ex.code().value);
} catch (const std::exception &ex) {
msg_Err(intf, "unhandled exception in %s "
"(%s)", func, ex.what());
} catch (...) {
msg_Err(intf, "unhandled exception in %s"
"(unknown)", func);
}
static bool is_playable_media(input_item_t *media)
{
switch (media->i_type) {
case ITEM_TYPE_DISC:
case ITEM_TYPE_FILE:
case ITEM_TYPE_STREAM:
return true;
case ITEM_TYPE_UNKNOWN:
case ITEM_TYPE_DIRECTORY:
case ITEM_TYPE_CARD:
case ITEM_TYPE_PLAYLIST:
case ITEM_TYPE_NODE:
default:
return false;
}
}
static void update_media_metadata(intf_thread_t *intf, input_item_t *media)
{
intf_sys_t *sys = intf->p_sys;
if (!is_playable_media(media))
return;
owned_cstr psz_title{ input_item_GetTitleFbName(media) };
owned_cstr psz_genre{ input_item_GetGenre(media) };
auto updater = sys->smtc.DisplayUpdater();
updater.ClearAll();
// SMTC supports different properties for different media types. Available
// types are music, video and image. We don't need to handle images. To
// distinguish video from music, just check if the media contains a video
// stream.
const auto esFormats = std::span(media->es, media->i_es);
bool hasVideo = std::any_of(esFormats.begin(), esFormats.end(),
[](auto *es) { return es->i_cat == VIDEO_ES; });
msg_Dbg(intf, "updating media metadata: hasVideo=%d, title=%s", hasVideo,
psz_title ? psz_title->begin() : "");
if (hasVideo) {
updater.Type(winrt::Windows::Media::MediaPlaybackType::Video);
auto v = updater.VideoProperties();
v.Title(hstr(psz_title));
// TODO: Set v.Subtitle?
if (!psz_genre->empty())
v.Genres().Append(hstr(psz_genre));
} else {
updater.Type(winrt::Windows::Media::MediaPlaybackType::Music);
auto m = updater.MusicProperties();
m.Title(hstr(psz_title));
m.Artist(hstr(owned_cstr{ input_item_GetArtist(media) }));
m.AlbumTitle(hstr(owned_cstr{ input_item_GetAlbum(media) }));
m.AlbumArtist(hstr(owned_cstr{ input_item_GetAlbumArtist(media) }));
if (!psz_genre->empty())
m.Genres().Append(hstr(psz_genre));
// VLC supplies track number and count as strings, but SMTC wants
// them as integers, so convert them if possible.
uint32_t trackNumber;
owned_cstr psz_track_number{ input_item_GetTrackNumber(media) };
auto res = std::from_chars(psz_track_number->begin(),
psz_track_number->end(), trackNumber);
if (res.ec == std::errc())
m.TrackNumber(trackNumber);
uint32_t trackTotal;
owned_cstr psz_track_total{ input_item_GetTrackTotal(media) };
res = std::from_chars(psz_track_total->begin(), psz_track_total->end(),
trackTotal);
if (res.ec == std::errc())
m.AlbumTrackCount(trackTotal);
}
// The metadata is not updated until Update() has been called.
updater.Update();
// Loading the artwork is potentially time-consuming, so do it on the
// plugin thread to not block this thread.
PostMessageW(sys->hwnd, WM_MY_ARTWORK_UPDATE_MSG, (WPARAM)intf,
(LPARAM)input_item_GetArtworkURL(media));
}
static void on_current_media_changed(vlc_player_t *player,
input_item_t *new_media,
void *data) noexcept try
{
VLC_UNUSED(player);
intf_thread_t *intf = (intf_thread_t *)data;
intf_sys_t *sys = intf->p_sys;
if (!new_media || !is_playable_media(new_media)) {
msg_Dbg(intf, "no media selected, disabling SMTC");
sys->smtc.IsEnabled(false);
return;
}
sys->smtc.IsEnabled(true);
update_media_metadata(intf, new_media);
} catch (...) {
log_exception((intf_thread_t *)data, __func__);
}
static void on_state_changed(vlc_player_t *player, vlc_player_state new_state,
void *data) noexcept try
{
intf_thread_t *intf = (intf_thread_t *)data;
intf_sys_t *sys = intf->p_sys;
MediaPlaybackStatus status;
switch (new_state) {
case VLC_PLAYER_STATE_STARTED:
case VLC_PLAYER_STATE_PLAYING:
status = MediaPlaybackStatus::Playing;
break;
case VLC_PLAYER_STATE_PAUSED:
status = MediaPlaybackStatus::Paused;
break;
case VLC_PLAYER_STATE_STOPPING:
status = MediaPlaybackStatus::Changing;
break;
case VLC_PLAYER_STATE_STOPPED:
default:
status = MediaPlaybackStatus::Stopped;
break;
}
sys->smtc.PlaybackStatus(status);
switch (new_state) {
case VLC_PLAYER_STATE_STOPPED:
case VLC_PLAYER_STATE_PAUSED: {
// When playback is paused or stopped, always update the timeline
// because on_position_changed is not going to be fired again.
sys->last_timeline_update_time = vlc_tick_now();
auto time = vlc_player_GetTime(player);
sys->timeline_props.Position(std::chrono::microseconds(US_FROM_VLC_TICK(time)));
sys->smtc.UpdateTimelineProperties(sys->timeline_props);
} break;
default:
break;
}
} catch (...) {
log_exception((intf_thread_t *)data, __func__);
}
static void on_position_changed(vlc_player_t *player, vlc_tick_t new_time,
double new_pos, void *data) noexcept try
{
VLC_UNUSED(player);
VLC_UNUSED(new_pos);
intf_thread_t *intf = (intf_thread_t *)data;
intf_sys_t *sys = intf->p_sys;
auto now = vlc_tick_now();
// Update the timeline at a limited rate. Always update if the player
// is paused.
if (!vlc_player_IsPaused(player) && sys->last_timeline_update_time &&
now - sys->last_timeline_update_time < SMTC_TIMELINE_UPDATE_INTERVAL)
return;
sys->last_timeline_update_time = now;
sys->timeline_props.Position(std::chrono::microseconds(US_FROM_VLC_TICK(new_time)));
sys->smtc.UpdateTimelineProperties(sys->timeline_props);
} catch (...) {
log_exception((intf_thread_t *)data, __func__);
}
static void on_length_changed(vlc_player_t *player, vlc_tick_t new_length,
void *data) noexcept try
{
VLC_UNUSED(player);
intf_thread_t *intf = (intf_thread_t *)data;
intf_sys_t *sys = intf->p_sys;
sys->timeline_props.StartTime(TimeSpan(0));
sys->timeline_props.MinSeekTime(TimeSpan(0));
TimeSpan length{0};
if (new_length > 0)
length = std::chrono::microseconds(US_FROM_VLC_TICK(new_length));
sys->timeline_props.MaxSeekTime(length);
sys->timeline_props.EndTime(length);
sys->smtc.UpdateTimelineProperties(sys->timeline_props);
} catch (...) {
log_exception((intf_thread_t *)data, __func__);
}
static void on_media_meta_changed(vlc_player_t *player, input_item_t *media,
void *data) noexcept try
{
VLC_UNUSED(player);
intf_thread_t *intf = (intf_thread_t *)data;
update_media_metadata(intf, media);
} catch (...) {
log_exception((intf_thread_t *)data, __func__);
}
static const vlc_player_cbs player_cbs = {
.on_current_media_changed = on_current_media_changed,
.on_state_changed = on_state_changed,
.on_position_changed = on_position_changed,
.on_length_changed = on_length_changed,
.on_media_meta_changed = on_media_meta_changed,
};
static void on_playback_repeat_changed(vlc_playlist_t *playlist,
vlc_playlist_playback_repeat repeat,
void *data) noexcept try
{
using RepeatMode = winrt::Windows::Media::MediaPlaybackAutoRepeatMode;
VLC_UNUSED(playlist);
intf_thread_t *intf = (intf_thread_t *)data;
intf_sys_t *sys = intf->p_sys;
RepeatMode repeatMode;
switch (repeat) {
case VLC_PLAYLIST_PLAYBACK_REPEAT_CURRENT:
repeatMode = RepeatMode::Track;
break;
case VLC_PLAYLIST_PLAYBACK_REPEAT_ALL:
repeatMode = RepeatMode::List;
break;
case VLC_PLAYLIST_PLAYBACK_REPEAT_NONE:
default:
repeatMode = RepeatMode::None;
break;
}
sys->smtc.AutoRepeatMode(repeatMode);
} catch (...) {
log_exception((intf_thread_t *)data, __func__);
}
static void on_playback_order_changed(vlc_playlist_t *playlist,
vlc_playlist_playback_order order,
void *data) noexcept try
{
VLC_UNUSED(playlist);
intf_thread_t *intf = (intf_thread_t *)data;
intf_sys_t *sys = intf->p_sys;
bool isRandom = order == VLC_PLAYLIST_PLAYBACK_ORDER_RANDOM;
sys->smtc.ShuffleEnabled(isRandom);
} catch (...) {
log_exception((intf_thread_t *)data, __func__);
}
static const vlc_playlist_callbacks playlist_cbs = {
.on_playback_repeat_changed = on_playback_repeat_changed,
.on_playback_order_changed = on_playback_order_changed,
};
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam,
LPARAM lParam) noexcept {
switch (uMsg) {
case WM_MY_QUIT_MSG:
PostQuitMessage(0);
return 0;
case WM_MY_ARTWORK_UPDATE_MSG: try {
intf_thread_t *intf = (intf_thread_t *)wParam;
if (!intf)
return 0;
intf_sys_t *sys = intf->p_sys;
owned_cstr psz_artwork_path{ [&]() -> char * {
owned_cstr psz_artwork_url{ (char *)lParam };
if (psz_artwork_url->empty())
return nullptr;
return vlc_uri2path(psz_artwork_url.get());
}() };
// Here it is possible for another thread to be updating other
// properties at the same time, but there should be no real risk of
// problematic race conditions. Worst case we just call Update() twice.
auto updater = sys->smtc.DisplayUpdater();
if (!psz_artwork_path->empty()) {
using winrt::Windows::Storage::StorageFile;
using StreamRef = winrt::Windows::Storage::Streams::
RandomAccessStreamReference;
msg_Dbg(intf, "updating artwork: %s", psz_artwork_path->begin());
// This blocks synchronously but should be fine because this is
// running on the plugin thread. To run this asynchronously, we
// will need to handle cancellation properly and also risk getting
// bitten by coroutine bugs in the compiler, so not really worth
// the effort.
auto artworkFile =
StorageFile::GetFileFromPathAsync(hstr(psz_artwork_path)).get();
// We now check if there are any newer artwork update requests
// from the thread, so that we can skip updating with the obsolete
// artwork.
MSG peekMsg{};
if (PeekMessageW(&peekMsg, hwnd, WM_MY_ARTWORK_UPDATE_MSG,
WM_MY_ARTWORK_UPDATE_MSG, PM_NOREMOVE))
return 0;
updater.Thumbnail(StreamRef::CreateFromFile(artworkFile));
} else {
msg_Dbg(intf, "updating artwork: (null)");
updater.Thumbnail(nullptr);
}
updater.Update();
} catch (...) {
log_exception((intf_thread_t *)wParam, "WM_MY_ARTWORK_UPDATE_MSG");
} return 0;
case WM_CLOSE:
return 0;
default:
break;
}
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
extern "C" IMAGE_DOS_HEADER __ImageBase;
static HWND CreateMessageHwnd() noexcept
{
HINSTANCE hInstance = (HINSTANCE)&__ImageBase;
HWND hwnd = CreateWindowExW(
0, // Optional window styles.
L"STATIC", // Window class
L"VLC SystemMediaTransportControls hwnd", // Window text
WS_POPUP, // Window style
// Size and position
0, 0, 0, 0,
nullptr, // Parent window
nullptr, // Menu
hInstance, // Instance handle
nullptr // Additional application data
);
if (!hwnd)
return nullptr;
SetWindowLongPtrW(hwnd, GWLP_WNDPROC, (LONG_PTR)WindowProc);
return hwnd;
}
static SystemMediaTransportControls GetSmtcForHwnd(HWND hwnd)
{
auto smtcInterop =
winrt::get_activation_factory<SystemMediaTransportControls,
ISystemMediaTransportControlsInterop>();
SystemMediaTransportControls smtc{ nullptr };
HRESULT hr = smtcInterop->GetForWindow(
hwnd, winrt::guid_of<SystemMediaTransportControls>(),
winrt::put_abi(smtc));
if (FAILED(hr)) {
winrt::throw_hresult(hr);
}
return smtc;
}
/**
* Plugin thread that runs the message loop.
*/
static void *PluginThread(void *data) noexcept
{
intf_thread_t *intf = (intf_thread_t *)data;
intf_sys_t *sys = intf->p_sys;
vlc_thread_set_name("vlc-smtc-win");
struct init_error_t {};
std::unique_ptr<std::remove_pointer_t<HWND>, void (*)(HWND)> hwnd
{ nullptr, [](HWND hwnd) { DestroyWindow(hwnd); } };
std::unique_ptr<std::remove_pointer_t<HANDLE>, void (*)(HANDLE)>
timeline_timer{ nullptr, [](HANDLE h) { CloseHandle(h); } };
SystemMediaTransportControls smtc{ nullptr };
try {
winrt::init_apartment();
hwnd.reset(CreateMessageHwnd());
if (!hwnd) {
DWORD lastError = GetLastError();
msg_Err(intf, "couldn't create window for Windows SMTC plugin "
"(error %lu)", lastError);
throw init_error_t{};
}
try {
smtc = GetSmtcForHwnd(hwnd.get());
sys->smtc = smtc;
sys->timeline_props = SystemMediaTransportControlsTimelineProperties{};
} catch (const winrt::hresult_error &ex) {
msg_Err(intf, "couldn't initialize SystemMediaTransportControls "
"(hresult %x)", ex.code().value);
throw init_error_t{};
}
sys->playlist = vlc_intf_GetMainPlaylist(intf);
vlc_player_t *player = vlc_playlist_GetPlayer(sys->playlist);
vlc_player_Lock(player);
sys->player_listener = vlc_player_AddListener(player, &player_cbs, intf);
sys->playlist_listener =
vlc_playlist_AddListener(sys->playlist, &playlist_cbs, intf, true);
vlc_player_Unlock(player);
if (!sys->player_listener || !sys->playlist_listener) {
msg_Err(intf, "couldn't start Windows SMTC plugin "
"(failed to add listeners)");
throw init_error_t{};
}
// Note: Changing these dynamically seems to be unreliable.
smtc.IsPlayEnabled(true);
smtc.IsPauseEnabled(true);
smtc.IsStopEnabled(true);
smtc.IsFastForwardEnabled(true);
smtc.IsRewindEnabled(true);
smtc.IsPreviousEnabled(true);
smtc.IsNextEnabled(true);
smtc.ButtonPressed(
[intf](const SystemMediaTransportControls &sender,
const SystemMediaTransportControlsButtonPressedEventArgs &args) {
using Button = SystemMediaTransportControlsButton;
VLC_UNUSED(sender);
intf_sys_t *sys = intf->p_sys;
vlc_player_t *player = vlc_playlist_GetPlayer(sys->playlist);
vlc_player_Lock(player);
switch (args.Button()) {
case Button::Play:
if (vlc_player_IsStarted(player))
vlc_player_Resume(player);
else
vlc_player_Start(player);
break;
case Button::Pause:
vlc_player_Pause(player);
break;
case Button::Stop:
vlc_player_Stop(player);
break;
case Button::Next:
vlc_playlist_Next(sys->playlist);
break;
case Button::Previous:
vlc_playlist_Prev(sys->playlist);
break;
case Button::FastForward:
case Button::Rewind: {
int sign = args.Button() == Button::FastForward ? 1 : -1;
int jmpsz = var_InheritInteger(vlc_object_instance(intf),
"medium-jump-size");
if (jmpsz >= 0)
vlc_player_JumpTime(player, vlc_tick_from_sec(jmpsz * sign));
} break;
default:
break;
}
vlc_player_Unlock(player);
});
smtc.PlaybackPositionChangeRequested(
[sys](const SystemMediaTransportControls &sender,
const PlaybackPositionChangeRequestedEventArgs &args) {
VLC_UNUSED(sender);
auto seekPosUsec =
std::chrono::duration_cast<std::chrono::microseconds>(
args.RequestedPlaybackPosition())
.count();
vlc_player_t *player = vlc_playlist_GetPlayer(sys->playlist);
vlc_player_Lock(player);
vlc_player_SetTimeFast(player, VLC_TICK_FROM_US(seekPosUsec));
vlc_player_Unlock(player);
});
smtc.AutoRepeatModeChangeRequested(
[sys](const SystemMediaTransportControls &sender,
const winrt::Windows::Media::AutoRepeatModeChangeRequestedEventArgs &args) {
using RepeatMode =
winrt::Windows::Media::MediaPlaybackAutoRepeatMode;
VLC_UNUSED(sender);
vlc_playlist_playback_repeat repeat;
switch (args.RequestedAutoRepeatMode()) {
case RepeatMode::Track:
repeat = VLC_PLAYLIST_PLAYBACK_REPEAT_CURRENT;
break;
case RepeatMode::List:
repeat = VLC_PLAYLIST_PLAYBACK_REPEAT_ALL;
break;
case RepeatMode::None:
default:
repeat = VLC_PLAYLIST_PLAYBACK_REPEAT_NONE;
break;
}
vlc_playlist_Lock(sys->playlist);
vlc_playlist_SetPlaybackRepeat(sys->playlist, repeat);
vlc_playlist_Unlock(sys->playlist);
});
smtc.ShuffleEnabledChangeRequested(
[sys](const SystemMediaTransportControls &sender,
const winrt::Windows::Media::ShuffleEnabledChangeRequestedEventArgs &args) {
VLC_UNUSED(sender);
auto order = args.RequestedShuffleEnabled()
? VLC_PLAYLIST_PLAYBACK_ORDER_RANDOM
: VLC_PLAYLIST_PLAYBACK_ORDER_NORMAL;
vlc_playlist_Lock(sys->playlist);
vlc_playlist_SetPlaybackOrder(sys->playlist, order);
vlc_playlist_Unlock(sys->playlist);
});
// Do not enable before we have an active playable media.
smtc.IsEnabled(false);
} catch (...) {
try {
throw;
} catch (const winrt::hresult_error &ex) {
msg_Err(intf, "couldn't initialize Windows SMTC plugin "
"(hresult %x)", ex.code().value);
} catch (const std::exception &ex) {
msg_Err(intf, "couldn't initialize Windows SMTC plugin "
"(unknown exception %s)", ex.what());
} catch (init_error_t) {
// ignored
} catch (...) {
msg_Err(intf, "couldn't initialize Windows SMTC plugin "
"(unknown exception)");
}
if (sys->player_listener || sys->playlist_listener) {
vlc_player_t *player = vlc_playlist_GetPlayer(sys->playlist);
vlc_player_Lock(player);
if (sys->player_listener)
vlc_player_RemoveListener(
player, std::exchange(sys->player_listener, nullptr));
if (sys->playlist_listener)
vlc_playlist_RemoveListener(
sys->playlist,
std::exchange(sys->playlist_listener, nullptr));
vlc_player_Unlock(player);
}
sys->thread_errored = true;
SetEvent(sys->ready_event);
return nullptr;
}
// Initialization completed.
sys->hwnd = hwnd.get();
SetEvent(sys->ready_event);
{
MSG msg{};
while (int ret = GetMessageW(&msg, nullptr, 0, 0)) {
if (ret < 0)
break;
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
sys->hwnd = nullptr;
return nullptr;
}
/**
* Starts our plugin.
*/
static int Open(vlc_object_t *obj) noexcept
{
intf_thread_t *intf = (intf_thread_t *)obj;
std::unique_ptr<intf_sys_t> sys{ new (std::nothrow) intf_sys_t{} };
if (unlikely(!sys))
return VLC_ENOMEM;
/* Run the plugin thread */
intf->p_sys = sys.get();
sys->ready_event = CreateEventW(nullptr, false, false, nullptr);
if (vlc_clone(&sys->thread, PluginThread, intf)) {
msg_Err(intf, "couldn't create thread for Windows SMTC plugin");
return VLC_ENOMEM;
}
WaitForSingleObject(sys->ready_event, INFINITE);
CloseHandle(sys->ready_event);
if (sys->thread_errored) {
vlc_join(sys->thread, nullptr);
return VLC_EGENERIC;
}
intf->p_sys = sys.release();
return VLC_SUCCESS;
}
/**
* Stops the plugin.
*/
static void Close(vlc_object_t *obj) noexcept
{
intf_thread_t *intf = (intf_thread_t *)obj;
std::unique_ptr<intf_sys_t> sys{ std::exchange(intf->p_sys, nullptr) };
if (sys->hwnd)
PostMessageW(sys->hwnd, WM_MY_QUIT_MSG, 0, 0);
vlc_join(sys->thread, nullptr);
vlc_player_t *player = vlc_playlist_GetPlayer(sys->playlist);
vlc_player_Lock(player);
vlc_player_RemoveListener(player, sys->player_listener);
vlc_playlist_RemoveListener(sys->playlist, sys->playlist_listener);
vlc_player_Unlock(player);
}
/* Module descriptor */
vlc_module_begin()
set_shortname("SMTC")
set_description(N_("System Media Transport Controls (SMTC) integration"))
set_capability("interface", 0)
set_callbacks(Open, Close)
vlc_module_end ()

View File

@ -753,6 +753,14 @@ SPrefsPanel::SPrefsPanel( qt_intf_t *_p_intf, QWidget *_parent,
#ifdef _WIN32
BUTTONACT( ui.assoButton, &SPrefsPanel::assoDialog );
if( module_exists( "win_smtc" ) )
{
CONFIG_BOOL( "smtc", enableSMTC );
}
else
{
ui.enableSMTC->hide();
}
#else
ui.osGroupBox->hide();
#endif

View File

@ -725,6 +725,13 @@ with no client-side decoration.</string>
</property>
</spacer>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="enableSMTC">
<property name="text">
<string>Enable System Media Transport Controls (SMTC)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -1167,6 +1167,10 @@ static const char *const clock_sources_text[] = {
#define DBUS_LONGTEXT N_("Allow other applications to control VLC " \
"using the D-Bus MPRIS protocol.")
#define SMTC_TEXT N_("Integrate with Windows SMTC")
#define SMTC_LONGTEXT N_("Integrate VLC with the System Media Transport " \
"Controls to enable system-wide media control UIs and hotkeys.")
/*****************************************************************************
* Playlist
****************************************************************************/
@ -2212,6 +2216,10 @@ vlc_module_begin ()
add_bool ( "stats", true, STATS_TEXT, STATS_LONGTEXT )
#if defined(_WIN32) && !defined(VLC_WINSTORE_APP)
add_bool( "smtc", true, SMTC_TEXT, SMTC_LONGTEXT )
#endif
set_subcategory( SUBCAT_INTERFACE_MAIN )
add_module_cat("intf", SUBCAT_INTERFACE_MAIN, NULL,
INTF_TEXT, INTF_LONGTEXT)

View File

@ -163,6 +163,9 @@ void system_Configure( libvlc_int_t *p_this, int i_argc, const char *const ppsz_
exit( 0 );
}
}
if( var_InheritBool( p_this, "smtc" ) )
libvlc_InternalAddIntf( p_this, "win_smtc,none" );
#endif
}