extern "C" { #include "media.h" #include "common/debug.h" #include "common/time.h" #include "common/windows/unicode.h" #include "common/windows/com.h" } #if FF_HAVE_WINRT #include "common/windows/winrt.hpp" #include #include #include #include #include #include #include #include #include static HRESULT ffSaveThumbnailToTempPath( abi_t* thumbnail, FFstrbuf* destination) { FF_AUTO_RELEASE_COM_OBJECT abi_t* contentStream = nullptr; HRESULT hr = ffRunAndWait([=](void** result) { return thumbnail->OpenReadAsync(result); }, &contentStream); if (FAILED(hr) || !contentStream) { return FAILED(hr) ? hr : E_FAIL; } FF_AUTO_RELEASE_COM_OBJECT abi_t* randomAccessStream = nullptr; hr = ffQueryInterface(contentStream, &randomAccessStream); if (FAILED(hr)) { return hr; } UINT64 size = 0; hr = randomAccessStream->get_Size(&size); if (FAILED(hr) || size == 0) { return FAILED(hr) ? hr : S_FALSE; } if (size > 0xFFFFFFFFu) { return HRESULT_FROM_WIN32(ERROR_FILE_TOO_LARGE); } FF_AUTO_RELEASE_COM_OBJECT abi_t* bufferFactory = nullptr; hr = ffGetActivationFactory(L"Windows.Storage.Streams.Buffer", &bufferFactory); if (FAILED(hr)) { return hr; } FF_AUTO_RELEASE_COM_OBJECT abi_t* buffer = nullptr; hr = bufferFactory->Create((UINT32) size, reinterpret_cast(&buffer)); if (FAILED(hr) || !buffer) { return FAILED(hr) ? hr : E_FAIL; } FF_AUTO_RELEASE_COM_OBJECT abi_t* inputStream = nullptr; hr = ffQueryInterface(contentStream, &inputStream); if (FAILED(hr)) { return hr; } FF_AUTO_RELEASE_COM_OBJECT abi_t* readBuffer = nullptr; hr = ffRunAndWait2([=](void** result) { return inputStream->ReadAsync(buffer, (uint32_t) size, (uint32_t) winrt::Windows::Storage::Streams::InputStreamOptions::None, result); }, &readBuffer); if (FAILED(hr) || !readBuffer) { return FAILED(hr) ? hr : E_FAIL; } UINT32 length = 0; hr = readBuffer->get_Length(&length); if (FAILED(hr) || length == 0) { return FAILED(hr) ? hr : S_FALSE; } FF_AUTO_RELEASE_COM_OBJECT Windows::Storage::Streams::IBufferByteAccess* byteAccess = nullptr; hr = readBuffer->QueryInterface(IID_PPV_ARGS(&byteAccess)); if (FAILED(hr)) { return hr; } byte* bytes = nullptr; hr = byteAccess->Buffer(&bytes); if (FAILED(hr) || !bytes) { return FAILED(hr) ? hr : E_FAIL; } wchar_t tempDirectory[MAX_PATH]; DWORD tempLength = GetTempPathW(MAX_PATH, tempDirectory); if (tempLength == 0 || tempLength >= MAX_PATH) { return HRESULT_FROM_WIN32(GetLastError()); } wchar_t tempFilePath[MAX_PATH]; if (!GetTempFileNameW(tempDirectory, L"fft", 0, tempFilePath)) { return HRESULT_FROM_WIN32(GetLastError()); } HANDLE file = CreateFileW(tempFilePath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) { DWORD writeError = GetLastError(); DeleteFileW(tempFilePath); return HRESULT_FROM_WIN32(writeError); } DWORD written = 0; BOOL writtenOk = WriteFile(file, bytes, length, &written, nullptr); NtClose(file); file = nullptr; if (!writtenOk || written != length) { DWORD writeError = GetLastError(); DeleteFileW(tempFilePath); return HRESULT_FROM_WIN32(writtenOk ? ERROR_WRITE_FAULT : writeError); } ffStrbufSetWS(destination, tempFilePath); return S_OK; } // Path 1: `Windows.ApplicationModel.AppInfo`, which answers straight from the package manifest. // It only knows packaged (MSIX) apps, but for those it is the cheap source: resolving a // `PackageFamilyName!AppId` AppUserModelId costs about half of what the shell needs, because the // shell has to look the application up in the package graph. A *failed* lookup still costs a // couple of milliseconds of WinRT class activation, so this is only worth trying on names that // are actually packaged -- the caller gates it, see `resolveAppUserModelId`. static bool resolvePackagedAppUserModelId(const wchar_t* aumid, FFstrbuf* result) { FF_AUTO_RELEASE_COM_OBJECT abi_t* statics = nullptr; if (FAILED(ffGetActivationFactory(L"Windows.ApplicationModel.AppInfo", &statics)) || !statics) { return false; } HSTRING_HEADER header; HSTRING aumidString; if (FAILED(WindowsCreateStringReference(aumid, (UINT32) wcslen(aumid), &header, &aumidString))) { return false; } FF_AUTO_RELEASE_COM_OBJECT abi_t* appInfo = nullptr; if (FAILED(statics->GetFromAppUserModelId(reinterpret_cast(aumidString), reinterpret_cast(&appInfo))) || !appInfo) { return false; } FF_AUTO_RELEASE_COM_OBJECT abi_t* displayInfo = nullptr; if (FAILED(appInfo->get_DisplayInfo(reinterpret_cast(&displayInfo))) || !displayInfo) { return false; } [[gnu::cleanup(ffDeleteHstring)]] HSTRING displayName = nullptr; if (FAILED(displayInfo->get_DisplayName(reinterpret_cast(&displayName))) || !displayName) { return false; } ffStrbufSetHstring(result, displayName); return result->length > 0; } // Path 2: the Start menu's `AppsFolder` namespace, i.e. the (display name, AppUserModelId) table // Windows itself displays. It covers packaged and unpackaged apps alike, and inside that namespace // an item's parsing name *is* its AppUserModelId, so `ParseDisplayName` finds a child directly -- // going through `SHCreateItemFromParsingName(L"shell:AppsFolder\\" + aumid)` costs several times // more, because the `shell:` protocol has to be activated first. static bool resolveAppsFolderAppUserModelId(const wchar_t* aumid, FFstrbuf* result) { const size_t aumidLength = wcslen(aumid); wchar_t name[512]; if (aumidLength == 0 || aumidLength >= ARRAY_SIZE(name)) { return false; } wmemcpy(name, aumid, aumidLength + 1); // `ParseDisplayName` wants a mutable string FF_AUTO_RELEASE_COM_OBJECT IShellItem* folder = nullptr; if (FAILED(SHGetKnownFolderItem(FOLDERID_AppsFolder, KF_FLAG_DEFAULT, nullptr, IID_PPV_ARGS(&folder))) || !folder) { return false; } FF_AUTO_RELEASE_COM_OBJECT IShellFolder* shellFolder = nullptr; if (FAILED(folder->BindToHandler(nullptr, BHID_SFObject, IID_PPV_ARGS(&shellFolder))) || !shellFolder) { return false; } LPITEMIDLIST child = nullptr; ULONG attributes = 0; if (FAILED(shellFolder->ParseDisplayName(nullptr, nullptr, name, &attributes, &child, nullptr)) || !child) { return false; } bool success = false; STRRET strret = {}; if (SUCCEEDED(shellFolder->GetDisplayNameOf(child, SHGDN_INFOLDER, &strret))) { wchar_t displayName[ARRAY_SIZE(name)]; if (SUCCEEDED(StrRetToBufW(&strret, child, displayName, ARRAY_SIZE(displayName)))) { ffStrbufSetWS(result, displayName); success = result->length > 0; } } CoTaskMemFree(child); return success; } // The two sources above agree on every packaged app, and the shell alone covers everything else, // so picking between them is purely a matter of cost. `PackageFamilyName!AppId` is the shape // `AppInfo` can serve, and the `!` is what distinguishes it from an unpackaged AppUserModelId // (`Chrome`, `PotPlayerMini64.exe`, a derived path). The shell stays the fallback either way, so a // packaged-looking name that `AppInfo` rejects is still resolved. static bool resolveAppUserModelId(const wchar_t* aumid, FFstrbuf* result) { if (wcschr(aumid, L'!') && resolvePackagedAppUserModelId(aumid, result)) { return true; } return resolveAppsFolderAppUserModelId(aumid, result); } static const char* getMedia(FFMediaResult* result, bool saveCover) { const char* error = ffInitCom(); if (error) { return error; } do { FF_AUTO_RELEASE_COM_OBJECT abi_t* managerStatics = nullptr; HRESULT hr = ffGetActivationFactory(L"Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager", &managerStatics); if (FAILED(hr) || !managerStatics) { error = "winrt: RoGetActivationFactory(GlobalSystemMediaTransportControlsSessionManager) failed"; break; } FF_AUTO_RELEASE_COM_OBJECT abi_t* manager = nullptr; hr = ffRunAndWait([=](void** result) { return managerStatics->RequestAsync(result); }, &manager); if (FAILED(hr) || !manager) { error = "winrt: RequestAsync().GetResults() failed"; break; } [[gnu::cleanup(ffDeleteHstring)]] HSTRING playerId = nullptr; FF_AUTO_RELEASE_COM_OBJECT abi_t* session = nullptr; if (instance.config.general.playerName.length) { FF_AUTO_RELEASE_COM_OBJECT abi_t>* sessions = nullptr; hr = manager->GetSessions(reinterpret_cast(&sessions)); if (FAILED(hr) || !sessions) { error = "winrt: GetSessions() failed"; break; } uint32_t sessionCount = 0; hr = sessions->get_Size(&sessionCount); if (FAILED(hr)) { error = "winrt: GetSessions().get_Size() failed"; break; } for (uint32_t i = 0; i < sessionCount; i++) { FF_AUTO_RELEASE_COM_OBJECT abi_t* currentSession = nullptr; hr = sessions->GetAt(i, reinterpret_cast(¤tSession)); if (FAILED(hr) || !currentSession) { continue; } hr = currentSession->get_SourceAppUserModelId(reinterpret_cast(&playerId)); if (FAILED(hr) || !playerId) { continue; } ffStrbufSetHstring(&result->playerId, playerId); if (ffStrbufContainIgnCase(&result->playerId, &instance.config.general.playerName)) { session = currentSession; currentSession = nullptr; // Don't release the session object break; } ffDeleteHstring(&playerId); ffStrbufClear(&result->playerId); } if (!session) { error = "winrt: No media session found with the specified player name"; break; } } else { hr = manager->GetCurrentSession(reinterpret_cast(&session)); if (FAILED(hr) || !session) { error = "winrt: GetCurrentSession() failed"; break; } hr = session->get_SourceAppUserModelId(reinterpret_cast(&playerId)); if (FAILED(hr)) { error = "winrt: get_SourceAppUserModelId() failed"; break; } ffStrbufSetHstring(&result->playerId, playerId); } FF_AUTO_RELEASE_COM_OBJECT abi_t* mediaProps = nullptr; hr = ffRunAndWait([=](void** result) { return session->TryGetMediaPropertiesAsync(result); }, &mediaProps); if (FAILED(hr) || !mediaProps) { error = "winrt: TryGetMediaPropertiesAsync().GetResults() failed"; break; } FF_AUTO_RELEASE_COM_OBJECT abi_t* playbackInfo = nullptr; hr = session->GetPlaybackInfo(reinterpret_cast(&playbackInfo)); bool isPlaying = false; double playbackRate = 1.0; if (SUCCEEDED(hr) && playbackInfo) { int32_t playbackStatusValue = 0; if (SUCCEEDED(playbackInfo->get_PlaybackStatus(&playbackStatusValue))) { isPlaying = playbackStatusValue == static_cast(winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing); switch (static_cast(playbackStatusValue)) { #define FF_MEDIA_SET_STATUS(status_code) \ case winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus::status_code: \ ffStrbufSetStatic(&result->status, #status_code); \ break; FF_MEDIA_SET_STATUS(Closed) FF_MEDIA_SET_STATUS(Opened) FF_MEDIA_SET_STATUS(Changing) FF_MEDIA_SET_STATUS(Stopped) FF_MEDIA_SET_STATUS(Playing) FF_MEDIA_SET_STATUS(Paused) #undef FF_MEDIA_SET_STATUS } } FF_AUTO_RELEASE_COM_OBJECT abi_t>* playbackRateRef = nullptr; if (SUCCEEDED(playbackInfo->get_PlaybackRate(reinterpret_cast(&playbackRateRef))) && playbackRateRef) { if (SUCCEEDED(playbackRateRef->get_Value(&playbackRate)) && playbackRate < 0.0) { playbackRate = 0.0; } } } [[gnu::cleanup(ffDeleteHstring)]] HSTRING title = nullptr; if (SUCCEEDED(mediaProps->get_Title(reinterpret_cast(&title)))) { ffStrbufSetHstring(&result->song, title); } [[gnu::cleanup(ffDeleteHstring)]] HSTRING artist = nullptr; if (SUCCEEDED(mediaProps->get_Artist(reinterpret_cast(&artist)))) { ffStrbufSetHstring(&result->artist, artist); } [[gnu::cleanup(ffDeleteHstring)]] HSTRING album = nullptr; if (SUCCEEDED(mediaProps->get_AlbumTitle(reinterpret_cast(&album)))) { ffStrbufSetHstring(&result->album, album); } FF_AUTO_RELEASE_COM_OBJECT abi_t* timelineProps = nullptr; hr = session->GetTimelineProperties(reinterpret_cast(&timelineProps)); if (SUCCEEDED(hr) && timelineProps) { int64_t duration = 0; if (SUCCEEDED(timelineProps->get_EndTime(&duration)) && duration > 0) { result->length = (uint32_t) (duration / 10000); // Convert from 100-nanosecond units to milliseconds int64_t position = 0; if (SUCCEEDED(timelineProps->get_Position(&position))) { result->position = (uint32_t) (position / 10000); // Convert from 100-nanosecond units to milliseconds int64_t lastUpdatedTime = 0; if (isPlaying && SUCCEEDED(timelineProps->get_LastUpdatedTime(&lastUpdatedTime)) && lastUpdatedTime > 0) { uint64_t lastUpdatedTimeMs = ffFileTimeToUnixMs((uint64_t) lastUpdatedTime); uint64_t nowMs = ffTimeGetNow(); if (nowMs > lastUpdatedTimeMs) { result->position += (uint32_t) (((double) (nowMs - lastUpdatedTimeMs)) * playbackRate); } } } } } if (playerId) { uint32_t aumidLength = 0; resolveAppUserModelId(WindowsGetStringRawBuffer(playerId, &aumidLength), &result->player); } if (result->player.length == 0) { ffStrbufSet(&result->player, &result->playerId); if (ffStrbufEndsWithIgnCaseS(&result->player, ".exe")) { ffStrbufSubstrBefore(&result->player, result->player.length - 4); } } if (saveCover) { FF_AUTO_RELEASE_COM_OBJECT abi_t* thumbnail = nullptr; hr = mediaProps->get_Thumbnail(reinterpret_cast(&thumbnail)); if (SUCCEEDED(hr) && thumbnail) { if (SUCCEEDED(ffSaveThumbnailToTempPath(thumbnail, &result->cover)) && result->cover.length > 0) { result->removeCoverAfterUse = true; } } } } while (false); return error; } #else static const char* getMedia(FFMediaResult* media, bool saveCover) { FF_UNUSED(media, saveCover); FF_DEBUG("Fastfetch is not compiled with WinRT support"); return "Fastfetch is not compiled with WinRT support"; } #endif // FF_HAVE_WINRT extern "C" void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) { const char* error = getMedia(media, saveCover); ffStrbufAppendS(&media->error, error); }