WinUI 3のMediaPlayerElementで今表示されている映像を静止画として保存する

![サンプルプログラムの画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212471/3a3a3f4d-0cae-4fee-aa7f-b19e1e53aaf2.png)
残したいんですよ。

[メディア プレーヤー](https://learn.microsoft.com/ja-jp/windows/apps/design/controls/media-playback)、ありますよね。
WinUI 3なら[MediaPlayerElement クラス](https://learn.microsoft.com/ja-jp/uwp/api/windows.ui.xaml.controls.mediaplayerelement?view=winrt-26100)です。
それで動画を再生したなら、決定的瞬間を写真に収めたいと思いませんか？私は思います。

やっていきましょうか。

## 先に結論
- [MediaPlayer.CopyFrameToVideoSurface](https://learn.microsoft.com/ja-jp/uwp/api/windows.media.playback.mediaplayer.copyframetovideosurface?view=winrt-26100#windows-media-playback-mediaplayer-copyframetovideosurface(windows-graphics-directx-direct3d11-idirect3dsurface))で今映ってるやつ静止画に写せます
- 写す先のビットマップは[Win2D](https://www.nuget.org/packages/Microsoft.Graphics.Win2D/1.3.2)の[CanvasBitmap.CreateFromSoftwareBitmap](https://microsoft.github.io/Win2D/WinUI2/html/M_Microsoft_Graphics_Canvas_CanvasBitmap_CreateFromSoftwareBitmap.htm)で作れます
- [Win2D.uwp](https://www.nuget.org/packages/Win2D.uwp/1.28.2)じゃなくて[Microsoft.Graphics.Win2D](https://www.nuget.org/packages/Microsoft.Graphics.Win2D/1.3.2)だからね！

サンプル：https://github.com/mifumi323/WinUI3MediaPlayerElementSnapshotSample

## 画面
```xml:MainWindow.xaml
<?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WinUI3MediaPlayerElementSnapshotSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3MediaPlayerElementSnapshotSample"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="WinUI3MediaPlayerElementSnapshotSample">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <CommandBar HorizontalAlignment="Left" DefaultLabelPosition="Right">
            <AppBarButton Click="PhotoButton_Click" Icon="Camera" Label="写真"/>
        </CommandBar>
        <MediaPlayerElement x:Name="Player" Source="ms-appx:///Assets/sample.mp4" AreTransportControlsEnabled="True" Grid.Row="1" />
    </Grid>
</Window>
```
サンプルですのでシンプルにメディアプレーヤーとボタンだけです。
[MediaPlayerElement.Source](https://learn.microsoft.com/ja-jp/uwp/api/windows.ui.xaml.controls.mediaplayerelement.source?view=winrt-26100)にファイルを指定すれば簡単に動画を読み込めるわけですね。
本筋とは関係ありませんが、ボタンには[コマンド バー](https://learn.microsoft.com/ja-jp/windows/apps/design/controls/command-bar)を使っています。

## NuGetパッケージのインストール
![NuGetの検索画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212471/19d35285-5839-4988-9ce0-38a641cebdac.png)
先に書いたように、Win2Dが必要になります。
Win2D.uwpのほうが上に表示されますが、名前を見てわかるようにWinUI3じゃなくて[UWP](https://learn.microsoft.com/ja-jp/windows/uwp/get-started/universal-application-platform-guide)用なので、インストールしても使えません。
Microsoft.Graphics.Win2Dのほうをインストールしましょう。

## 保存するコード
```cs:MainWindow.xaml.cs
private async void PhotoButton_Click(object sender, RoutedEventArgs e)
{
    var savePicker = new FileSavePicker();
    savePicker.FileTypeChoices.Add("PNG", [".png"]);

    // このキモいおまじない無くなるといいな(https://qiita.com/hayashida-katsutoshi/items/21b8f99c462c01bb7c6d)
    InitializeWithWindow.Initialize(savePicker, WindowNative.GetWindowHandle(this));

    var file = await savePicker.PickSaveFileAsync();
    if (file != null)
    {
        int naturalVideoWidth = (int)Player.MediaPlayer.PlaybackSession.NaturalVideoWidth;
        int naturalVideoHeight = (int)Player.MediaPlayer.PlaybackSession.NaturalVideoHeight;

        CanvasDevice canvasDevice = CanvasDevice.GetSharedDevice();

        var frameServerDest = new SoftwareBitmap(BitmapPixelFormat.Rgba8, naturalVideoWidth, naturalVideoHeight, BitmapAlphaMode.Ignore);
        using var inputBitmap = CanvasBitmap.CreateFromSoftwareBitmap(canvasDevice, frameServerDest);
        Player.MediaPlayer.CopyFrameToVideoSurface(inputBitmap);

        var photoPath = file.Path;
        await inputBitmap.SaveAsync(photoPath, CanvasBitmapFileFormat.Png);
    }
}
```
これで画像として保存できます。
まあ、コードに書いた通りです。

コードの前半は保存ダイアログを出しているだけです。
コメントに書いてあるように、メインウィンドウを先に登録してやる必要があります(参考：[Windows App SDK, WinUI3, Desktopでファイル選択ダイアログを表示する](https://qiita.com/hayashida-katsutoshi/items/21b8f99c462c01bb7c6d))。

CanvasDevice以降の保存処理本体は公式ドキュメントの[フレーム サーバー モードでの MediaPlayer の使用](https://learn.microsoft.com/ja-jp/windows/uwp/audio-video-camera/play-audio-and-video-with-mediaplayer#use-mediaplayer-in-frame-server-mode)を参考にしています。
実はフレームサーバーモードじゃなくても普通に保存できるのと、VS2022にお任せで足りないライブラリ入れさせようとするとWin2D.uwp(WinUI3では使えない)を提案してくるあたりが罠でした。
コピー先のビットマップ作るだけで結構回りくどい処理しますね。

## 環境
Windows 11
Visual Studio 2022 Version 17.13.1

## 終わりに
WinUI3、DPI Aware PerMonitorV2に最初から対応しているので高解像度モニタできれいに表示されてなかなかいいですよ。