UnrealEngineでWindowsのファイルのプロパティから動画の再生時間を取得する

UnrealEngineでWindowsのファイルのプロパティから動画の再生時間を取得するのをやってみたのでメモしておきます。

## 試した環境

* UE5.2.1
* Windows11

## 参照サイト
 * [Windows-classic-samples/Samples/Win7Samples/winui/shell/appplatform/PropertyEdit/PropertyEdit.cpp at main · microsoft/Windows-classic-samples · GitHub](https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/winui/shell/appplatform/PropertyEdit/PropertyEdit.cpp)
 * [c# - Get video duration without playing? - Stack Overflow](https://stackoverflow.com/questions/10627217/get-video-duration-without-playing)
 * [c# - How to get video duration from mp4, wmv, flv, mov videos - Stack Overflow](https://stackoverflow.com/questions/10190906/how-to-get-video-duration-from-mp4-wmv-flv-mov-videos)

## 試したコード

```csharp:[プロジェクト名].build.cs
        if (Target.Platform == UnrealTargetPlatform.Win64)
        {
            PublicSystemLibraries.AddRange(new string[] { "Propsys.lib" });
        }
```

```cpp:コード本体
#include "Windows/AllowWindowsPlatformTypes.h"
#include "Windows/AllowWindowsPlatformAtomics.h"
#include "Windows/PreWindowsApi.h"

#include <shobjidl.h>
#include <propsys.h>
#include <propvarutil.h>
#include <propkey.h>
#include <strsafe.h>

#include "Windows/PostWindowsApi.h"
#include "Windows/HideWindowsPlatformAtomics.h"
#include "Windows/HideWindowsPlatformTypes.h"

int UPlatformUtils::GetMediaDurationFromFile(FString Filename)
{
    int Duration = -1;

#if PLATFORM_WINDOWS
    // Convert the Canonical name of the property to PROPERTYKEY
    PROPERTYKEY key;
    HRESULT hr = PSGetPropertyKeyFromName(TEXT("System.Media.Duration"), &key);
    if (!SUCCEEDED(hr))
    {
        UE_LOG(LogTemp, Error, TEXT("Invalid property specified: System.Media.Duration"));
        return Duration;
    }

    IPropertyStore* pps = NULL;

    WCHAR szExpanded[MAX_PATH];
    hr = ExpandEnvironmentStrings(*Filename, szExpanded, ARRAYSIZE(szExpanded)) ? S_OK : HRESULT_FROM_WIN32(GetLastError());
    if (!SUCCEEDED(hr))
    {
        UE_LOG(LogTemp, Error, TEXT("Error %x: failed to ExpandEnvironmentStrings"), hr);
        return Duration;
    }

    WCHAR szAbsPath[MAX_PATH];
    hr = _wfullpath(szAbsPath, szExpanded, ARRAYSIZE(szAbsPath)) ? S_OK : E_FAIL;
    if (!SUCCEEDED(hr))
    {
        UE_LOG(LogTemp, Error, TEXT("Error %x: failed to _wfullpath"), hr);
        return Duration;
    }

    hr = SHGetPropertyStoreFromParsingName(szAbsPath, NULL, GPS_DEFAULT, IID_PPV_ARGS(&pps));
    if (!SUCCEEDED(hr))
    {
        UE_LOG(LogTemp, Error, TEXT("Error %x: getting the propertystore for the item."), hr);
        return Duration;
    }
    TSharedPtr<IPropertyStore> Pps(pps, [](IPropertyStore* ptr) {
        if (ptr) ptr->Release();
        });

    PROPVARIANT propvarValue = { 0 };
    hr = Pps->GetValue(key, &propvarValue);
    if (!SUCCEEDED(hr))
    {
        UE_LOG(LogTemp, Error, TEXT("Error %x: cannot get value"), hr);
        return Duration;
    }
    TSharedPtr<PROPVARIANT> PropvarValue(&propvarValue, [](PROPVARIANT* ptr) {
        if (ptr) PropVariantClear(ptr);
        });

    PWSTR pszDisplayValue = NULL;
    hr = PSFormatForDisplayAlloc(key, propvarValue, PDFF_DEFAULT, &pszDisplayValue);
    if (!SUCCEEDED(hr))
    {
        UE_LOG(LogTemp, Error, TEXT("Error %x: failed to PSFormatForDisplayAlloc."), hr);
        return Duration;
    }
    TSharedPtr<WCHAR> PszDisplayValue(pszDisplayValue, [](PWSTR ptr) {
        if (ptr) CoTaskMemFree(ptr);
        });

    FString DurationStr = PszDisplayValue.Get();

    const FRegexPattern Pattern = FRegexPattern(TEXT("^([0-9]*):([0-9]*):([0-9]*)$"));
    FRegexMatcher Matcher(Pattern, DurationStr);
    if (Matcher.FindNext())
    {
        Duration = FCString::Atoi(*Matcher.GetCaptureGroup(3));
        Duration += FCString::Atoi(*Matcher.GetCaptureGroup(2)) * 60;
        Duration += FCString::Atoi(*Matcher.GetCaptureGroup(1)) * 60 * 60;
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Error: duration format is invalid %s"), *DurationStr);
    }
#endif
    return Duration;
}
```