ホーム >プログラム >Delphi 6 ローテクTips >壁紙ビデオレンダラで壁紙ビデオ

壁紙ビデオレンダラで壁紙ビデオ

壁紙ビデオレンダラというDirectShowを使って壁紙ビデオを実現させているものを利用させてもらって壁紙ビデオを再生しましょうというページ。
これを使えばオーバーレイの効かない場合(他のプレーヤーでオーバーレイが使われている場合や無圧縮のAVIなど)であっても問題なく壁紙ビデオになります。


参考サイト


下準備

  1. 壁紙ビデオレンダラをダウンロードして解凍します。

    解凍先はパスの通っているフォルダである必要はありません。
    常時PCに繋がっているHDDであればどこでもOKです。
    極端なことを言えば取り外し可能なリムーバブルメディアであっても壁紙ビデオレンダラを使うときにPCに繋がっていれば(インストール時と同じドライブである必要はありますが)OKであったりもします。
    が、まぁ普通にPCに常時繋がっているHDDに専用ディレクトリを作って解凍するのが良いでしょう。
  2. 壁紙ビデオレンダラをシステムに登録します。

    コマンドプロンプトを起動して、壁紙ビデオレンダラフィルタ(wallpapr.ax)を解凍したフォルダにcdコマンドなどを駆使して移動します。
    そして以下のように記述してEnterキーを押します。
    regsvr32 wallpapr.ax
    あるいはコマンドプロンプトを起動してregsvr32 (後ろに半角スペースあり)と入力しておいてエクスプローラから解凍してできたwallpapr.axをコマンドプロンプトへドラッグアンドドロップしてEnterキーを押します。

    逆に壁紙ビデオレンダラをシステムから取り除くにはコマンドプロンプトからwallpapr.axのあるフォルダに移動して
    regsvr32 /u wallpapr.ax
    というようにregsvr32に/uオプションをつけて実行します。
    あるいはregsvr32 /u にwallpapr.axのフルパスを指定して実行でもOKです。

    コマンドプロンプトを使うのが面倒なら上記のコマンドをインストール用バッチファイルとアンイストール用バッチファイルに書いてwallpapr.axファイルのあるフォルダに保存して実行すればOKです。
  3. 壁紙ビデオレンダラのタイプライブラリを取り込みます。

    壁紙ビデオの表示位置の設定用に必要になります。
    Delphiのメニューで「プロジェクト」→「タイプライブラリの取り込み」と進んで出てきたダイアログのリストの中からWallpaper Renderer Type Libraryを選択します。

    ちなみに、表示位置がデフォルトの「中央」だけで良ければこのタイプライブラリの取り込みは不要です。

    「ユニットの作成」ボタンを押すと「ユニットディレクトリ名」で指定したフォルダにWallpaperTypeLib_TLB.pasというファイルができるので、あらかじめライブラリパスの通ったフォルダを「ユニットディレクトリ名」に指定すると良いでしょう。
    ちなみに「インストール」を押してもいいのですが、コンポーネントパレットには登録されないのでユニットの作成で充分だと思います。
  4. DirectShowのヘッダーをDelphi用に翻訳したユニットを使えるようにします。

    DirectShowのヘッダーをDelphi用に翻訳したものはDSPackなどのDelphiでDirectShowを使うためのライブラリに入っています。
    ダウンロードして解凍したらDirectShow9.pasのあるフォルダ(src\Directx9)をDelphiのライブラリのパスに追加します。
    Delphiのメニューで「プロジェクト」→「オプション」で出てくるダイアログの「ディレクトリ/条件」タブの「検索パス」の欄に追加します。

これで準備OKです。

ちなみに下のサンプルプログラムのzipファイルには展開済みの壁紙ビデオレンダラとそのインストールとアンインストール用バッチファイル、そしてタイプライブラリを取り込んだWallpaperTypeLib_TLB.pasも入れてあります。

またDSPackの圧縮ファイルを解凍してできるファイルにはDirectX用のヘッダーユニットの他にもサンプルソースコードなどが色々あります。
中には私がやってるハードウェアオーバーレイを利用した壁紙ビデオと同じことをやってるものもあったりして驚いてしまいます。
(実装の仕方も似たようなもので、考えることは同じなんだなぁ、、と妙に納得してしまったり)

