ホーム >プログラム >Delphi 6 ローテクTips

DirectShowを利用して再生・その6


前回はビデオの映像をビットマップとして取得できるようにしました。

今回はコマ送りと再生速度の変更を行います。
スロー再生やコマ送り、コマ戻しなどができればビデオのキャプチャもよりやりやすくなります。


コマ送り

コマ送り(フレーム移動)はフレーム間の平均時間を求めて現在位置の時間に足し引きして移動することで実現できます。
現在位置の時間に1フレーム分の時間を足して移動させれば次のフレームが表示されるし1フレーム分の時間を引いて移動させれば一つ前のフレームが表示されるということです。
単純です。

フレーム間の平均時間はフレームレートの取得で詳しく述べていますが、ビデオレンダラー(VMR7)の入力ピンのメディアタイプを取得しそのフォーマットタイプを調べてそのうちのAvgTimePerFrameの値がフレーム間の平均時間です。

//--- フレームレート ---
function _Round(fNum: Double): Integer;
//fNumを四捨五入して返す。
//Delphiのヘルプからコピー。
begin
  if (fNum >= 0) then begin
    Result := Trunc(fNum + 0.5);
  end else begin
    Result := Trunc(fNum - 0.5);
  end;
end;

function TForm1.F_GetFrameRate: String;
var
  li_Time     : Int64;
  l_EnumPins  : IEnumPins;
  l_Pin       : IPin;
  l_PinInfo   : TPinInfo;
  l_MediaType : TAMMediaType;
begin
  Result := '-';
  F_fAvgTimePerFrame := 0;

  if (F_VideoRenderer = nil) then begin
    Exit;
  end;

  F_VideoRenderer.EnumPins(l_EnumPins);
  try
    while(l_EnumPins.Next(1, l_Pin, nil) = S_OK) do begin
      l_Pin.QueryPinInfo(l_PinInfo);
      try
        if (l_PinInfo.dir = PINDIR_INPUT) then begin
          //入力
          l_Pin.ConnectionMediaType(l_MediaType);
          try
            if (IsEqualGUID(l_MediaType.majortype, MEDIATYPE_Video)) then begin
              if (IsEqualGUID(l_MediaType.formattype, FORMAT_VideoInfo)) then begin
                //VIDEOINFOHEADER
                li_Time := PVideoInfoHeader(l_MediaType.pbFormat)^.AvgTimePerFrame;
              end else if (IsEqualGUID(l_MediaType.formattype, FORMAT_VideoInfo2)) then begin
                //VIDEOINFOHEADER2
                li_Time := PVideoInfoHeader2(l_MediaType.pbFormat)^.AvgTimePerFrame;
              end else if (IsEqualGUID(l_MediaType.formattype, FORMAT_MPEGVideo)) then begin
                //MPEG1VIDEOINFO
                li_Time := PMpeg1VideoInfo(l_MediaType.pbFormat)^.hdr.AvgTimePerFrame;
              end else if (IsEqualGUID(l_MediaType.formattype, FORMAT_MPEG2Video)) then begin
                //MPEG2VIDEOINFO
                li_Time := PMpeg2VideoInfo(l_MediaType.pbFormat)^.hdr.AvgTimePerFrame;
              end else begin
                li_Time := 0;
              end;
              if (li_Time <> 0) then begin
                F_fAvgTimePerFrame := li_Time / UNITS;
                Result := Format('%g', [_Round(100 / F_fAvgTimePerFrame) / 100]);
              end;
              Break;
            end;
          finally
            FreeMediaType(@l_MediaType);
          end;
        end;
      finally
        if (l_PinInfo.pFilter <> nil) then begin
          l_PinInfo.pFilter := nil;
        end;
        l_Pin := nil;
      end;
    end;
  finally
    if (l_EnumPins <> nil) then begin
      l_EnumPins := nil;
    end;
  end;
end;

この関数は'29.97'や'59.94'などのフレームレートの表示用の文字列を取得する関数なのですが、この中でF_fAvgTimePerFrameにセットしている値がフレーム間の平均時間で、単位は秒です。
59.97fpsの動画なら0.0166833、30fpsの動画なら0.0333333、29.97fpsの動画なら0.033375などの値(に近い値)になります。

//--- フレーム移動 ---
procedure TForm1.Button_FrameBackClick(Sender: TObject);
//前のフレーム。
var
  lf_Pos : TRefTime;
begin
  Button_PauseClick(nil);
  if (F_fAvgTimePerFrame = 0) then begin
    //F_fAvgTimePerFrameが0の場合暫定で29.97fpsのビデオであるとする。
    F_fAvgTimePerFrame := 1 / 29.97;
  end;
  F_MediaPosition.get_CurrentPosition(lf_Pos);
  F_MediaPosition.put_CurrentPosition(lf_Pos - F_fAvgTimePerFrame);
end;

procedure TForm1.Button_FrameNextClick(Sender: TObject);
//次のフレーム。
var
  lf_Pos : TRefTime;
begin
  Button_PauseClick(nil);
  if (F_fAvgTimePerFrame = 0) then begin
    //F_fAvgTimePerFrameが0の場合暫定で29.97fpsのビデオであるとする。
    F_fAvgTimePerFrame := 1 / 29.97;
  end;
  F_MediaPosition.get_CurrentPosition(lf_Pos);
  F_MediaPosition.put_CurrentPosition(lf_Pos + F_fAvgTimePerFrame);
end;
//--------

