Files
Velix-MPV-Core/osdep/w32_register.c
T
Kacper Michajłow 8c6680a868 osdep/w32_register: add a DVD-Audio AutoPlay handler
Register mpv for the PlayDVDAudioOnArrival event, like the existing
CD-Audio/DVD/Blu-ray handlers.
2026-08-26 01:47:41 +02:00

503 lines
20 KiB
C

/*
* 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 "w32_register.h"
#include <windows.h>
#include <knownfolders.h>
#include <pathcch.h>
#include <shlobj.h>
#include <shlwapi.h>
#include <common/msg.h>
#include <options/options.h>
#include <osdep/io.h>
#include <player/core.h>
#include <stream/stream.h>
#include <version.h>
#include "windows_utils.h"
#define MPV_NAME L"mpv"
#define MPV_FRIENDLY_NAME L"mpv media player"
#define KEY_AUTOPLAY L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\AutoplayHandlers"
#define KEY_MPV_APP L"Software\\Classes\\Applications\\" MPV_NAME L".exe"
#define KEY_MPV_APP_PATHS L"Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\"
#define KEY_MPV_APP_PATH(suffix) KEY_MPV_APP_PATHS MPV_NAME suffix
#define KEY_MPV_CAPABILITIES_APP L"Software\\Clients\\Media\\" MPV_NAME
#define KEY_MPV_CAPABILITIES KEY_MPV_CAPABILITIES_APP L"\\Capabilities"
#define KEY_MPV_UNINSTALL L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" MPV_NAME
#define MPV_PROG_ID_PREFIX L"io.mpv."
#define MPV_PROG_ID(h) MPV_PROG_ID_PREFIX h
#define KEY_MPV_PROG_ID(h) L"Software\\Classes\\" MPV_PROG_ID(h)
struct w32_register_opts {
bool register_opt;
char *rpath;
bool unregister;
};
#define OPT_BASE_STRUCT struct w32_register_opts
const struct m_sub_options w32_register_conf = {
.opts = (const struct m_option[]) {
{"register", OPT_BOOL(register_opt)},
{"register-rpath", OPT_STRING(rpath)},
{"unregister", OPT_BOOL(unregister)},
{0}
},
.size = sizeof(struct w32_register_opts)
};
static bool is_admin(void)
{
PSID sid;
SID_IDENTIFIER_AUTHORITY identifier_authority = SECURITY_NT_AUTHORITY;
if (!AllocateAndInitializeSid(&identifier_authority, 2, SECURITY_BUILTIN_DOMAIN_RID,
DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &sid))
{
return false;
}
BOOL member = false;
CheckTokenMembership(NULL, sid, &member);
if (sid)
FreeSid(sid);
return member;
}
static void create_shortcut(struct mp_log *log, const wchar_t *target,
const wchar_t *shortcut)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
if (FAILED(hr)) {
mp_msg(log, MSGL_ERR, "Failed to initialize COM: %s\n",
mp_HRESULT_to_str(hr));
return;
}
IShellLinkW *shell_link = NULL;
IPersistFile *persist_file = NULL;
hr = CoCreateInstance(&CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
&IID_IShellLinkW, (void **)&shell_link);
if (FAILED(hr)) {
mp_msg(log, MSGL_ERR, "Failed to create IShellLinkW instance: %s\n",
mp_HRESULT_to_str(hr));
goto done;
}
hr = IShellLinkW_SetPath(shell_link, target);
if (FAILED(hr)) {
mp_msg(log, MSGL_ERR, "Failed to set target path: %s\n",
mp_HRESULT_to_str(hr));
goto done;
}
hr = IShellLinkW_QueryInterface(shell_link, &IID_IPersistFile,
(LPVOID *)&persist_file);
if (FAILED(hr)) {
mp_msg(log, MSGL_ERR, "Failed to get IPersistFile interface: %s\n",
mp_HRESULT_to_str(hr));
goto done;
}
hr = IPersistFile_Save(persist_file, shortcut, TRUE);
if (FAILED(hr)) {
mp_msg(log, MSGL_ERR, "Failed to save shortcut: %s\n",
mp_HRESULT_to_str(hr));
goto done;
}
mp_msg(log, MSGL_V, "Shortcut created: '%ls' -> '%ls'\n", shortcut, target);
done:
SAFE_RELEASE(persist_file);
SAFE_RELEASE(shell_link);
CoUninitialize();
}
static void reg_add(struct mp_log *log, HKEY key, LPCWSTR sub_key, LPCWSTR name,
DWORD type, const BYTE *data, DWORD size)
{
HKEY sub_key_handle;
if (RegCreateKeyExW(key, sub_key, 0, NULL, REG_OPTION_NON_VOLATILE,
KEY_WRITE, NULL, &sub_key_handle,
NULL) != ERROR_SUCCESS)
{
mp_msg(log, MSGL_ERR, "Failed to create registry key\n");
return;
}
if (RegSetValueExW(sub_key_handle, name, 0, type, data, size) != ERROR_SUCCESS)
mp_msg(log, MSGL_ERR, "Failed to set registry value\n");
RegCloseKey(sub_key_handle);
}
static void reg_add_str(struct mp_log *log, HKEY key, LPCWSTR sub_key,
LPCWSTR name, LPCWSTR value)
{
const wchar_t *root = key == HKEY_LOCAL_MACHINE ? L"HKEY_LOCAL_MACHINE"
: L"HKEY_CURRENT_USER";
mp_msg(log, MSGL_V, "Adding registry key: %ls\\%ls\\%ls -> '%ls'\n", root,
sub_key, name ? name : L"(Default)", value);
reg_add(log, key, sub_key, name, REG_SZ, (const BYTE *)value,
(wcslen(value) + 1) * sizeof(WCHAR));
}
static void reg_add_dwr(struct mp_log *log, HKEY key, LPCWSTR sub_key,
LPCWSTR name, DWORD value)
{
const wchar_t *root = key == HKEY_LOCAL_MACHINE ? L"HKEY_LOCAL_MACHINE"
: L"HKEY_CURRENT_USER";
mp_msg(log, MSGL_V, "Adding registry key: %ls\\%ls\\%ls -> %lu\n", root,
sub_key, name ? name : L"(Default)", value);
reg_add(log, key, sub_key, name, REG_DWORD, (const BYTE *)&value,
sizeof(DWORD));
}
static void reg_del(struct mp_log *log, HKEY key, LPCWSTR sub_key,
LPCWSTR value)
{
const wchar_t *root = key == HKEY_LOCAL_MACHINE ? L"HKEY_LOCAL_MACHINE"
: L"HKEY_CURRENT_USER";
const char *name = value ? "value" : "key";
mp_msg(log, MSGL_V, "Removing registry %s: %ls\\%ls%ls%ls\n", name, root,
sub_key, value ? L"\\" : L"", value ? value : L"");
LSTATUS status = value ? RegDeleteKeyValueW(key, sub_key, value)
: RegDeleteTreeW(key, sub_key);
if (status != ERROR_SUCCESS) {
mp_msg(log, status == ERROR_FILE_NOT_FOUND ? MSGL_DEBUG : MSGL_ERR,
"Failed to delete registry %s: %ls\\%ls%ls%ls -> %s\n", name,
root, sub_key, value ? L"\\" : L"", value ? value : L"",
mp_HRESULT_to_str(HRESULT_FROM_WIN32(status)));
}
}
static uint64_t get_file_size(const wchar_t *path)
{
WIN32_FILE_ATTRIBUTE_DATA fad;
if (!GetFileAttributesExW(path, GetFileExInfoStandard, &fad))
return 0;
return ((uint64_t)fad.nFileSizeHigh << 32) | fad.nFileSizeLow;
}
static wchar_t *w32_get_shortcut_path(struct mp_log *log)
{
LPWSTR shortcut_path = NULL;
LPWSTR programs_path = NULL;
if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_Programs, 0, NULL, &programs_path))) {
PathAllocCombine(programs_path, MPV_NAME L".lnk",
PATHCCH_ALLOW_LONG_PATHS, &shortcut_path);
} else {
mp_msg(log, MSGL_ERR, "Failed to get Programs folder path.\n");
}
CoTaskMemFree(programs_path);
return shortcut_path;
}
#define REGISTER_AUTOPLAY_HANDLER(media, event, action) \
reg_add_str(log, root, KEY_MPV_PROG_ID(media) L"\\shell", NULL, L"open"); \
reg_add_str(log, root, KEY_MPV_PROG_ID(media) L"\\shell\\open\\command", \
NULL, L"mpv.exe " media L":// --" media L"-device=%L"); \
reg_add_str(log, root, \
KEY_AUTOPLAY L"\\Handlers\\MpvPlay" event "OnArrival", \
L"Action", L"" action); \
reg_add_str(log, root, \
KEY_AUTOPLAY L"\\Handlers\\MpvPlay" event "OnArrival", \
L"DefaultIcon", icon_path); \
reg_add_str(log, root, \
KEY_AUTOPLAY L"\\Handlers\\MpvPlay" event "OnArrival", \
L"InvokeProgID", MPV_PROG_ID(media)); \
reg_add_str(log, root, \
KEY_AUTOPLAY L"\\Handlers\\MpvPlay" event "OnArrival", \
L"InvokeVerb", L"open"); \
reg_add_str(log, root, \
KEY_AUTOPLAY L"\\Handlers\\MpvPlay" event "OnArrival", \
L"Provider", MPV_FRIENDLY_NAME); \
reg_add_str(log, root, \
KEY_AUTOPLAY L"\\EventHandlers\\Play" event "OnArrival", \
L"MpvPlay" event "OnArrival", L"");
static void w32_register(struct MPContext *mpctx)
{
void *tmp = talloc_new(NULL);
struct mp_log *log = mp_log_new(tmp, mpctx->log, "win32");
LPWSTR mpv_path = talloc_array(tmp, wchar_t, MP_PATH_MAX);
DWORD mpv_path_len = GetModuleFileNameW(NULL, mpv_path, MP_PATH_MAX);
if (!mpv_path_len || mpv_path_len == MP_PATH_MAX) {
mp_msg(log, MSGL_ERR, "Failed to get mpv path.\n");
return;
}
LPWSTR icon_path = talloc_array(tmp, wchar_t, mpv_path_len + 3);
memcpy(icon_path, mpv_path, mpv_path_len * sizeof(wchar_t));
icon_path[mpv_path_len + 0] = L',';
icon_path[mpv_path_len + 1] = L'0';
icon_path[mpv_path_len + 2] = L'\0';
HKEY root = is_admin() ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER;
mp_msg(log, MSGL_INFO, "Registering mpv for %s...\n",
root == HKEY_LOCAL_MACHINE ? "all users" : "the current user");
// Register mpv as an application
// <https://learn.microsoft.com/windows/win32/shell/app-registration>
reg_add_str(log, root, KEY_MPV_APP_PATH(".exe"), NULL, mpv_path);
reg_add_dwr(log, root, KEY_MPV_APP_PATH(".exe"), L"UseUrl", 1);
char *rpath = mpctx->opts->w32_register_opts->rpath;
if (rpath) {
wchar_t *rpath_w = mp_from_utf8(tmp, rpath);
reg_add_str(log, root, KEY_MPV_APP_PATH(".exe"), L"Path", rpath_w);
reg_add_str(log, root, KEY_MPV_APP_PATH(".com"), L"Path", rpath_w);
}
// Name of the "Open With" entry in the context menu
// I prefer a simple "mpv" here, so let's skip this.
// reg_add_str(log, root, KEY_MPV_APP, L"FriendlyAppName", MPV_FRIENDLY_NAME);
// Add shell open command
reg_add_str(log, root, KEY_MPV_APP L"\\shell", NULL, L"open");
reg_add_str(log, root, KEY_MPV_APP L"\\shell\\open\\command", NULL,
L"mpv.exe -- \"%L\"");
// Register mpv capabilities and handlers
// Note that we do not register any extensions or protocols directly,
// as mpv does not own any. It is simply a media player that can open them.
// Windows will prompt the user to choose the default application for each
// file type encountered.
// Register only a single URL and file handler; there is no need to
// duplicate this for each supported protocol or file type.
// Register URL handler
reg_add_str(log, root, KEY_MPV_PROG_ID("url"), NULL, L"URL:mpv");
reg_add_dwr(log, root, KEY_MPV_PROG_ID("url"), L"EditFlags", FTA_Show);
reg_add_str(log, root, KEY_MPV_PROG_ID("url"), L"FriendlyTypeName",
L"mpv URL handler");
reg_add_str(log, root, KEY_MPV_PROG_ID("url") L"\\shell", NULL, L"open");
reg_add_str(log, root, KEY_MPV_PROG_ID("url") L"\\shell\\open\\command",
NULL, L"mpv.exe -- \"%L\"");
reg_add_str(log, root, KEY_MPV_PROG_ID("url") L"\\shell\\open\\ddeexec",
NULL, L"");
// Register URL protocols
char **safe_protocols = talloc_steal(tmp, stream_get_proto_list(true));
mp_assert(safe_protocols);
char *protocols_str = NULL;
for (int i = 0; safe_protocols[i]; ++i) {
mp_require(strlen(safe_protocols[i]) < 41);
char *protocol_key = mp_tprintf(61, "Software\\Classes\\%s", safe_protocols[i]);
wchar_t *protocol_key_w = mp_from_utf8(tmp, protocol_key);
HKEY key;
LRESULT res = RegOpenKeyExW(root, protocol_key_w, 0, KEY_READ, &key);
if (res == ERROR_FILE_NOT_FOUND) {
// Add protocol definition only if it doesn't already exist
char *protocol_name = mp_tprintf(61, "URL:%s", safe_protocols[i]);
reg_add_str(log, root, protocol_key_w, NULL, mp_from_utf8(tmp, protocol_name));
reg_add_str(log, root, protocol_key_w, L"URL Protocol", L"");
reg_add_dwr(log, root, protocol_key_w, L"EditFlags", FTA_Show);
} else if (res == ERROR_SUCCESS) {
RegCloseKey(key);
}
reg_add_str(log, root, KEY_MPV_CAPABILITIES L"\\URLAssociations",
mp_from_utf8(tmp, safe_protocols[i]),
MPV_PROG_ID("url"));
if (!protocols_str) {
protocols_str = talloc_strdup(tmp, safe_protocols[i]);
} else {
protocols_str = talloc_asprintf_append(protocols_str, ":%s",
safe_protocols[i]);
}
}
if (protocols_str) {
reg_add_str(log, root, KEY_MPV_APP_PATH(".exe"), L"SupportedProtocols",
mp_from_utf8(tmp, protocols_str));
}
// Register file handler
reg_add_str(log, root, KEY_MPV_PROG_ID("file"), NULL, L"mpv");
reg_add_str(log, root, KEY_MPV_PROG_ID("file"), L"FriendlyTypeName", L"mpv Media File");
reg_add_dwr(log, root, KEY_MPV_PROG_ID("file"), L"EditFlags", FTA_OpenIsSafe);
reg_add_str(log, root, KEY_MPV_PROG_ID("file") L"\\shell", NULL, L"open");
reg_add_str(log, root, KEY_MPV_PROG_ID("file") L"\\shell\\open\\command",
NULL, L"mpv.exe -- \"%L\"");
// Register file associations
char **exts_groups[] = {
#if HAVE_LIBARCHIVE
mpctx->opts->archive_exts,
#endif
mpctx->opts->audio_exts,
mpctx->opts->image_exts,
mpctx->opts->playlist_exts,
mpctx->opts->video_exts,
};
for (int i = 0; i < MP_ARRAY_SIZE(exts_groups); ++i) {
char **exts = exts_groups[i];
for (int j = 0; exts && exts[j]; ++j) {
wchar_t *ext = mp_from_utf8(tmp, mp_tprintf(10, ".%s", exts[j]));
reg_add_str(log, root, KEY_MPV_APP L"\\SupportedTypes", ext, L"");
reg_add_str(log, root, KEY_MPV_CAPABILITIES L"\\FileAssociations",
ext, MPV_PROG_ID("file"));
}
}
// Connect above handlers to the application
reg_add_str(log, root, KEY_MPV_CAPABILITIES, L"ApplicationName", MPV_NAME);
reg_add_str(log, root, KEY_MPV_CAPABILITIES, L"ApplicationDescription", MPV_FRIENDLY_NAME);
// Register the application
reg_add_str(log, root, L"Software\\RegisteredApplications", MPV_NAME, KEY_MPV_CAPABILITIES);
// <https://learn.microsoft.com/windows/win32/shell/how-to-register-an-event-handler>
#if HAVE_CDDA
// Register CDDA handler
REGISTER_AUTOPLAY_HANDLER("cdda", "CDAudio", "Play CD-Audio");
#endif
#if HAVE_DVDNAV
// Register DVD handler
REGISTER_AUTOPLAY_HANDLER("dvd", "DVDMovie", "Play DVD movie");
#endif
#if HAVE_DVDA
// Register DVD-Audio handler
REGISTER_AUTOPLAY_HANDLER("dvda", "DVDAudio", "Play DVD-Audio");
#endif
#if HAVE_LIBBLURAY
// Register Blu-ray handler
REGISTER_AUTOPLAY_HANDLER("bluray", "BluRay", "Play Blu-ray movie");
#endif
// Create a shortcut in the Start Menu
// Note that this is required for SystemMediaTransportControls to detect the
// app correctly. Which is quite stupid, but it's the only way to make it work.
wchar_t *shortcut_path = w32_get_shortcut_path(log);
if (shortcut_path) {
create_shortcut(log, mpv_path, shortcut_path);
LocalFree(shortcut_path);
}
// Register uninstaller
// <https://learn.microsoft.com/windows/win32/msi/uninstall-registry-key>
reg_add_str(log, root, KEY_MPV_UNINSTALL, L"DisplayName", L"mpv media player");
reg_add_str(log, root, KEY_MPV_UNINSTALL, L"DisplayIcon", icon_path);
reg_add_str(log, root, KEY_MPV_UNINSTALL, L"DisplayVersion", L"" VERSION);
reg_add_str(log, root, KEY_MPV_UNINSTALL, L"Version", L"" VERSION);
reg_add_dwr(log, root, KEY_MPV_UNINSTALL, L"Language", 1033);
reg_add_str(log, root, KEY_MPV_UNINSTALL, L"UninstallString", L"mpv.exe --no-config --unregister");
reg_add_dwr(log, root, KEY_MPV_UNINSTALL, L"NoModify", 1);
reg_add_dwr(log, root, KEY_MPV_UNINSTALL, L"NoRepair", 1);
reg_add_str(log, root, KEY_MPV_UNINSTALL, L"Publisher", L"mpv");
reg_add_str(log, root, KEY_MPV_UNINSTALL, L"HelpLink", L"https://mpv.io/manual");
reg_add_str(log, root, KEY_MPV_UNINSTALL, L"Readme", L"https://mpv.io/community");
reg_add_str(log, root, KEY_MPV_UNINSTALL, L"URLInfoAbout", L"https://mpv.io");
reg_add_str(log, root, KEY_MPV_UNINSTALL, L"URLUpdateInfo", L"https://mpv.io/installation");
uint64_t size = get_file_size(mpv_path);
if (size)
reg_add_dwr(log, root, KEY_MPV_UNINSTALL, L"EstimatedSize", size / 1024);
if (SUCCEEDED(PathCchRemoveFileSpec(mpv_path, MP_PATH_MAX)))
reg_add_str(log, root, KEY_MPV_UNINSTALL, L"InstallLocation", mpv_path);
mp_msg(log, MSGL_INFO, "mpv has been successfully registered.\n");
mp_msg(log, MSGL_INFO, "Note: The mpv binary has not been copied or moved. Please keep it in the current directory.\n");
mp_msg(log, MSGL_INFO, "If you move it, simply run this process again to re-register.\n");
talloc_free(tmp);
}
static void w32_unregister(struct MPContext *mpctx)
{
struct mp_log *log = mp_log_new(NULL, mpctx->log, "win32");
HKEY root = is_admin() ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER;
mp_msg(log, MSGL_INFO, "Unregistering mpv for %s...\n",
root == HKEY_LOCAL_MACHINE ? "all users" : "the current user");
reg_del(log, root, KEY_MPV_APP_PATH(".exe"), NULL);
reg_del(log, root, KEY_MPV_APP_PATH(".com"), NULL);
reg_del(log, root, KEY_MPV_APP, NULL);
reg_del(log, root, KEY_MPV_CAPABILITIES_APP, NULL);
reg_del(log, root, L"Software\\RegisteredApplications", MPV_NAME);
reg_del(log, root, KEY_MPV_PROG_ID("bluray"), NULL);
reg_del(log, root, KEY_MPV_PROG_ID("cdda"), NULL);
reg_del(log, root, KEY_MPV_PROG_ID("dvd"), NULL);
reg_del(log, root, KEY_MPV_PROG_ID("dvda"), NULL);
reg_del(log, root, KEY_MPV_PROG_ID("file"), NULL);
reg_del(log, root, KEY_MPV_PROG_ID("url"), NULL);
reg_del(log, root, KEY_AUTOPLAY L"\\Handlers\\MpvPlayCDAudioOnArrival", NULL);
reg_del(log, root, KEY_AUTOPLAY L"\\EventHandlers\\PlayCDAudioOnArrival",
L"MpvPlayCDAudioOnArrival");
reg_del(log, root, KEY_AUTOPLAY L"\\Handlers\\MpvPlayDVDMovieOnArrival", NULL);
reg_del(log, root, KEY_AUTOPLAY L"\\EventHandlers\\PlayDVDMovieOnArrival",
L"MpvPlayDVDMovieOnArrival");
reg_del(log, root, KEY_AUTOPLAY L"\\Handlers\\MpvPlayDVDAudioOnArrival", NULL);
reg_del(log, root, KEY_AUTOPLAY L"\\EventHandlers\\PlayDVDAudioOnArrival",
L"MpvPlayDVDAudioOnArrival");
reg_del(log, root, KEY_AUTOPLAY L"\\Handlers\\MpvPlayBluRayOnArrival", NULL);
reg_del(log, root, KEY_AUTOPLAY L"\\EventHandlers\\PlayBluRayOnArrival",
L"MpvPlayBluRayOnArrival");
wchar_t *shortcut_path = w32_get_shortcut_path(log);
if (shortcut_path) {
mp_msg(log, MSGL_V, "Removing shortcut: '%ls'\n", shortcut_path);
if (!DeleteFileW(shortcut_path)) {
DWORD err = GetLastError();
mp_msg(log, err == ERROR_FILE_NOT_FOUND ? MSGL_DEBUG : MSGL_ERR,
"Failed to delete shortcut: '%ls' -> %s\n", shortcut_path,
mp_LastError_to_str());
}
LocalFree(shortcut_path);
}
reg_del(log, root, KEY_MPV_UNINSTALL, NULL);
mp_msg(log, MSGL_INFO, "mpv has been successfully unregistered.\n");
mp_msg(log, MSGL_INFO, "You may now safely delete the mpv binary if desired.\n");
talloc_free(log);
}
bool mp_w32_handle_register(struct MPContext *mpctx)
{
struct w32_register_opts *opts = mpctx->opts->w32_register_opts;
if (!opts->register_opt && !opts->unregister)
return false;
if (opts->register_opt) {
w32_register(mpctx);
} else if (opts->unregister) {
w32_unregister(mpctx);
}
return true;
}