juhara.com
Language : English Indonesia

Multimedia Player with DirectShow Part 1

Zamrony P Juhara
15 September 2006 16:28:00
 (5720 views)
First part of tutorial that explains how to use DirectShow, one of DirectX component, to create multimedia player with Delphi

Introduction

DirectShow is one of DirectX component useful for multimedia streaming. This tutorial is first part of creating TV tuner application tutorial (inspired by question posted by mikroplus in Delphindo mailing list). Because creating TV Tuner with DirectShow needs knowledge about Directshow, so for first part, this tutorial discusses basic steps to create DirectShow application. To create simple application with DirectShow is not so hard.

What do you need?

You need following softwares:

  • Brain.
  • DirectX version 8 or newer.
  • Delphi compiler. I use Delphi 7.
  • DirectX header conversion. You can download DirectX 9 header conversion or DirectX 8.1 header conversion. I use DirectX 8.1 header conversion.

COM Initialization

To initialize COM, we callCoInitialize(). For example:

CoInitialize(nil);

CoInitialize() is declared in Activex.pas unit.

Creating Filter Graph Manager.

DirectShow uses filter graph (or simply filter) to refer to software components that operate on multimedia data. Filter graph manager is manager of filters.

To create Filter Graph Manager, we can use CoCreateInstance() function, declared in activex.pas

For example:

var FFilterGraph:IGraphBuilder;

 CoCreateInstance(CLSID_FilterGraph,nil,
                  CLSCTX_INPROC_SERVER,
                  IID_IGraphBuilder,
                  FFilterGraph);

CLSID_FilterGraph is classID for filter graph manager (declared in Directshow.pas unit), CLSCTX_INPROC_SERVER indicates that we use in-process COM server (DirectX is in-process COM server). IID_IGraphBuilder is interface ID for IGraphBuilder. IGraphiBuilder interface is interface of filter graph manager. If succeed, FFilterGraph will be filled with address of interface IGraphBuilder instance, otherwise it will be filled with nil.

Creating filter graph and add it to Filter graph manager.

DirectShow has many built-in filters, on of it is filter reader to read multimedia file. To read multimedia file and play it, IGraphBuilder has RenderFile() method useful for constructing filter graph and add filter graph to filter graph manager automatically.

var FFilename:string;
    wFilename:widestring;
begin

  wFilename:=WideString(FFilename);
  FFilterGraph.RenderFile(PWideChar(wFilename),nil);
end;

RenderFile() expects filename to be rendered. It should be type of PWideChar (2 byte per 1 character), therefore, if we use string, it must be typecasted to widestring. Second parameter is PlayList. This parameter is reserved and must be set to nil.

RenderFile doesn't clear existing filter graph. If we call RenderFile twice then all filter will be played simultaneous. This behaviour may not what we expected.

Removing filter graph from filter graph manager.

IGraphBuilder has RemoveFilter method to remove a filter from filter graph manager. For example:

FFilterGraph.RemoveFilter(aFilter);

Where aFilter is filter graph type of IBaseFilter that we want to delete. To get list of filters in filter graph manager, we use enumFilters().

FFilterGraph.EnumFilters(enum);

enum is type of IEnumFilters. This interface has method called Next to access next filter. For example

while (enum.Next(1,afilter,@totRead)=S_OK) do

begin
  //do something with filter
end;

First parameter is number of filter we want to retrieve. In example above, we take filter one by one. afilter is filter type of IBaseFilter, totRead is address of variable that will filled with actual filter retrieved. According to Microsoft, afilter is array of IBaseFilter and its total element is equal to number of filter requested, but ub its declaration in DirectShow.pas, its type is IBaseFilter.

To remove all filter:

while (enum.Next(1,afilter,@totRead)=S_OK) do
begin
  FFilterGraph.RemoveFilter(aFilter);
  enum.Reset;
end;

After a filter is deleted, enum is no longer consistent with content of filter graph (out of sync). To make it consistent, we need to reset it.

