[C#] Media Foundation で手軽に動画サムネイル作成

ちょっと動画のサムネイル作りたいと思いたったんですが、大体出てくる情報がFFmpegやDirectShowばかりで、どちらも若干面倒そうなので、もっと手軽なものは無いのかなーと探したら、[Microsoft Media Foundation](https://docs.microsoft.com/ja-jp/windows/win32/medfound/microsoft-media-foundation-sdk) がありました。

Media Foundationは、公式ページの翻訳を持ってくると、
>Microsoft Media Foundationは、Windows Vista以降でデジタルメディアを使用するためのアプリケーションとコンポーネントの開発を可能にします。
Media FoundationはWindows向けの次世代マルチメディアプラットフォームであり、開発者、消費者、コンテンツプロバイダーは、強化された堅牢性、比類のない品質、シームレスな相互運用性を備えたプレミアムコンテンツの新しい波を受け入れることができます。

まあ、イメージとしてはDirectShowの新しいバージョン的な感じでしょうか。Windows Vista以降なら使えるそうで、今時Win7は問題外だし、バンバン使って問題ないでしょう。当然Windows限定なので、他OSメインの方には申し訳ありません。

で、公式のサンプルを探したところ、[VideoThumbnail Sample](https://docs.microsoft.com/ja-jp/windows/win32/medfound/videothumbnail-sample) と今回の用途にピッタリのものがありました。ただ、元がC++なのと、色々と余計な処理が盛られてて判りにくいので、参考にしつつもバッサリと欲しい部分以外は切り捨てます。僕はただ動画から画像を取得したいだけなんだ…

#サンプル作成
さて、それではサンプル作成に入ります。
とりあえず適当に WindowsForms でフォームを作ることにします。

##適当にフォーム作成
![サンプルフォーム](https://i.imgur.com/CgPrvU0.png)
なんの捻りもやる気もないフォームですが、勘弁してください。
PictureBoxは、

- BorderStyle:FixedSingle
- SizeMode:StretchImage

に設定しています。

##SharpDX.MediaFoundation のインストール
次に、C#から普通にMedia Foundationを使用するのは大変なので、今回はNuGetから SharpDX.MediaFoundationをインストールします。もっとコンパクトにしたい場合は、C++/CLIで直接Media Foundation使ったほうがよいでしょう。
![NuGet画面](https://i.imgur.com/cQnXI2K.png)


##コード
さて、いよいよコードになります。

```cs
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Diagnostics;
using SharpDX.MediaFoundation;
using System.Runtime.InteropServices;

namespace VideoThumbnail
{
    public partial class Form1 : Form
    {
        [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", CallingConvention = CallingConvention.StdCall)]
        private static extern void RtlMoveMemory(IntPtr Destination, IntPtr Source, [MarshalAs(UnmanagedType.U4)] int Length);

        public Form1()
        {
            InitializeComponent();

            //MediaFoundation使用前にMediaManager.Startupが必要
            MediaManager.Startup();
        }

        protected override void OnFormClosed(FormClosedEventArgs e)
        {
            //MediaFoundation終了処理
            MediaManager.Shutdown();

            base.OnFormClosed(e);
        }

        /// <summary>
        /// 動画の指定位置から画像を取得し、Bitmapオブジェクトを作成する。
        /// </summary>
        /// <param name="moviePath">動画ファイルパス</param>
        /// <param name="positionOfPercent">取得する再生位置(0～100%)</param>
        /// <returns>作成したBitmapオブジェクト</returns>
        private Bitmap CreateVideoBitmap(string moviePath, double positionOfPercent)
        {
            var stopwatch = Stopwatch.StartNew();
            SourceReader reader = null;
            try
            {
                using (var attr = new MediaAttributes(1))
                using (var newMediaType = new MediaType())
                {
                    //SourceReaderに動画のパスを設定
                    attr.Set(SourceReaderAttributeKeys.EnableVideoProcessing.Guid, true);
                    reader = new SourceReader(moviePath, attr);

                    //出力メディアタイプをRGB32bitに設定
                    newMediaType.Set(MediaTypeAttributeKeys.MajorType, MediaTypeGuids.Video);
                    newMediaType.Set(MediaTypeAttributeKeys.Subtype, VideoFormatGuids.Rgb32);
                    reader.SetCurrentMediaType(SourceReaderIndex.FirstVideoStream, newMediaType);

                    //元のメディアタイプから動画情報を取得する
                    // duration:ビデオの総フレーム数
                    // frameSize:フレーム画像サイズ（上位32bit:幅 下位32bit:高さ）
                    // stride:フレーム画像一ライン辺りのバイト数
                    var mediaType = reader.GetCurrentMediaType(SourceReaderIndex.FirstVideoStream);
                    var duration = reader.GetPresentationAttribute(SourceReaderIndex.MediaSource, PresentationDescriptionAttributeKeys.Duration);
                    var frameSize = mediaType.Get(MediaTypeAttributeKeys.FrameSize);
                    var stride = mediaType.Get(MediaTypeAttributeKeys.DefaultStride);
                    var rect = new Rectangle()
                    {
                        Width = (int)(frameSize >> 32),
                        Height = (int)(frameSize & 0xffffffff)
                    };

                    //取得する動画の位置を設定
                    var mulPositionOfPercent = Math.Min(Math.Max(positionOfPercent, 0), 100.0) / 100.0;
                    reader.SetCurrentPosition((long)(duration * mulPositionOfPercent));

                    //動画から1フレーム取得し、Bitmapオブジェクトを作成してメモリコピー
                    int actualStreamIndex;
                    SourceReaderFlags readerFlags;
                    long timeStampRef;
                    using (var sample = reader.ReadSample(SourceReaderIndex.FirstVideoStream, SourceReaderControlFlags.None, out actualStreamIndex, out readerFlags, out timeStampRef))
                    using (var buf = sample.ConvertToContiguousBuffer())
                    {
                        int maxLength;
                        int currentLength;
                        var pBuffer = buf.Lock(out maxLength, out currentLength);
                        var bmp = new Bitmap(rect.Width, rect.Height, System.Drawing.Imaging.PixelFormat.Format32bppRgb);
                        var bmpData = bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppRgb);
                        RtlMoveMemory(bmpData.Scan0, pBuffer, stride * rect.Height);
                        bmp.UnlockBits(bmpData);
                        buf.Unlock();
                        return bmp;
                    }
                }
            }
            finally
            {
                if (reader != null) reader.Dispose();

                stopwatch.Stop();
                textBox1.AppendText($"process time {stopwatch.ElapsedMilliseconds} msec ({moviePath})\r\n");
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            pictureBox1.Image = CreateVideoBitmap(@"C:\test\mov_hts-samp003.mp4", 75);
            pictureBox2.Image = CreateVideoBitmap(@"C:\test\mov_hts-samp004.mp4", 75);
            pictureBox3.Image = CreateVideoBitmap(@"C:\test\mov_hts-samp005.mp4", 75);
        }
    }
}

```

公式のサンプルよりは大分シンプルになったかなと思います。WindowsForms用にBitmapオブジェクトにしていますが、WPFで使うならWriteableBitmap辺りに置き換えれば多分行けると思います。

注意点としては、

- CreateVideoBitmap を呼び出す前に MediaManager.Startup を呼び出す
- 終了前に MediaManager.Shutdown を呼び出す

の二つです。
動画ファイル以外が指定された場合や、例外処理等は全く考慮してないので、実際に使う場合はきちんと例外対応を入れてください。ちなみに、サンプルではPictureBoxに設定したBitmapの解放をしてないので、連続でボタンをクリックするとメモリリークします（笑）

#実行結果
![実行結果](https://i.imgur.com/juGDkh9.png)

やりましたね！速度もまあこんなもんでしょう。
ちなみに、動画に対応するコーデックはインストールしてないと多分失敗します。
ビルドするとSharpDX関連のDLLが色々出力されますが、サンプルは SharpDX.dll と SharpDX.MediaFoundation.dll があればとりあえず動作します。

サンプル動画は、下記サイトのものを使わせて頂きました。
[HYBRID CREATIVE MOVIE サクラ](https://www.home-movie.biz/free_movie.html)