それらのサンプルを動かすにはDirectX9_Dx.dpkとDSPack_Dx.dpkの二つをコンパイルしたあとDSPackDesign_Dx.dpkをインストールする必要があります。
インストールのやり方はreadme.htmlに(英文ですが)書いてあります。Delphi 6の場合なら、

  1. ライブラリの検索パスにsrcフォルダ中のDirectx9とDSPackの二つのフォルダを追加します。
  2. packagesフォルダ中のDirectX9_D6.dpkを読み込んでコンパイル。
    インストールはしません。
  3. packagesフォルダ中のDSPack_D6.dpkを読み込んでコンパイル。
    これもインストールはしません。
  4. 最後にpackagesフォルダ中のDSPackDesign_D6.dpkを読み込んでコンパイルしてインストール。

これでOKです。
DirectX9_D6.dpkとDSPack_D6.dpkはコンパイルするだけでインストールはしません。
DSPackDesign_D6.dpkをインストールするだけです。
メディアプレーヤーのサンプルだけでなくDVDプレーヤーのサンプルやフィルターのサンプルなどもあるのでインストールしておいて損はないと思います。


とりあえず動かしてみる

wallpapr.axはDirectShowのフィルタなので、それを使うにはDirectShowを使ってプログラムしなければなりません。
敷居は高い感じがします。

が、参考サイトの「オーバーレイされない?」に壁紙ビデオレンダラの作者さんがDelphiでのプログラム例をあげてくれたのでそれを参考にあれこれやってみました。
結果。
DirecShowというのはただファイルを読み込んで再生させるだけなら意外と簡単なものでした。
MCIを使って再生させる感じに近いのでTWindowsMediaPlayerを使って再生させるよりも理解しやすいかも知れません。

DirectShowを調べてるとフィルターとかフィルターグラフとかレンダラーなど聞き慣れない単語が出てきて何のことやらでわけが分かりませんが、もうこの辺は慣れなのだと思います。
とりあえず私はフィルターというのは要するに部品ということなのだなと理解しています。
ソースフィルターは入力用の部品ということだしビデオレンダラーは画面出力用の部品ということなのであろうと。
で、それらの部品の組み合わせがフィルターグラフで、そうやって各部品を色々組み合わせてメディアを再生するというのがDirectShowのやり方なのだろうと。

サンプルコード

unit main;

interface
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls,
  DirectShow9;

const
  WM_GRAPH_NOTIFY = (WM_APP +1);