Get other interfaces from filter graph manager.

Other interfaces we need is IMediaControl useful for controlling multimedia streaming process. IMediaSeeking is useful for seeking position in multimedia stream. IMediaEvent and IMediaEventEx.is used for event notification. To get those interfaces (except IMediaEventEx), we query from IGraphBuilder interface. For example:

var FMediaControl:IMediaControl;
    aEvent:IMediaEvent;
    FMediaEvent:IMediaEventEx;
    FMediaSeek:IMediaSeek;

FFilterGraph.QueryInterface(IID_IMediaControl,FMediaControl);
FFilterGraph.QueryInterface(IID_IMediaEvent,aEvent);
aEvent.QueryInterface(IID_IMediaEventEx,FMediaEvent);
FFilterGraph.QueryInterface(IID_IMediaSeeking,FMediaSeek);

For IMediaEventEx, we don't quiery it from IGraphBuilder. IMediaEventEx is an extension of IMediaEvent and must be queried from IMediaEvent.

Sending DirectShow Event Notification Request.

To let us know what is currently going on, we need to tell DirectShow that we want to get notified. We use SetNotifyWindow() method of IMediaEventEx. For example:

FMediaEvent.SetNotifyWindow(aHandle,WM_MMNOTIFY,integer(self));

aHandle is window window that will receive WM_MMNOTIFY message. WM_MMNOTIFY is our own custom message we define. Custom message should be between WM_APP-WM_APP+$BFFF.

const WM_MMNOTIFY=WM_APP+$1234;

Don't forget to add messages.pas unit. Third parameter is our own data. This parameter will get passed to message handler via lParam of WM_MMNOTIFY. In above example, we send address of class instance.

Processing event

Create and event handler for WM_MMNOTIFY, for example:

procedure WM_MMNotify(var msg:TMessage);message WM_MMNOTIFY;

and in its implementation

procedure TfrmMediaPlayer.WM_MMNotify(var msg: TMessage);
var aplayer:TBasicPlayer;
    evCode,param1,param2:integer;
begin
  aplayer:=TBasicPlayer(msg.LParam);
  aplayer.EventObj.GetEvent(evCode,param1,param2,0);
  case evCode of

    EC_COMPLETE:begin
                  //lakukan sesuatu
                end;
  end;
  aplayer.EventObj.FreeEventParams(evCode,param1,param2);
end;

msg.lParam will hold address of class instance (see "Sending DirectShow Event Notification Request" above).

IMediaEventEx has GetEvent() useful to get code of event occured and its parameters. Because this function allocates memory for parameters of event, we must call FreeEventParams() to avoid memory leak. For complete event code, you can look at DirectX documentation. I use EC_COMPLETE very often. It tells us when data streaming is complete.

Controlling Data Streaming.

IMediaControl is used to control flow of multimedia data.

To start streaming, we use Run.

FMediaControl.Run;

To stop streaming, we use Stop, for example

FMediaControl.Stop;

To pause,

FMediaControl.Pause;

Seeking Position in Stream

We use IMediaSeeking to seek out position in multimedia stream. With IMediaSeeking, we can play stream from any position. From beginning of stream, from middle or else. Directshow uses two position terms for seeking, i.e, current position and stop position. Current position is current position of streaming, while stop position is position where streaming will be stopped. To get current position and stop position, we use GetPositions().

FMediaSeek.GetPositions(currentPos,
                        StopPos);

To set current position and stop position, we use SetPositions().

FMediaSeek.SetPositions(CurrentPos,
                        AM_SEEKING_AbsolutePositioning,
                        StopPos,
                        AM_SEEKING_AbsolutePositioning);
  • AMSEEKING_AbsolutePositioning is flag that indicates that curentPos and stopPos are absolute position from beginning of stream.
  • AMSEEKING_RelativePositioning indicates currentPos and stopPos are relative to previous current and stop position.
  • AMSEEKING_NoPositioning indicates position is not changes, we use if we want to change one of them (current or stop) and the other is remain unchanged.

