あたも技術ブログ

セットジャパンコーポレーションの社員が運営しています。

【C#】Windowsフォーム アプリケーションで動画再生ユーザコントロール

 引き続きWindowsフォーム アプリケーション関連のメモです。
今回はWindowsフォーム アプリケーションでの動画再生についてです。一番簡単な方法はWindows Media Playerを利用することですが、あまりカスタマイズが出来ないため今回はDirectShowを使用します。

 環境によってはActiveMovie control type libraryが参照できないときがあります。その場合、以下の方法を試してください。

 1. レジストリエディタを起動し「HKEY_CLASSES_ROOT\TypeLib\{56A868B0-0AD4-11CE-B03A-0020AF0BA770}\1.0」を開く。
 2. データの値が「quartz.dll」となっているのを確認して、「C:\Windows\System32\quartz.dll」の形式に変更。
 3. Visual Studioで参照の追加を行い、ActiveMovie control type libraryを参照します。

namespace MoviePlayerSample
{
  partial class MoviePlayer
  {
    /// <summary>
    /// 必要なデザイナー変数です。
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    /// <summary>
    /// 使用中のリソースをすべてクリーンアップします。
    /// </summary>
    /// <param name="disposing">マネージ リソースが破棄される場合 true、破棄されない場合は false です。</param>
    protected override void Dispose(bool disposing)
    {
      if (disposing && (components != null))
      {
        components.Dispose();
      }
      base.Dispose(disposing);
    }

    #region コンポーネント デザイナーで生成されたコード

    /// <summary>
    /// デザイナー サポートに必要なメソッドです。このメソッドの内容を
    /// コード エディターで変更しないでください。
    /// </summary>
    private void InitializeComponent()
    {
      this.picBackground = new System.Windows.Forms.PictureBox();
      ((System.ComponentModel.ISupportInitialize)(this.picBackground)).BeginInit();
      this.SuspendLayout();
      //
      // picBackground
      //
      this.picBackground.BackColor = System.Drawing.Color.MistyRose;
      this.picBackground.Dock = System.Windows.Forms.DockStyle.Fill;
      this.picBackground.Location = new System.Drawing.Point(0, 0);
      this.picBackground.Margin = new System.Windows.Forms.Padding(0);
      this.picBackground.Name = "picBackground";
      this.picBackground.Size = new System.Drawing.Size(431, 190);
      this.picBackground.TabIndex = 0;
      this.picBackground.TabStop = false;
      //
      // MoviePlayer
      //
      this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
      this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
      this.BackColor = System.Drawing.Color.OrangeRed;
      this.Controls.Add(this.picBackground);
      this.Margin = new System.Windows.Forms.Padding(0);
      this.Name = "MoviePlayer";
      this.Size = new System.Drawing.Size(431, 190);
      ((System.ComponentModel.ISupportInitialize)(this.picBackground)).EndInit();
      this.ResumeLayout(false);

    }

    #endregion

    private System.Windows.Forms.PictureBox picBackground;

  }
}


 再生不可な動画を指定した場合、例外が発生しますのでエラー時の処理を追加することをお勧めします。後、Playメソッドで動画の拡張子チェックなども行うと良いでしょう。
またWndProcメソッドで動画再生の終了メッセージを補足したとき、ループ再生を行っています。そのまま停止させたい場合は改良してください。
早送り、巻き戻しには対応していません。