type
  TForm1 = class(TForm)
    RadioGroup1:   TRadioGroup;
    Button_Open:   TButton;
    Button_Play:   TButton;
    Button_Pause:  TButton;
    Button_Stop:   TButton;
    CheckBox_Mute: TCheckBox;
    OpenDialog1:   TOpenDialog;
    procedure FormCreate (Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button_OpenClick  (Sender: TObject);
    procedure Button_PlayClick  (Sender: TObject);
    procedure Button_PauseClick (Sender: TObject);
    procedure Button_StopClick  (Sender: TObject);
    procedure CheckBox_MuteClick(Sender: TObject);
  private
    { Private 宣言 }
    F_GraphBuilder : IGraphBuilder;
    F_Wallpaper    : IBaseFilter;
    F_MediaEventEx : IMediaEventEx;

    procedure WMGraphNotify(var Msg: TMessage); message WM_GRAPH_NOTIFY;
    procedure F_Close;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation
uses
  ActiveX,
  WallpaperTypeLib_TLB;

{$R *.dfm}


const
  //壁紙ビデオレンダラのGUID
  CLSID_Wallpaper: TGUID = '{9A1585D2-CECD-432D-B8AA-F1F91F217D47}';

procedure TForm1.FormCreate(Sender: TObject);
begin
  //グラフ作成
  CoCreateInstance(
    CLSID_FilterGraph,
    nil,
    CLSCTX_INPROC_SERVER,
    IID_IGraphBuilder,
    F_GraphBuilder
  );

  //イベント通知ウィンドウをセット。
  F_GraphBuilder.QueryInterface(IMediaEventEx, F_MediaEventEx);
  F_MediaEventEx.SetNotifyWindow(Self.Handle, WM_GRAPH_NOTIFY, 0);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  F_Close;
  F_MediaEventEx  := nil;
  F_Wallpaper     := nil;
  F_GraphBuilder  := nil;
end;

procedure TForm1.F_Close;
var
  l_MediaControl : IMediaControl;
  l_EnumFilters  : IEnumFilters;
  l_BaseFilter   : IBaseFilter;
begin
  if (F_GraphBuilder <> nil) then begin
    F_GraphBuilder.QueryInterface(IMediaControl, l_MediaControl);
    l_MediaControl.Stop;
    l_MediaControl := nil;

    //Sotpしただけではフィルタはまだ接続されたままなのでフィルタを列挙して削除する。
    //http://msdn.microsoft.com/ja-jp/library/cc973418.aspx
    F_GraphBuilder.EnumFilters(l_EnumFilters);
    while (l_EnumFilters.Next(1, l_BaseFilter, nil) = S_OK) do begin
      F_GraphBuilder.RemoveFilter(l_BaseFilter);
      l_BaseFilter := nil;
      l_EnumFilters.Reset;
    end;
    l_EnumFilters := nil;
    F_Wallpaper   := nil;
  end;
end;

procedure TForm1.WMGraphNotify(var Msg: TMessage);
var
  li_EventCode, li_Param1, li_Param2: Longint;
begin
  if not(Assigned(F_MediaEventEx)) then begin
    Exit;
  end;

  //イベントを全て取得
  while (Succeeded(F_MediaEventEx.GetEvent(li_EventCode, li_Param1, li_Param2, 0))) do begin
    case (li_EventCode) of
      EC_COMPLETE
      :begin
        //再生完了
        //繰り返し再生
        Button_StopClick(nil);
        Button_PlayClick(nil);
      end;
    end;
    F_MediaEventEx.FreeEventParams(li_EventCode, li_Param1, li_Param2);
  end;
end;

procedure TForm1.Button_OpenClick(Sender: TObject);
var
  l_WallConfig : IWallpaperConfig;
begin
  if (OpenDialog1.Execute) then begin
    F_Close;

    if (F_Wallpaper = nil) then begin
      {レンダラ追加}
      CoCreateInstance(
        CLSID_Wallpaper,
        nil,
        CLSCTX_INPROC_SERVER,
        IID_IBaseFilter,
        F_Wallpaper
      );
      if (F_Wallpaper = nil) then begin
        Beep;
        ShowMessage('壁紙ビデオレンダラが登録されていません');
        F_Close;
        Exit;
      end;
      F_GraphBuilder.AddFilter(F_Wallpaper, 'Video Renderer');
    end;

    {表示位置設定}
    F_Wallpaper.QueryInterface(IID_IWallpaperConfig, l_WallConfig);
    case RadioGroup1.ItemIndex of
      1:   l_WallConfig.SetStyle(WallpaperStyle_Tile);    //タイル
      2:   l_WallConfig.SetStyle(WallpaperStyle_Stretch); //全画面
      else l_WallConfig.SetStyle(WallpaperStyle_Center);  //中央(デフォルト)
    end;
    l_WallConfig := nil; //後始末

    if not(Succeeded(F_GraphBuilder.RenderFile(PWideChar(WideString(OpenDialog1.FileName)), nil))) then begin
      ShowMessage(OpenDialog1.FileName + #13'は開けません');
      Exit;
    end;

    CheckBox_MuteClick(nil); //ミュート
    Button_PlayClick(nil);   //再生
  end;
end;

procedure TForm1.Button_PlayClick(Sender: TObject);
//Play
var
  l_MediaControl: IMediaControl;
begin
  if (Assigned(F_GraphBuilder)) then begin
    F_GraphBuilder.QueryInterface(IMediaControl, l_MediaControl);
    l_MediaControl.Run;
    l_MediaControl := nil; //後始末
  end;
end;

procedure TForm1.Button_PauseClick(Sender: TObject);
//Pause
var
  l_MediaControl: IMediaControl;
begin
  if (Assigned(F_GraphBuilder)) then begin
    F_GraphBuilder.QueryInterface(IMediaControl, l_MediaControl);
    l_MediaControl.Pause;
    l_MediaControl := nil;
  end;
end;

procedure TForm1.Button_StopClick(Sender: TObject);
//Stop
var
  l_MediaControl  : IMediaControl;
  l_MediaPosition : IMediaPosition;
begin
  if (Assigned(F_GraphBuilder)) then begin
    F_GraphBuilder.QueryInterface(IMediaControl, l_MediaControl);
    l_MediaControl.Stop;

    //ただ停止させただけではPauseと変わらない
    //先頭に戻すにはIMediaPositionを使う
    F_GraphBuilder.QueryInterface(IMediaPosition, l_MediaPosition);
    l_MediaPosition.put_CurrentPosition(0);
    l_MediaPosition := nil;

    //移動させただけでは現在位置のフレームが描画されない。
    //現在位置のフレームを描画させるためにPauseあるいはStopWhenReadyを呼ぶ。
    l_MediaControl.StopWhenReady;
    l_MediaControl := nil;
  end;
end;

procedure TForm1.CheckBox_MuteClick(Sender: TObject);
//ミュート
var
  l_BasicAudio: IBasicAudio;
begin
  if (Assigned(F_GraphBuilder)) then begin
    F_GraphBuilder.QueryInterface(IID_IBasicAudio, l_BasicAudio);
    if (Assigned(l_BasicAudio)) then begin
      if (CheckBox_Mute.Checked) then begin
        l_BasicAudio.put_Volume(-10000);
      end else begin
        l_BasicAudio.put_Volume(0);
      end;
      l_BasicAudio := nil;
    end;
  end;
end;


end.

初期化

DirectShowを使うにはまずCOMを使えるようにする必要があるらしく、そのためにフォームのOnCreateイベントでCoInitializeを呼んで初期化して、終了時にOnDestroyイベントでCoUninitializeを呼んで終了処理を行うというのが定石なようです。
が、実はこれやらなくても問題なく動いてしまいます。

どうもこのCOMを使うための初期化というのはスレッドごとに一度行えばそれで良いものらしく、この例の場合だとF_iCoInitには常にS_FALSEが入ります。
どうやら既にどこかで一度CoInitializeが呼ばれているようなのですが、多分VCLのどこかの初期化部かなにかで呼ばれているのでしょうと。
で、CoInitializeとCoUninitializeはセットで行わないといけないのでCoInitializeの戻り値をF_iCoInitに保存しておき、終了時にその値をSucceeded関数に入れてTrueならCoUnInitializeを呼ぶ、というようにしておかないといけないようです。

※以前「終了時にその値を見てS_OKならCoUnInitializeを呼ぶ」としていましたがそれは間違いでした。

procedure TForm1.FormCreate(Sender: TObject);
begin
  F_iCoInit := CoInitialize(nil);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
//NG!  if (F_iCoInit = S_OK) then begin
  if (Succeeded(F_iCoInit)) then begin
    CoUninitialize;
  end;
end;

よくよく調べてみたらComObj.pasの中でこの初期化は行われており、ComObjはDirectShow9.pasで呼ばれているので、結局わざわざ自分で初期化処理を書く必要はないことが分かりました。
ということでサンプルコードではこの処理は省いています。

壁紙ビデオレンダラ作成

メインのルーチンです。
ここが肝であるといえます。
慣れないとわけが分かりませんが、慣れてしまえばなんてこともなく思えてきます。
この部分で壁紙ビデオレンダラーを作成してフィルターグラフに追加します。
この処理を行わないとデフォルトのビデオレンダラーが使われます。
壁紙ビデオレンダラがシステムに登録されていない場合にもデフォルトのビデオレンダラーが使われます。

procedure TForm1.Button_OpenClick(Sender: TObject);

  ...

    if (F_Wallpaper = nil) then begin
      {レンダラ追加}
      CoCreateInstance(
        CLSID_Wallpaper,
        nil,
        CLSCTX_INPROC_SERVER,
        IID_IBaseFilter,
        F_Wallpaper
      );
      if (F_Wallpaper = nil) then begin
        Beep;
        ShowMessage('壁紙ビデオレンダラが登録されていません');
        F_Close;
        Exit;
      end;
      F_GraphBuilder.AddFilter(F_Wallpaper, 'Video Renderer');
    end;

  ...

    //ファイルを開く
    F_GraphBuilder.RenderFile(PWideChar(WideString(OpenDialog1.FileName)), nil);

ファイルを開いたらIMediaControlインターフェースをIGraphBuilderから取得すればあとは再生させたり一時停止したりなどできます。

壁紙ビデオの表示位置設定

壁紙ビデオの表示位置をデフォルトの中央以外にしたい場合は表示位置の設定を行います。
そのためには設定用のインターフェースを取得して設定したい値をSetStyleメソッドに渡します。
設定が終わったら設定用インターフェースは必要なくなるので後始末しておきます。

   {表示位置設定}
    F_Wallpaper.QueryInterface(IID_IWallpaperConfig, l_WallConfig);
    case RadioGroup1.ItemIndex of
      1:   l_WallConfig.SetStyle(WallpaperStyle_Tile);    //タイル
      2:   l_WallConfig.SetStyle(WallpaperStyle_Stretch); //全画面
      else l_WallConfig.SetStyle(WallpaperStyle_Center);  //中央(デフォルト)
    end;
    l_WallConfig := nil; //後始末

連続再生

この処理はファイルを選択して一度再生して終わりで良ければ必要のない処理なのですが、まぁ普通は次のファイルを再生するとか連続して再生するとかしたいですよね。
ということで、メディアの再生が終了しましたよというメッセージイベントを受け取れるようにしなければなりません。
そのためにまずイベント通知用のメッセージの番号をWM_GRAPH_NOTIFYとして宣言しておきます。

WM_GRAPH_NOTIFYという識別子はエラーにならなければ何でも構いませんしその値は他と被らなければこれもまた何でも構いません。
もしWM_APP+1が他で使われているなら別の値にします。
そしてそのWM_GRAPH_NOTIFYメッセージを受け取れるようにメッセージハンドラを作成します。

const
  WM_GRAPH_NOTIFY = (WM_APP +1);

  ...

  private
    { Private 宣言 }
    F_MediaEventEx: IMediaEventEx;
    procedure WMGraphNotify(var Msg: TMessage); message WM_GRAPH_NOTIFY;

イベントを受け取った時の処理の例です。
今回は開くファイルをリストで管理していないので選択したファイルを延々連続再生させています。

procedure TForm1.WMGraphNotify(var Msg: TMessage);
var
  li_EventCode, li_Param1, li_Param2: Longint;
begin
  if not(Assigned(F_MediaEventEx)) then begin
    Exit;
  end;

  // イベントを全て取得
  while (Succeeded(F_MediaEventEx.GetEvent(li_EventCode, li_Param1, li_Param2, 0))) do begin
    case (li_EventCode) of
      EC_COMPLETE: begin
        //再生完了
        //必要ならリストを使って次のファイルを再生させる処理をここに書く
        Button_StopClick(nil);
        Button_PlayClick(nil);
      end;
    end;
    F_MediaEventEx.FreeEventParams(li_EventCode, li_Param1, li_Param2);
  end;
end;

イベント通知を受け取るようにするにはIMediaEventExインターフェースのSetNotifyWindowメソッドにメッセージを受け取るウィンドウのウィンドウハンドルとメッセージの番号(自分で宣言した上記のWM_GRAPH_NOTIFY)を渡します。
今回はプレーヤーのフォームでメッセージを受け取るようにしています。


  //イベント通知
  F_GraphBuilder.QueryInterface(IMediaEventEx, F_MediaEventEx);
  F_MediaEventEx.SetNotifyWindow(Self.Handle, WM_GRAPH_NOTIFY, 0);

問題点

  1. この壁紙ビデオレンダラはマルチモニタには対応していないようです。
    「タイル表示」や「全画面表示」はプライマリモニタ以外には表示されません。
    「中央に表示」で表示位置をセカンダリモニタなどの座標に設定した場合のみプライマリモニタ以外にも表示することができるようです。
  2. 「中央に表示」でビデオのサイズがモニタ画面以上ある場合はセカンダリモニタなどの画面にはみ出ます。
    例えばビデオサイズが1280x720でプライマリモニタの画面サイズが1024x768でセカンダリモニタがプライマリモニタの右横にある場合、セカンダリモニタの左側に128ドット分ビデオが表示されてしまいます。
  3. 「全画面に表示」の場合、元のビデオのアスペクト比とモニタ画面のアスペクト比が異なる場合ビデオの映像が歪みます。
    このあたりは壁紙の設定での「拡大して表示」と同じ要領です。
  4. 結構CPUパワーを食います。
    私のAtomなネットブックだと中央に表示で640x360で30fpsのwmvビデオ程度がギリギリな感じです。
    「タイル表示」や「全画面に表示」の場合などだと映像はスローモーションのようになり(再生速度が遅くなる感じ)、音はブツブツ途切れてしまいます。
    ※音がブツブツ途切れるのはwmvファイルだけのようです。
    その他のmp4やflvなどだと音は普通に流れます。
    音と映像の同期は取れずに音が終わってからしばらくは残りの映像が無音のまま流れ続けます。
    ※その後mp4の場合でもスプリッタにHaali Media Splitterを使う場合は音がブツブツ途切れることが分かりました。

解決策

3の「全画面に表示」で映像が歪んでしまう問題はffdshowを利用して対処する方法があります。
ビデオのデコードをすべてffdshowに任せるようにして壁紙ビデオレンダラに入力されるビデオのサイズをモニタ画面いっぱいになるようにあらかじめリサイズしてしまいます。
そのために壁紙ビデオレンダラの表示モードは「全画面に表示」ではなく「中央に表示」にします。
ffdshowの設定はちょっと手間がかかりますがコツを掴めば壁紙ビデオレンダラ以外にも色々応用がきくのでやってみて損はありません。

ffdshowのインストールはWindowsMediaPlayerでflvファイルを再生WindowsMediaPlayerでmp4ファイルを再生などを参考にどうぞ。
コーデックのページを選んでとりあえず選択できるものはすべてffdshowでデコードするように設定します。

「リサイズ、アスペクト」のページでビデオのサイズを画面サイズに合わせてしまいます。

  1. 「リサイズ」のチェックボックスにチェックを入れます。
  2. 「スクリーンの解像度にリサイズする」にチェックを入れます。
  3. 「常にリサイズする」にチェックを入れます。
  4. 「アスペクト比率」グループの「オリジナルのアスペクト比率を維持する」にチェックを入れます。

ちなみにこの設定ではビデオのアスペクト比とモニタ画面のアスペクト比が異なる場合はレターボックスのようにビデオの上下あるいは左右に黒帯が入ります。

画面イメージ。
モニタの画面アスペクト比とビデオのアスペクト比が違うので(この場合は)上下に黒帯が入ります。

この設定を行うと以後すべてのビデオがモニタのサイズにリサイズされてしまうので壁紙ビデオレンダラを使う場合のみリサイズするようにffdshowを設定します。
そのためにはリサイズの設定をプリセットとして登録しておいて壁紙ビデオレンダラを使うときだけそのプリセットを使ってデコード処理するようにします。

  1. 「プリセット」の「新規」ボタンを押します。
  2. 新しくできた(例えば)default1プリセット名を分かりやすいようにWallvideoなどの名前に変えます。
    ちなみにプリセット名は判別用のためなのでWallvideoでなくてもOKです。
  3. 「プリセットを自動的に読み込む」にチェックを入れます。
  4. 「プリセットの自動読み込みの条件」ボタンを押してこのプリセットを自動で読み込むための条件の設定を行います。

プリセットの自動読み込みの条件にDirectShowフィルタがフィルタグラフに存在するときが指定できるのでそれを利用します。

  1. 「条件が一つでも一致したとき(OR)」にチェックを入れます。
  2. 「DirectShowフィルタの存在」にチェックを入れます。
  3. フィルタ名のWallpaperを記入します。

これで壁紙ビデオレンダラで壁紙ビデオを表示するときだけWallvideoプリセットが読み込まれてモニタの画面サイズにリサイズが行われるようになります。
それ以外はdefaultプリセットが読み込まれてデコードされます。

ちなみにこのプリセットの自動読み込みの条件は様々に指定できるので今回の件に限らず色々と応用できます。

サンプルプログラム

wallvideo_wallpapr.zip ソースコードと実行ファイルの詰め合わせ。