To get total duration of stream, we use GetDuration().

FMediaSeek.GetDuration(durasi);

CurrentPos, StopPos and durasi are type of int64.

Free memory and uninitialize COM

//remove all filter from filter graph manager
RemoveAllFilters;
FMediaControl:=nil;
FMediaEvent:=nil;
FMediaSeeking:=nil;
FFilterGraph:=nil;
CoUninitialize;

Below is an eaxample of multimedia player implementation, it still not complete and have not yet been tried for all media file types. File formats that have been tested are WAV, MP3, MIDI, MPEG, AVI, WMV,WMA, ASF.

unit uDirectShowPlayer;


interface

uses classes,windows,messages,directShow,controls;

const WM_MMNOTIFY=WM_APP+$1234;

type TPlayPosition=record
       Current:int64;
       Stop:int64;
     end;

     TBasicPlayer=class(TObject)
     private
       FFilterGraph:IGraphBuilder;
       FMediaControl:IMediaControl;
       FMediaEvent:IMediaEventEx;
       FMediaSeek:IMediaSeeking;       

       FHandle: HWND;
       procedure SetHandle(const Value: HWND);

       function GetDuration: int64;
       function GetPosition: TPlayPosition;
       procedure SetPosition(const Value: TPlayPosition);
       procedure SetControl(const Value: TWinControl);
     protected
       procedure SetWindow(const ahandle:HWND);

       procedure SetNotifyWindow(const ahandle:HWND);
     public
       constructor Create;
       destructor Destroy;override;

       procedure BuildFilterGraph;virtual;abstract;
       procedure RemoveAllFilters;

       procedure Run;
       procedure Stop;
       procedure Pause;
       procedure Rewind;

     published
       property Handle:HWND read FHandle write SetHandle;

       property GraphObj:IGraphBuilder read FFilterGraph;
       property ControlObj:IMediaControl read FMediaControl;
       property EventObj:IMediaEventEx read FMediaEvent;
       property SeekObj:IMediaSeeking read FMediaSeek;

       property Position:TPlayPosition read GetPosition write SetPosition;
       property Duration:int64 read GetDuration;
     end;

     TMMPlayer=class(TBasicPlayer)
     private
       FFilename: string;
       procedure SetFilename(const Value: string);
     public
       procedure BuildFilterGraph;override;
     published
       property Filename:string read FFilename write SetFilename;
     end;


function SetPlayPosition(const curr,stop:int64):TPlayPosition;

implementation

uses sysutils,activeX;

function SetPlayPosition(const curr,stop:int64):TPlayPosition;
begin
  result.Current:=curr;
  result.Stop:=stop;
end;

{ TBasicPlayer }

constructor TBasicPlayer.Create;

var aEvent:IMediaEvent;
begin
  CoCreateInstance(CLSID_FilterGraph,nil,
                   CLSCTX_INPROC_SERVER,
                   IID_IGraphBuilder,FFilterGraph);

  if FFilterGraph=nil then
     raise Exception.Create('Filter graph manager initialization failed');

   FFilterGraph.QueryInterface(IID_IMediaControl,FMediaControl);
   FFilterGraph.QueryInterface(IID_IMediaEvent,aEvent);
   aEvent.QueryInterface(IID_IMediaEventEx,FMediaEvent);
   FFilterGraph.QueryInterface(IID_IMediaSeeking,FMediaSeek);
end;

destructor TBasicPlayer.Destroy;
begin
  RemoveAllFilters;

  FFilterGraph:=nil;
  FMediaControl:=nil;
  FMediaEvent:=nil;
  FMediaSeek:=nil;
  inherited;

end;

procedure TBasicPlayer.Run;
begin
  FMediaControl.Run;
end;

procedure TBasicPlayer.Stop;
begin
  FMediaControl.Stop;
end;

