ohmu
·
2026-10-15
1extern "C" {
2#include "media.h"
3#include "common/debug.h"
4#include "common/time.h"
5#include "common/windows/unicode.h"
6#include "common/windows/com.h"
7}
8
9#if FF_HAVE_WINRT
10 #include "common/windows/winrt.hpp"
11
12 #include <robuffer.h>
13
14 #include <shobjidl.h>
15 #include <shlobj.h>
16 #include <knownfolders.h>
17 #include <shlwapi.h>
18
19 #include <winrt/Windows.ApplicationModel.h>
20 #include <winrt/Windows.Foundation.h>
21 #include <winrt/Windows.Media.Control.h>
22 #include <winrt/Windows.Storage.Streams.h>
23
24static HRESULT ffSaveThumbnailToTempPath(
25 abi_t<winrt::Windows::Storage::Streams::IRandomAccessStreamReference>* thumbnail,
26 FFstrbuf* destination) {
27 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IRandomAccessStreamWithContentType>* contentStream = nullptr;
28 HRESULT hr = ffRunAndWait<winrt::Windows::Storage::Streams::IRandomAccessStreamWithContentType>([=](void** result) {
29 return thumbnail->OpenReadAsync(result);
30 },
31 &contentStream);
32 if (FAILED(hr) || !contentStream) {
33 return FAILED(hr) ? hr : E_FAIL;
34 }
35
36 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IRandomAccessStream>* randomAccessStream = nullptr;
37 hr = ffQueryInterface<winrt::Windows::Storage::Streams::IRandomAccessStream>(contentStream, &randomAccessStream);
38 if (FAILED(hr)) {
39 return hr;
40 }
41
42 UINT64 size = 0;
43 hr = randomAccessStream->get_Size(&size);
44 if (FAILED(hr) || size == 0) {
45 return FAILED(hr) ? hr : S_FALSE;
46 }
47
48 if (size > 0xFFFFFFFFu) {
49 return HRESULT_FROM_WIN32(ERROR_FILE_TOO_LARGE);
50 }
51
52 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IBufferFactory>* bufferFactory = nullptr;
53 hr = ffGetActivationFactory<winrt::Windows::Storage::Streams::IBufferFactory>(L"Windows.Storage.Streams.Buffer", &bufferFactory);
54 if (FAILED(hr)) {
55 return hr;
56 }
57
58 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IBuffer>* buffer = nullptr;
59 hr = bufferFactory->Create((UINT32) size, reinterpret_cast<void**>(&buffer));
60 if (FAILED(hr) || !buffer) {
61 return FAILED(hr) ? hr : E_FAIL;
62 }
63
64 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IInputStream>* inputStream = nullptr;
65 hr = ffQueryInterface<winrt::Windows::Storage::Streams::IInputStream>(contentStream, &inputStream);
66 if (FAILED(hr)) {
67 return hr;
68 }
69
70 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IBuffer>* readBuffer = nullptr;
71 hr = ffRunAndWait2<winrt::Windows::Storage::Streams::IBuffer>([=](void** result) {
72 return inputStream->ReadAsync(buffer, (uint32_t) size, (uint32_t) winrt::Windows::Storage::Streams::InputStreamOptions::None, result);
73 },
74 &readBuffer);
75 if (FAILED(hr) || !readBuffer) {
76 return FAILED(hr) ? hr : E_FAIL;
77 }
78
79 UINT32 length = 0;
80 hr = readBuffer->get_Length(&length);
81 if (FAILED(hr) || length == 0) {
82 return FAILED(hr) ? hr : S_FALSE;
83 }
84
85 FF_AUTO_RELEASE_COM_OBJECT Windows::Storage::Streams::IBufferByteAccess* byteAccess = nullptr;
86 hr = readBuffer->QueryInterface(IID_PPV_ARGS(&byteAccess));
87 if (FAILED(hr)) {
88 return hr;
89 }
90
91 byte* bytes = nullptr;
92 hr = byteAccess->Buffer(&bytes);
93 if (FAILED(hr) || !bytes) {
94 return FAILED(hr) ? hr : E_FAIL;
95 }
96
97 wchar_t tempDirectory[MAX_PATH];
98 DWORD tempLength = GetTempPathW(MAX_PATH, tempDirectory);
99 if (tempLength == 0 || tempLength >= MAX_PATH) {
100 return HRESULT_FROM_WIN32(GetLastError());
101 }
102
103 wchar_t tempFilePath[MAX_PATH];
104 if (!GetTempFileNameW(tempDirectory, L"fft", 0, tempFilePath)) {
105 return HRESULT_FROM_WIN32(GetLastError());
106 }
107
108 HANDLE file = CreateFileW(tempFilePath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
109 if (file == INVALID_HANDLE_VALUE) {
110 DWORD writeError = GetLastError();
111 DeleteFileW(tempFilePath);
112 return HRESULT_FROM_WIN32(writeError);
113 }
114
115 DWORD written = 0;
116 BOOL writtenOk = WriteFile(file, bytes, length, &written, nullptr);
117 NtClose(file);
118 file = nullptr;
119
120 if (!writtenOk || written != length) {
121 DWORD writeError = GetLastError();
122 DeleteFileW(tempFilePath);
123 return HRESULT_FROM_WIN32(writtenOk ? ERROR_WRITE_FAULT : writeError);
124 }
125
126 ffStrbufSetWS(destination, tempFilePath);
127 return S_OK;
128}
129
130// Path 1: `Windows.ApplicationModel.AppInfo`, which answers straight from the package manifest.
131// It only knows packaged (MSIX) apps, but for those it is the cheap source: resolving a
132// `PackageFamilyName!AppId` AppUserModelId costs about half of what the shell needs, because the
133// shell has to look the application up in the package graph. A *failed* lookup still costs a
134// couple of milliseconds of WinRT class activation, so this is only worth trying on names that
135// are actually packaged -- the caller gates it, see `resolveAppUserModelId`.
136static bool resolvePackagedAppUserModelId(const wchar_t* aumid, FFstrbuf* result) {
137 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::ApplicationModel::IAppInfoStatics>* statics = nullptr;
138 if (FAILED(ffGetActivationFactory<winrt::Windows::ApplicationModel::IAppInfoStatics>(L"Windows.ApplicationModel.AppInfo", &statics)) || !statics) {
139 return false;
140 }
141
142 HSTRING_HEADER header;
143 HSTRING aumidString;
144 if (FAILED(WindowsCreateStringReference(aumid, (UINT32) wcslen(aumid), &header, &aumidString))) {
145 return false;
146 }
147
148 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::ApplicationModel::IAppInfo>* appInfo = nullptr;
149 if (FAILED(statics->GetFromAppUserModelId(reinterpret_cast<void*>(aumidString), reinterpret_cast<void**>(&appInfo))) || !appInfo) {
150 return false;
151 }
152
153 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::ApplicationModel::IAppDisplayInfo>* displayInfo = nullptr;
154 if (FAILED(appInfo->get_DisplayInfo(reinterpret_cast<void**>(&displayInfo))) || !displayInfo) {
155 return false;
156 }
157
158 [[gnu::cleanup(ffDeleteHstring)]] HSTRING displayName = nullptr;
159 if (FAILED(displayInfo->get_DisplayName(reinterpret_cast<void**>(&displayName))) || !displayName) {
160 return false;
161 }
162
163 ffStrbufSetHstring(result, displayName);
164 return result->length > 0;
165}
166
167// Path 2: the Start menu's `AppsFolder` namespace, i.e. the (display name, AppUserModelId) table
168// Windows itself displays. It covers packaged and unpackaged apps alike, and inside that namespace
169// an item's parsing name *is* its AppUserModelId, so `ParseDisplayName` finds a child directly --
170// going through `SHCreateItemFromParsingName(L"shell:AppsFolder\\" + aumid)` costs several times
171// more, because the `shell:` protocol has to be activated first.
172static bool resolveAppsFolderAppUserModelId(const wchar_t* aumid, FFstrbuf* result) {
173 const size_t aumidLength = wcslen(aumid);
174 wchar_t name[512];
175 if (aumidLength == 0 || aumidLength >= ARRAY_SIZE(name)) {
176 return false;
177 }
178 wmemcpy(name, aumid, aumidLength + 1); // `ParseDisplayName` wants a mutable string
179
180 FF_AUTO_RELEASE_COM_OBJECT IShellItem* folder = nullptr;
181 if (FAILED(SHGetKnownFolderItem(FOLDERID_AppsFolder, KF_FLAG_DEFAULT, nullptr, IID_PPV_ARGS(&folder))) || !folder) {
182 return false;
183 }
184
185 FF_AUTO_RELEASE_COM_OBJECT IShellFolder* shellFolder = nullptr;
186 if (FAILED(folder->BindToHandler(nullptr, BHID_SFObject, IID_PPV_ARGS(&shellFolder))) || !shellFolder) {
187 return false;
188 }
189
190 LPITEMIDLIST child = nullptr;
191 ULONG attributes = 0;
192 if (FAILED(shellFolder->ParseDisplayName(nullptr, nullptr, name, &attributes, &child, nullptr)) || !child) {
193 return false;
194 }
195
196 bool success = false;
197 STRRET strret = {};
198 if (SUCCEEDED(shellFolder->GetDisplayNameOf(child, SHGDN_INFOLDER, &strret))) {
199 wchar_t displayName[ARRAY_SIZE(name)];
200 if (SUCCEEDED(StrRetToBufW(&strret, child, displayName, ARRAY_SIZE(displayName)))) {
201 ffStrbufSetWS(result, displayName);
202 success = result->length > 0;
203 }
204 }
205
206 CoTaskMemFree(child);
207 return success;
208}
209
210// The two sources above agree on every packaged app, and the shell alone covers everything else,
211// so picking between them is purely a matter of cost. `PackageFamilyName!AppId` is the shape
212// `AppInfo` can serve, and the `!` is what distinguishes it from an unpackaged AppUserModelId
213// (`Chrome`, `PotPlayerMini64.exe`, a derived path). The shell stays the fallback either way, so a
214// packaged-looking name that `AppInfo` rejects is still resolved.
215static bool resolveAppUserModelId(const wchar_t* aumid, FFstrbuf* result) {
216 if (wcschr(aumid, L'!') && resolvePackagedAppUserModelId(aumid, result)) {
217 return true;
218 }
219
220 return resolveAppsFolderAppUserModelId(aumid, result);
221}
222
223static const char* getMedia(FFMediaResult* result, bool saveCover) {
224 const char* error = ffInitCom();
225 if (error) {
226 return error;
227 }
228
229 do {
230 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManagerStatics>* managerStatics = nullptr;
231 HRESULT hr = ffGetActivationFactory<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManagerStatics>(L"Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager", &managerStatics);
232 if (FAILED(hr) || !managerStatics) {
233 error = "winrt: RoGetActivationFactory(GlobalSystemMediaTransportControlsSessionManager) failed";
234 break;
235 }
236
237 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManager>* manager = nullptr;
238 hr = ffRunAndWait<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManager>([=](void** result) {
239 return managerStatics->RequestAsync(result);
240 },
241 &manager);
242 if (FAILED(hr) || !manager) {
243 error = "winrt: RequestAsync().GetResults() failed";
244 break;
245 }
246
247 [[gnu::cleanup(ffDeleteHstring)]] HSTRING playerId = nullptr;
248
249 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSession>* session = nullptr;
250 if (instance.config.general.playerName.length) {
251 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Foundation::Collections::IVectorView<winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSession>>* sessions = nullptr;
252 hr = manager->GetSessions(reinterpret_cast<void**>(&sessions));
253 if (FAILED(hr) || !sessions) {
254 error = "winrt: GetSessions() failed";
255 break;
256 }
257 uint32_t sessionCount = 0;
258 hr = sessions->get_Size(&sessionCount);
259 if (FAILED(hr)) {
260 error = "winrt: GetSessions().get_Size() failed";
261 break;
262 }
263 for (uint32_t i = 0; i < sessionCount; i++) {
264 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSession>* currentSession = nullptr;
265 hr = sessions->GetAt(i, reinterpret_cast<void**>(¤tSession));
266 if (FAILED(hr) || !currentSession) {
267 continue;
268 }
269
270 hr = currentSession->get_SourceAppUserModelId(reinterpret_cast<void**>(&playerId));
271 if (FAILED(hr) || !playerId) {
272 continue;
273 }
274
275 ffStrbufSetHstring(&result->playerId, playerId);
276
277 if (ffStrbufContainIgnCase(&result->playerId, &instance.config.general.playerName)) {
278 session = currentSession;
279 currentSession = nullptr; // Don't release the session object
280 break;
281 }
282 ffDeleteHstring(&playerId);
283 ffStrbufClear(&result->playerId);
284 }
285
286 if (!session) {
287 error = "winrt: No media session found with the specified player name";
288 break;
289 }
290 } else {
291 hr = manager->GetCurrentSession(reinterpret_cast<void**>(&session));
292
293 if (FAILED(hr) || !session) {
294 error = "winrt: GetCurrentSession() failed";
295 break;
296 }
297
298 hr = session->get_SourceAppUserModelId(reinterpret_cast<void**>(&playerId));
299 if (FAILED(hr)) {
300 error = "winrt: get_SourceAppUserModelId() failed";
301 break;
302 }
303
304 ffStrbufSetHstring(&result->playerId, playerId);
305 }
306
307 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionMediaProperties>* mediaProps = nullptr;
308 hr = ffRunAndWait<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionMediaProperties>([=](void** result) {
309 return session->TryGetMediaPropertiesAsync(result);
310 },
311 &mediaProps);
312 if (FAILED(hr) || !mediaProps) {
313 error = "winrt: TryGetMediaPropertiesAsync().GetResults() failed";
314 break;
315 }
316
317 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionPlaybackInfo>* playbackInfo = nullptr;
318 hr = session->GetPlaybackInfo(reinterpret_cast<void**>(&playbackInfo));
319 bool isPlaying = false;
320 double playbackRate = 1.0;
321 if (SUCCEEDED(hr) && playbackInfo) {
322 int32_t playbackStatusValue = 0;
323 if (SUCCEEDED(playbackInfo->get_PlaybackStatus(&playbackStatusValue))) {
324 isPlaying = playbackStatusValue == static_cast<int32_t>(winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing);
325 switch (static_cast<winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus>(playbackStatusValue)) {
326 #define FF_MEDIA_SET_STATUS(status_code) \
327 case winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus::status_code: \
328 ffStrbufSetStatic(&result->status, #status_code); \
329 break;
330 FF_MEDIA_SET_STATUS(Closed)
331 FF_MEDIA_SET_STATUS(Opened)
332 FF_MEDIA_SET_STATUS(Changing)
333 FF_MEDIA_SET_STATUS(Stopped)
334 FF_MEDIA_SET_STATUS(Playing)
335 FF_MEDIA_SET_STATUS(Paused)
336 #undef FF_MEDIA_SET_STATUS
337 }
338 }
339
340 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Foundation::IReference<double>>* playbackRateRef = nullptr;
341 if (SUCCEEDED(playbackInfo->get_PlaybackRate(reinterpret_cast<void**>(&playbackRateRef))) && playbackRateRef) {
342 if (SUCCEEDED(playbackRateRef->get_Value(&playbackRate)) && playbackRate < 0.0) {
343 playbackRate = 0.0;
344 }
345 }
346 }
347
348 [[gnu::cleanup(ffDeleteHstring)]] HSTRING title = nullptr;
349 if (SUCCEEDED(mediaProps->get_Title(reinterpret_cast<void**>(&title)))) {
350 ffStrbufSetHstring(&result->song, title);
351 }
352
353 [[gnu::cleanup(ffDeleteHstring)]] HSTRING artist = nullptr;
354 if (SUCCEEDED(mediaProps->get_Artist(reinterpret_cast<void**>(&artist)))) {
355 ffStrbufSetHstring(&result->artist, artist);
356 }
357
358 [[gnu::cleanup(ffDeleteHstring)]] HSTRING album = nullptr;
359 if (SUCCEEDED(mediaProps->get_AlbumTitle(reinterpret_cast<void**>(&album)))) {
360 ffStrbufSetHstring(&result->album, album);
361 }
362
363 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionTimelineProperties>* timelineProps = nullptr;
364 hr = session->GetTimelineProperties(reinterpret_cast<void**>(&timelineProps));
365 if (SUCCEEDED(hr) && timelineProps) {
366 int64_t duration = 0;
367 if (SUCCEEDED(timelineProps->get_EndTime(&duration)) && duration > 0) {
368 result->length = (uint32_t) (duration / 10000); // Convert from 100-nanosecond units to milliseconds
369
370 int64_t position = 0;
371 if (SUCCEEDED(timelineProps->get_Position(&position))) {
372 result->position = (uint32_t) (position / 10000); // Convert from 100-nanosecond units to milliseconds
373
374 int64_t lastUpdatedTime = 0;
375 if (isPlaying && SUCCEEDED(timelineProps->get_LastUpdatedTime(&lastUpdatedTime)) && lastUpdatedTime > 0) {
376 uint64_t lastUpdatedTimeMs = ffFileTimeToUnixMs((uint64_t) lastUpdatedTime);
377 uint64_t nowMs = ffTimeGetNow();
378 if (nowMs > lastUpdatedTimeMs) {
379 result->position += (uint32_t) (((double) (nowMs - lastUpdatedTimeMs)) * playbackRate);
380 }
381 }
382 }
383 }
384 }
385
386 if (playerId) {
387 uint32_t aumidLength = 0;
388 resolveAppUserModelId(WindowsGetStringRawBuffer(playerId, &aumidLength), &result->player);
389 }
390
391 if (result->player.length == 0) {
392 ffStrbufSet(&result->player, &result->playerId);
393 if (ffStrbufEndsWithIgnCaseS(&result->player, ".exe")) {
394 ffStrbufSubstrBefore(&result->player, result->player.length - 4);
395 }
396 }
397
398 if (saveCover) {
399 FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IRandomAccessStreamReference>* thumbnail = nullptr;
400 hr = mediaProps->get_Thumbnail(reinterpret_cast<void**>(&thumbnail));
401 if (SUCCEEDED(hr) && thumbnail) {
402 if (SUCCEEDED(ffSaveThumbnailToTempPath(thumbnail, &result->cover)) && result->cover.length > 0) {
403 result->removeCoverAfterUse = true;
404 }
405 }
406 }
407 } while (false);
408
409 return error;
410}
411#else
412static const char* getMedia(FFMediaResult* media, bool saveCover) {
413 FF_UNUSED(media, saveCover);
414 FF_DEBUG("Fastfetch is not compiled with WinRT support");
415 return "Fastfetch is not compiled with WinRT support";
416}
417#endif // FF_HAVE_WINRT
418
419extern "C" void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
420 const char* error = getMedia(media, saveCover);
421 ffStrbufAppendS(&media->error, error);
422}