フレーム間の平均時間であるF_fAvgTimePerFrameを現在位置の時間から引いて移動すれば前のフレーム、足せば次のフレームになります。
F_fAvgTimePerFrameを5倍すればそれぞれ5フレーム前、5フレーム後のフレームが表示されます。
目的のフレーム位置まで移動するシーク操作があるので速度は遅いです。
高解像度の動画で非力なPCだと暫く無反応になることもあります。

IVideoFrameStepインターフェースを使うと動画フォーマットが対応していればもっとスムーズな表示ができます。
まずフレーム移動ができるか調べます。
IVideoFrameStepインターフェースのCanStepメソッドの第一引数にステップ数を指定して調べます。
1なら次のフレーム、-1なら一つ前のフレームになります。
フレーム移動可能であればIVideoFrameStepインターフェースのStepメソッドに移動するフレーム数を指定して呼ぶことでフレーム移動できます。

とはいえ現状大抵の動画ファイルは1以上ならOKでも-1以下だとNGになると思います。
つまりコマ送りはできるけれどもコマ戻しはIVideoFrameStepインターフェースでは現状できないということになります。
その結果コマ送りはスムーズにできるけれどもコマ戻しは上記のフレーム間の平均時間(F_fAvgTimePerFrame)を使ってシーク移動する方法をとらざるを得ないので遅くなってしまいます。

function TForm1.F_CanStep(iStep: Integer): Boolean;
var
  l_VideoFrameStep : IVideoFrameStep;
begin
  Result := False;
  if (F_GraphBuilder <> nil) then
  begin
    F_GraphBuilder.QueryInterface(IVideoFrameStep, l_VideoFrameStep);
    if (l_VideoFrameStep <> nil) then
    begin
      Result := (l_VideoFrameStep.CanStep(iStep, nil) = S_OK);
      l_VideoFrameStep := nil;
    end;
  end;
end;

procedure TForm1.Button_FrameNextClick(Sender: TObject);
//次のフレーム。
var
  l_VideoFrameStep : IVideoFrameStep;
  lb_Step          : Boolean;
  lf_Pos           : TRefTime;
begin
  lb_Step := False;
  if (F_CanStep(1)) then
  begin
    F_GraphBuilder.QueryInterface(IVideoFrameStep, l_VideoFrameStep);
    if (l_VideoFrameStep <> nil) then
    begin
      lb_Step := (l_VideoFrameStep.Step(1, nil) = S_OK);
      l_VideoFrameStep := nil;
    end;
  end;
  if (lb_Step) then
  begin
    //フレーム移動成功なので以下の処理を飛ばす
    Exit;
  end;

  //↑でうまくいかなかった場合
  Button_PauseClick(nil);
  if (F_fAvgTimePerFrame = 0) then begin
    //F_fAvgTimePerFrameが0の場合暫定で29.97fpsのビデオであるとする。
    F_fAvgTimePerFrame := 1 / 29.97;
  end;
  F_MediaPosition.get_CurrentPosition(lf_Pos);
  F_MediaPosition.put_CurrentPosition(lf_Pos + F_fAvgTimePerFrame);
end;

再生速度変更

再生速度の変更はIMediaPositionインターフェースのput_Rateメソッドで行います。
等倍なら1を、2倍速なら2を、半分の速度なら0.5を指定します。
TWindowsMediaPlayerの場合と違いwmaとwmvファイルの速度の変更はできないようです。
逆にTWindowsMediaPlayerではできなかったmp4やflvファイルの速度の変更はできます。
また0.5倍未満の速度(例えば0.1倍)であっても音が出ます(TWindowsMediaPlayerでは0.5倍未満は音はでません)。

//--- 再生速度変更 ---
procedure TForm1.MenuItem_Rate_10Click(Sender: TObject);
//wmv,wma,amazonのmp3は再生速度の変更はできない
var
  li_Ret  : HResult;
  ls_Err  : String;
  ls_Msg  : String;
  lf_Rate : Double;
  i       : Integer;
begin
  if (Sender is TMenuItem) then begin
    TMenuItem(Sender).Checked := True;
  end;

  if (F_MediaPosition = nil) then begin
    Exit;
  end;

  lf_Rate := 1.0;
  for i := 0 to MenuItem_Rate.Count -1 do begin
    if (MenuItem_Rate.Items[i].Checked) then begin
      lf_Rate := StrToFloatDef(MenuItem_Rate.Items[i].Caption, 1.0);
      Break;
    end;
  end;

  li_Ret := F_MediaPosition.put_Rate(lf_Rate);

  case li_Ret of
    S_OK
    :begin
      ls_Err := 'S_OK';
      ls_Msg := '再生速度変更'
    end;
    E_NOTIMPL
    :begin
      ls_Err := 'E_NOTIMPL';
      ls_Msg := '指定された機能が実装されていません。';
    end;
    E_INVALIDARG
    :begin
      ls_Err := 'E_INVALIDARG';
      ls_Msg := '指定したレートは、0 または負の値だった。';
    end;
    E_POINTER
    :begin
      ls_Err := 'E_POINTER';
      ls_Msg := 'NULL ポインタ引数。';
    end;
    VFW_E_UNSUPPORTED_AUDIO
    :begin
      ls_Err := 'VFW_E_UNSUPPORTED_AUDIO';
      ls_Msg := 'オーディオ デバイスあるいはフィルタがこのレートをサポートしていない。';
    end;
  end;

  debug_msg.ShowDbg(Format('%.1f 0x%.8x %s %s', [lf_Rate, li_Ret, ls_Err, ls_Msg]));
end;
//--------

再生速度の指定はメニューのCaptionに'1.0'や'2.0'や'0.5'などとしているのを利用しています。

ダウンロード

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