procedure TBasicPlayer.Pause;
begin
  FMediaControl.Pause;

end;

procedure TBasicPlayer.SetHandle(const Value: HWND);
begin
  if FHandle<>Value then
  begin
    FHandle := Value;
    SetWindow(FHandle);
  end;
end;

procedure TBasicPlayer.SetWindow(const aHAndle: HWND);

begin
  SetNotifyWindow(AHandle);
end;

procedure TBasicPlayer.SetNotifyWindow(const ahandle: HWND);
begin
  FMediaEvent.SetNotifyWindow(aHandle,WM_MMNOTIFY,integer(self));
end;

function TBasicPlayer.GetDuration: int64;
begin
  FMediaSeek.GetDuration(result);
end;


function TBasicPlayer.GetPosition: TPlayPosition;
begin
  FMediaSeek.GetPositions(result.current,
                          result.Stop);
end;

procedure TBasicPlayer.SetPosition(const Value: TPlayPosition);
var apos:TPlayPosition;
begin
  apos:=value;
  FMediaSeek.SetPositions(aPos.Current,
                          AM_SEEKING_AbsolutePositioning,
                          aPos.Stop,
                          AM_SEEKING_AbsolutePositioning);
end;

procedure TBasicPlayer.Rewind;

begin
  SetPosition(SetPlayPosition(0,GetDuration));
end;


procedure TBasicPlayer.RemoveAllFilters;
var enum:IEnumFilters;
    aFilter:IBaseFilter;
    totread:cardinal;
begin
  totRead:=0;
  Stop;
  FFilterGraph.EnumFilters(enum);
  while (enum.Next(1,afilter,@totRead)=S_OK) do
  begin
    FFilterGraph.RemoveFilter(aFilter);
    enum.Reset;
  end;
  enum:=nil;

end;

{TMMPlayer}
procedure TMMPlayer.BuildFilterGraph;
var wFilename:widestring;
begin
  wFilename:=WideString(FFilename);
  FFilterGraph.RenderFile(PWideChar(wFilename),nil);
end;


procedure TMMPlayer.SetFilename(const Value: string);
begin
  FFilename := Value;
end;

initialization
CoInitialize(nil);

finalization
CoUnInitialize;
end.

Following code is example application built using TMMPlayer class.

unit ufrmMain;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs,DirectShow, StdCtrls, ExtCtrls,
uDirectShowPlayer, ComCtrls;