using QuartzTypeLib;
using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace MoviePlayerSample
{
  /// <summary>
  /// 動画再生ユーザコントロール
  /// </summary>
  public partial class MoviePlayer : UserControl
  {
    #region 定数
    /// 動画を子として描画する値を表す定数
    /// </summary>
    private const int WS_CHILD = 0x40000000;
    /// <summary>
    /// 子ウィンドウを互いに重ならないようにクリップする値を表す定数
    /// </summary>
    private const int WS_CLIPSIBLINGS = 0x04000000;
    /// <summary>
    /// すべてのデータのレンダリングをし終えた値を表す定数
    /// </summary>
    private const int EC_COMPLETE = 0x01;
    /// <summary>
    /// ユーザーが再生を強制終了した値を表す定数
    /// </summary>
    private const int EC_USERABORT = 0x02;
    /// <summary>
    /// アプリケーション定義のメッセージを定義する値を表す定数
    /// </summary>
    private const int WM_APP = 0x8000;
    /// <summary>
    /// フィルタグラフイベントのメッセージ取得に使用するIDを表す定数
    /// </summary>
    private const int WM_GRAPHNOTIFY = WM_APP + 1;
    #endregion

    #region メンバ変数
    /// <summary>
    /// フィルタグラフマネージャ
    /// </summary>
    private FilgraphManager _FilterGraph = null;
    /// <summary>
    /// メディアコントロールインターフェース
    /// </summary>
    private IMediaControl _MediaControl = null;
    /// <summary>
    /// ビデオウィンドウインターフェース
    /// </summary>
    private IVideoWindow _VideoWindow = null;
    /// <summary>
    /// メディアイベントインターフェース
    /// </summary>
    private IMediaEventEx _MediaEventEx = null;
    /// <summary>
    /// ベーシックビデオインターフェース
    /// </summary>
    private IBasicVideo _BasicVideo = null;
    /// <summary>
    /// ベーシックオーディオインターフェース
    /// </summary>
    private IBasicAudio _BasicAudio = null;
    /// <summary>
    /// メディアポジションインターフェース
    /// </summary>
    private IMediaPosition _MediaPosition = null;
    /// <summary>
    /// 再生を行う動画ファイルのパス
    /// </summary>
    private string _Source = String.Empty;
    #endregion

    #region プロパティ
    /// <summary>
    /// 再生する動画のファイルパスを取得または設定します。
    /// </summary>
    [Browsable(true)]
    [Category("動作")]
    [Description("再生する動画のファイルパスを取得または設定します。")]
    public string Source
    {
      get
      {
        return this._Source;
      }
      set
      {
        if (this._Source != value)
        {
          this._Source = value;
          Play();
        }
      }
    }

    /// <summary>
    /// 背景に画像を表示する場合のファイルパスを取得または設定します。
    /// </summary>
    [Browsable(true)]
    [Category("動作")]
    [Description("背景に画像を表示する場合のファイルパスを取得または設定します。")]
    public string Background
    {
      get
      {
        return this.picBackground.ImageLocation;
      }
      set
      {
        if (this.picBackground.ImageLocation != value)
        {
          this.picBackground.ImageLocation = value;
          ImageCorrection();
        }
      }
    }
    #endregion

    #region コンストラクタ
    /// <summary>
    /// クラスの新しいインスタンスを初期化します。
    /// </summary>
    public MoviePlayer()
    {
      InitializeComponent();
    }
    #endregion

    #region イベント
    /// <summary>
    /// ユーザコントロールがアンロードされたときに発生します。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void OnUnload(object sender, EventArgs e)
    {
      // COMオブジェクトの初期化
      InitializeControl();
    }
    #endregion

    #region publicメソッド
    /// <summary>
    /// 動画を再生します。
    /// </summary>
    /// <param name="filePath"></param>
    public void Play()
    {
      // 再生する動画ファイルのパスが設定されていない場合、処理から抜ける
      if (String.IsNullOrEmpty(this._Source)) { return; }

      // 再生する動画ファイルが存在しない場合、処理から抜ける
      if (!File.Exists(this._Source)) { return; }

      PlayMovie();
    }

    /// <summary>
    /// 音量を上げます。
    /// </summary>
    public void VolumeUp()
    {
      if (this._BasicAudio != null && this._BasicAudio.Volume != 0)
      {
        this._BasicAudio.Volume += 100;
      }
    }

    /// <summary>
    /// 音量を下げます。
    /// </summary>
    public void VolumeDown()
    {
      if (this._BasicAudio != null && this._BasicAudio.Volume != -10000)
      {
        this._BasicAudio.Volume -= 100;
      }
    }

    /// <summary>
    /// 一時停止を解除します。
    /// </summary>
    public void Unpause()
    {
      if (this._MediaControl != null)
      {
        // グラフの一時停止を解除する
        this._MediaControl.Run();
      }
    }

    /// <summary>
    /// 動画の再生を一時停止します。
    /// </summary>
    public void Pause()
    {
      if (this._MediaControl != null)
      {
        // グラフを一時停止する
        this._MediaControl.Stop();
      }
    }
    #endregion

    #region protectedメソッド
    /// <summary>
    /// Load イベントを発生させます。
    /// </summary>
    /// <param name="e">イベントの引数</param>
    protected override void OnLoad(EventArgs e)
    {
      this.Disposed += OnUnload;

      base.OnLoad(e);
    }

    /// <summary>
    /// Resize イベントを発生させます。
    /// </summary>
    /// <param name="e">イベントの引数</param>
    protected override void OnResize(EventArgs e)
    {
      // 背景画像のサイズを変更します。
      ImageCorrection();

      if (this._VideoWindow == null) { return; }

      // 動画のサイズの取得
      var size = CalcDisplaySize();

      // 表示位置のオフセットを取得
      var point = CalcDisplayPoint(size);

      // VideWindowを動画のサイズに設定
      this._VideoWindow.SetWindowPosition(point.X, point.Y, size.X, size.Y);

      base.OnResize(e);
    }

    /// <summary>
    /// Windowsメッセージ処理のオーバーライド
    /// </summary>
    /// <param name="m"></param>
    protected override void WndProc(ref Message m)
    {
      int eventCode;
      int lparam1;
      int lparam2;

      if (m.Msg == WM_GRAPHNOTIFY)
      {
        // イベントをキューから取得する
        // while文でキューから全てのイベントを取得する
        while (true)
        {
          try
          {
            this._MediaEventEx.GetEvent(out eventCode, out lparam1, out lparam2, 0);
            this._MediaEventEx.FreeEventParams(eventCode, lparam1, lparam2);

            // ストリーム終了または中断
            if ((eventCode == EC_COMPLETE) || (eventCode == EC_USERABORT))
            {
              this._MediaPosition.CurrentPosition = 0;
              this._MediaControl.Run();
            }
          }
          catch (Exception)
          {
            break;
          }
        }
      }

      base.WndProc(ref m);
    }
    #endregion

    #region privateメソッド
    /// <summary>
    /// コントロールを初期化します。
    /// </summary>
    private void InitializeControl()
    {
      // 必要な場合は、動画の停止と画面の初期化
      if (this._MediaControl != null)
      {
        this._MediaControl.Stop();
      }

      if (this._MediaEventEx != null)
      {
        this._MediaEventEx.SetNotifyWindow(0, 0, 0);
      }

      if (this._VideoWindow != null)
      {
        this._VideoWindow.Visible = 0;
        this._VideoWindow.Owner = 0;
      }

      // 各COMオブジェクト(インターフェース)のRelease
      if (this._FilterGraph != null)
      {
        Marshal.ReleaseComObject(this._FilterGraph);
        this._FilterGraph = null;
      }

      if (this._BasicVideo != null)
      {
        Marshal.ReleaseComObject(this._BasicVideo);
        this._BasicVideo = null;
      }

      if (this._BasicAudio != null)
      {
        Marshal.ReleaseComObject(this._BasicAudio);
        this._BasicAudio = null;
      }

      if (this._MediaControl != null)
      {
        Marshal.ReleaseComObject(this._MediaControl);
        this._MediaControl = null;
      }

      if (this._MediaPosition != null)
      {
        Marshal.ReleaseComObject(this._MediaPosition);
        this._MediaPosition = null;
      }

      if (this._MediaEventEx != null)
      {
        Marshal.ReleaseComObject(this._MediaEventEx);
        this._MediaEventEx = null;
      }

      if (this._VideoWindow != null)
      {
        Marshal.ReleaseComObject(this._VideoWindow);
        this._VideoWindow = null;
      }
    }

    /// <summary>
    /// 動画を再生します。
    /// </summary>
    private void PlayMovie()
    {
      // COMオブジェクトの初期化
      if (this._MediaControl == null) { InitializeControl(); }

      try
      {
        if (this._MediaControl != null)
        {
          // 既にグラフが設定されていた場合、再生を行う
          this._MediaControl.Run();
          return;
        }

        // フィルタグラフマネージャを生成
        this._FilterGraph = new FilgraphManager();

        // 各インターフェースを取得
        this._BasicVideo = (IBasicVideo)this._FilterGraph;
        this._BasicAudio = (IBasicAudio)this._FilterGraph;
        this._MediaControl = (IMediaControl)this._FilterGraph;
        this._MediaEventEx = (IMediaEventEx)this._FilterGraph;
        this._VideoWindow = (IVideoWindow)this._FilterGraph;
        this._MediaPosition = (IMediaPosition)this._FilterGraph;

        // グラフを構築
        this._FilterGraph.RenderFile(this._Source);

        // VideoWindowをメインWindowにアタッチして、子ウィンドウに設定
        this._VideoWindow.Owner = (int)this.Handle;
        this._VideoWindow.WindowStyle = WS_CHILD | WS_CLIPSIBLINGS;

        // 動画のサイズの取得
        var size = CalcDisplaySize();

        // 表示位置のオフセットを取得
        var point = CalcDisplayPoint(size);

        // VideWindowを動画のサイズに設定
        this._VideoWindow.SetWindowPosition(point.X, point.Y, size.X, size.Y);

        // イベント通知を受け取るように所有者ウィンドウを設定
        this._MediaEventEx.SetNotifyWindow((int)this.Handle, WM_GRAPHNOTIFY, 0);

        // グラフを実行
        this._MediaControl.Run();
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex.StackTrace);
        // COMオブジェクトの初期化
        InitializeControl();
      }
    }

    /// <summary>
    /// 引数のサイズからアスペクト比を算出します。
    /// </summary>
    /// <param name="w">横の解像度</param>
    /// <param name="h">縦の解像度</param>
    /// <returns>アスペクト比</returns>
    private Point CalcAspect(int w, int h)
    {
      // 最大公約数を求める
      var gcd = Gcd(w, h);

      var result = new Point();
      result.X = w / gcd;
      result.Y = h / gcd;
      return result;
    }

    /// <summary>
    /// 最大公約数を求めます。
    /// </summary>
    /// <param name="w">横の解像度</param>
    /// <param name="h">縦の解像度</param>
    /// <returns></returns>
    private int Gcd(int w, int h)
    {
      if (w < h)
      {
        return Gcd(h, w);
      }
      while (h != 0)
      {
        var remainder = w % h;
        w = h;
        h = remainder;
      }
      return w;
    }

    /// <summary>
    /// 表示する動画のサイズを算出します。
    /// </summary>
    /// <returns>動画のサイズ</returns>
    private Point CalcDisplaySize()
    {
      // コントロールのサイズ取得
      var dh = this.Height;
      var dw = this.Width;

      // 動画のサイズ取得
      var sh = 0;
      var sw = 0;
      this._BasicVideo.GetVideoSize(out sw, out sh);
      // アスペクト比を求める
      var aspect = CalcAspect(sw, sh);

      // 表示解像度
      var h = dh;
      var w = dw;

      // コントロールと動画の高さを比較し、大きい方を設定する
      // = 高さの解像度 / 高さの比率
      // = 比率*幅の比率

      // = 幅の解像度 / 幅の比率
      // = 比率*高さの比率

      // 高さを求める
      var ratio = ((double)h / (double)aspect.Y);
      var w1 = (int)(ratio * aspect.X);
      // 幅を求める
      ratio = ((double)w / (double)aspect.X);
      var h1 = (int)(ratio * aspect.Y);

      if (h1 < h)
      {
        h = h1;
      }
      if (w1 < w)
      {
        w = w1;
      }

      return new Point(w, h);
    }

    /// <summary>
    /// 動画を表示する位置を算出します。
    /// </summary>
    /// <returns>動画の表示位置</returns>
    private Point CalcDisplayPoint(Point size)
    {
      // コントロールのサイズ取得
      var h = this.Height;
      var w = this.Width;

      var x = 0;
      var y = 0;

      // 動画の幅とコントロールの幅を比較し動画のサイズが小さい場合、オフセット位置を算出する
      if (size.X < w)
      {
        x = (w - size.X) / 2;
      }
      // 動画の高さとコントロールの高さを比較し動画のサイズが小さい場合、オフセット位置を算出する
      if (size.Y < h)
      {
        y = (h - size.Y) / 2;
      }

      return new Point(x, y);
    }

    /// <summary>
    /// 画像を補間処理します。
    /// </summary>
    private void ImageCorrection()
    {
      if (String.IsNullOrEmpty(this.picBackground.ImageLocation)) { return; }

      using (var image = new Bitmap(this.picBackground.ImageLocation))
      {
        this.picBackground.Image = ResizeImage(image, this.picBackground.Width, this.picBackground.Height);
      }
    }

    /// <summary>
    /// 画像リサイズ
    /// </summary>
    /// <param name="image">画像</param>
    /// <param name="width">表示領域の幅</param>
    /// <param name="height">表示領域の高さ</param>
    /// <returns>リサイズを行ったビットマップ画像</returns>
    private Image ResizeImage(Image image, int width, int height)
    {
      // 画像のサイズを算出
      var w = image.Width;
      var h = image.Height;

      var s = System.Math.Min(width / w, height / h);
      var sW = (int)(s * w);
      var sH = (int)(s * h);

      // リサイズした画像を生成する
      var result = new Bitmap(width, height);
      using (var g = Graphics.FromImage(result))
      {
        g.InterpolationMode = InterpolationMode.HighQualityBicubic;
        g.DrawImage(image, (width - sW) / 2, (height - sH) / 2, sW, sH);
        g.Dispose();
      }

      return result;
    }
    #endregion
  }
}

 Windowsのデフォルトだとコーデックがまったくインストールされていないため、WMVやAVIくらいしか再生できませんので、フリーのコーデックパックを使用すると良いでしょう。