type

  TfrmMediaPlayer = class(TForm)
    btnOpen: TButton;
    OpenDialog1: TOpenDialog;
    btnPlay: TButton;
    btnStop: TButton;
    Timer1: TTimer;
    ProgressBar1: TProgressBar;
    lblProgress: TLabel;
    btnPause: TButton;
    procedure btnOpenClick(Sender: TObject);
    procedure btnPlayClick(Sender: TObject);
    procedure btnStopClick(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure btnPauseClick(Sender: TObject);
  private
    FDuration:int64;
    FMMPlayer:TMMPlayer;

    procedure WM_MMNotify(var msg:TMessage);message WM_MMNOTIFY;

    { Private declarations }
  public
    constructor Create(AOwner:TComponent);override;
    destructor Destroy;override;
    { Public declarations }
  end;


var
  frmMediaPlayer: TfrmMediaPlayer;

implementation

{$R *.dfm}

{ TForm1 }

constructor TfrmMediaPlayer.Create(AOwner: TComponent);
begin
  inherited;
  FMMPlayer:=TMMPlayer.Create;
  FMMPlayer.Handle:=Handle;
end;

destructor TfrmMediaPlayer.Destroy;
begin
  FMMPlayer.Free;
  inherited;
end;


procedure TfrmMediaPlayer.WM_MMNotify(var msg: TMessage);
var aplayer:TBasicPlayer;
    evCode,param1,param2:integer;
begin
  aplayer:=TBasicPlayer(msg.LParam);
  aplayer.EventObj.GetEvent(evCode,param1,param2,0);
  case evCode of
    EC_COMPLETE:begin
                  Timer1.Enabled:=false;
                  aplayer.Stop;
                  aplayer.Rewind;
                  ProgressBar1.Position:=0;
                  lblProgress.Caption:='0%';
                end;
  end;
  aplayer.EventObj.FreeEventParams(evCode,param1,param2);

end;

procedure TfrmMediaPlayer.btnOpenClick(Sender: TObject);
begin
  if opendialog1.Execute then
  begin
    FMMPlayer.Stop;
    FMMPlayer.Rewind;
    FMMPlayer.RemoveAllFilters;
    ProgressBar1.Position:=0;
    lblProgress.Caption:='0%';

    FMMPlayer.Filename:=opendialog1.FileName;
    FMMPlayer.BuildFilterGraph;
    FDuration:=FMMPlayer.Duration;
  end;
end;

procedure TfrmMediaPlayer.btnPlayClick(Sender: TObject);
begin
  Timer1.Enabled:=true;
  FMMPlayer.Run;
end;


procedure TfrmMediaPlayer.btnStopClick(Sender: TObject);
begin
  Timer1.Enabled:=false;
  FMMPlayer.Stop;
  FMMPlayer.Rewind;
  ProgressBar1.Position:=0;
  lblProgress.Caption:='0%';
end;

procedure TfrmMediaPlayer.Timer1Timer(Sender: TObject);
begin
  ProgressBar1.Position:=round(FMMPlayer.Position.Current/FDuration*ProgressBar1.Max);
  lblProgress.Caption:=inttostr(ProgressBar1.Position)+'%';
end;

procedure TfrmMediaPlayer.btnPauseClick(Sender: TObject);
begin
  FMMPlayer.Pause;
end;


end.

Below is DFM of form above

object frmMediaPlayer: TfrmMediaPlayer
  Left = 192
  Top = 127
  BorderStyle = bsDialog
  Caption = 'Simple MediaPlayer DirectShow'
  ClientHeight = 84
  ClientWidth = 681
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'MS Sans Serif'
  Font.Style = []
  OldCreateOrder = False
  PixelsPerInch = 96
  TextHeight = 13
  object lblProgress: TLabel
    Left = 408
    Top = 48
    Width = 51
    Height = 13
    Caption = 'lblProgress'
  end
  object btnOpen: TButton
    Left = 16
    Top = 16
    Width = 75
    Height = 25
    Caption = 'Open'
    TabOrder = 0
    OnClick = btnOpenClick
  end

  object btnPlay: TButton
    Left = 96
    Top = 16
    Width = 75
    Height = 25
    Caption = 'Play'
    TabOrder = 1
    OnClick = btnPlayClick
  end
  object btnStop: TButton
    Left = 16
    Top = 48
    Width = 75
    Height = 25
    Caption = 'Stop'
    TabOrder = 2
    OnClick = btnStopClick
  end
  object ProgressBar1: TProgressBar
    Left = 200
    Top = 16
    Width = 441
    Height = 19
    Smooth = True
    TabOrder = 3
  end
  object btnPause: TButton
    Left = 96
    Top = 48
    Width = 75
    Height = 25
    Caption = 'Pause'
    TabOrder = 4
    OnClick = btnPauseClick
  end
  object OpenDialog1: TOpenDialog
    Left = 496
    Top = 48
  end
  object Timer1: TTimer
    Enabled = False
    Interval = 1
    OnTimer = Timer1Timer
    Left = 544
    Top = 48
  end
end

Download source code here.

Conclusion

We have discussed basic steps to utilize DirectShow, including creating filter graph manager, querying media control interface and media seeking interface and how to play multimedia multimedia such as audio file and movie with filter graph manager.

Related Article

Do you like this article? Help this website improve by donating. Any amounts is appreciated.

Or you can help by bookmarking this page. Delicious Bookmark this on Delicious