Quantcast
Channel: Planet Object Pascal
Viewing all articles
Browse latest Browse all 1725

Delphi Bistro: Having fun with FireMonkey 2 multimedia components

$
0
0

Para leer este articulo en espanol haz click aqui.

The other day I started to play with the multimedia components included on FireMonkey. I went to the web and found a couple of demos from Embarcadero. Took my time to look at the demos and followed the steps to build the demo. Cool! It worked fine! No surprises there I suppose.

However, I got to think, can I do more? What if I hack together on a form two media players – can I have two media players on form? Can I play two media players at once?

So I put together a project like so and found the answers to my questions. Yes, yes and yes. Cool!

So I turned back and asked myself can I do even more? Can I put eight media players on a form and have them play all at once? Can I use live bindings to connect certain components? Can I use minimal code? After all having an event handler per object would be really clunky, hard to maintain and make me look bad.

So I set of to do the application. I started by deciding to create all 8 player controls at design time and make it all go thru one set of event handlers. I could have done one set and instantiated as many players as I needed, but that would have taken me away from my experiment goal. But I digress.

So I started by creating a FireMonkey HD project. Then dropped a TGridLayout and set the item sizes to a height of 250 and a width of 100. Next I dropped a TPanel, a TMediaPlayer, a couple of buttons, a couple of labels and a track bar. Made 7 more copies of the panel, placing a copy per cell of my TGridLayout.  After that I went off to fix all the component names to my liking and then set the tag property to match their player position. That way I could properly address the proper player I was supposed to service at any given request.

Double Player Main Form

Double Player Main Form at design time

Then I went on to create my base form. The base form contains a single function designed to find a component by name and return it to the caller. Let’s take a quick look at the code:

// Helper function that allows forms to find it's components by name and number
// - MediaPlayer and 8 will look for MediaPlayer8
//   and if it finds will return the component otw returns nil
function TfrmBase.GetObjectByName(objName: string; Number: integer): TComponent;
var
  ComponentName: String;
begin
  // Volume Commands can only came from TTrackbar
  ComponentName := objName + IntToStr(Number);
  Result := FindComponent(ComponentName);
end;

Pretty trivial stuff. Next I make my main form descend from the base form. Then I figure which events I needed to have in order to achieve my design goals and have a functional player. The events i need are as follows:

  • procedure HandleLoadMedia(Sender: TObject);
  • procedure btnPlayClick(Sender: TObject);
  • procedure HandleVolumeChangesByTrackbar(Sender: TObject);

Now that I know what is needed to make this project tick, I will got thru each procedure in detail. But before I do, I start connecting the track bars to the volume labels under the track bar. These labels will display a numeric representation of the volume. I bind the TTrackBar.Value to the Label.Text. I also need to use the round function to get only  integers displayed on my label.

Volume trackbar Binding

Live binding for the volume track bar

I will start by going over the procedure HandleLoadMedia:

//------------------------------------------------------------------------------------------------------------
// Loads media into the media player
procedure TForm1.HandleLoadMedia(Sender: TObject);
var
  MediaPlayer: TComponent;
  PlayButton: TComponent;
begin
  // Only supported files
  OpenDialog1.Filter := TMediaCodecManager.GetFilterString;
  if (OpenDialog1.Execute) then
  begin
    // Tries to find the proper media player to load media onto
    MediaPlayer := GetObjectByName(MEDIA_PLAYER, (Sender as TComponent).Tag);
    // Makes sure it found a media player component
    if Assigned(MediaPlayer) and (MediaPlayer is TMediaPlayer) then
    begin
      // Attempts to load media
      (MediaPlayer as TMediaPlayer).Clear;
      (MediaPlayer as TMediaPlayer).FileName := OpenDialog1.FileName;
      if (MediaPlayer as TMediaPlayer).State = TMediaState.Stopped then
      begin
        // If media is recognized as valid then it enables the play button
        PlayButton := GetObjectByName(PLAY_BUTTON, (Sender as TComponent).Tag);
        if Assigned(PlayButton) and (PlayButton is TButton) then
        begin
          (PlayButton as TButton).Enabled := True;
        end;
      end;
    end;
  end;
end;

This procedure is the event for the Load button. Taking advantage of the multimedia components I ask the framework to provide me with all the supported media types. Next I ask the user to let me know which file he or she wants to load. Once I obtain the media filename I proceed to locate the MediaPlayer using my base function GetObjectByName and then load it to the media player. Notice that I receive back a TComponent and It is my responsibility to ensure that an object was returned and that  object is of the right type. I also use the media player object to verify that the file I received from the user is indeed a valid media file. For that I check that the Media State is different than Unavailable. After ensuring that everything went OK I then enable the play button.

Now, let’s examine what is needed to play the media selected by the user. Let’s take a look at btnPlayClick event:

//------------------------------------------------------------------------------------------------------------
// Handles play button
procedure TForm1.btnPlayClick(Sender: TObject);
var
  MediaPlayer: TComponent;
begin
  // Tries to find the proper media player
  MediaPlayer := GetObjectByName(MEDIA_PLAYER, (Sender as TComponent).Tag);
  // Makes sure it found a media player component
  if Assigned(MediaPlayer) and (MediaPlayer is TMediaPlayer) then
  begin
  // Does it have a valid media attached?
  if (MediaPlayer as TMediaPlayer).State <> TMediaState.Unavailable  then
    // Figures if it needs to start or stop playback
    if (MediaPlayer as TMediaPlayer).State = TMediaState.Stopped  then
      (MediaPlayer as TMediaPlayer).Play
    else
      (MediaPlayer as TMediaPlayer).Stop;
  end;
end;

Again I locate the proper media player and make all the checks as described on the HandleLoadMedia event. Then I check the media state and I reverse it. If it is playing I’ll stop it otherwse I’ll start it. Notice that this operation pauses the playback. If you wanted to rewind the media you would have to set the media player time property to zero.

Lastly, let’s take a look at the HandleVolumeChangesByTrackbar event:

//------------------------------------------------------------------------------------------------------------
// Handles volume changes
procedure TForm1.HandleVolumeChangesByTrackbar(Sender: TObject);
var
  MediaPlayer: TComponent;
begin
  // Volume Commands can only come from TTrackbar
  if Sender is TTrackBar then
  begin
    MediaPlayer := GetObjectByName(MEDIA_PLAYER, (Sender as TComponent).Tag);

    if Assigned(MediaPlayer) and (MediaPlayer is TMediaPlayer) then
    begin
      (MediaPlayer as TMediaPlayer).Volume := (Sender as TTrackBar).Value/100;

      // Some controls send notifications when setting properties,
      // like TTrackBar
      if FNotifying = 0 then
      begin
        Inc(FNotifying);
        // Send notification to cause expression re-evaluation of dependent expressions
        try
          BindingsList1.Notify(Sender, '');
        finally
          Dec(FNotifying);
        end;
      end;
    end;
  end;
end;

Yet again I locate the proper media player and make all the checks as described on the HandleLoadMedia event. I then set the volume of the mediaplayer using the value of the track bar and then I send a notification to my binding list using the Sender object – the track bar. I found that if I do not do that my  volume label underneath my track bar will not get updated.

And last but not least I want my media player to update a status label at the top of each panel every time there is a change on it’s status. So I proceed to make the binding connections. I start by connecting the media player state property to the label’s text property. But then I get an error that tells me that there is no conversion between a TMediaState and Text. More specifically the error was: “EvalError in MediaPlayerStatus1: Unable to cast or find converters between types TMediaState and string.”. Dang! Now what?

Binding Error

Live binding error message when attempting to resolve MediaState to String at design time

Custom Output Converter to the rescue!

I go searching around the internet trying to look for a solution and can not find anything. So I decide to investigate the framework and how it does it. After a few minutes digging around the FMX library I have a better understanding of what needs to be done.

I start by creating a new unit on my project and adding the following code to it:

unit MediaPlayer.StateConversion;

interface

uses System.Classes, System.SysUtils, Data.Bind.Components, System.Bindings.Helper, System.Generics.Collections,
  FMX.Types, FMX.Media;

implementation

uses System.Bindings.EvalProtocol, System.Rtti, System.Bindings.Outputs, Fmx.Bind.Consts;

const
  sMediaPlayerStateToString = 'MediaPlayerStateToString';
  sThisUnit = 'MediaPlayer.StateConversion';
  sMediaPlayerStateToStringDesc = 'Assigns a Media Player State to a String';

procedure RegisterOutputConversions;
begin
  // Assign String from State
    TValueRefConverterFactory.RegisterConversion(TypeInfo(TMediaState), TypeInfo(string),
    TConverterDescription.Create(
        procedure(const InValue: TValue; var OutValue: TValue)
        begin
          Assert(InValue.IsType(TypeInfo(TMediaState)), 'Input needs to be MediaState');

          case TMediaState(InValue.AsOrdinal) of
          TMediaState.Unavailable:
            OutValue := TValue.From<string>('Unavailable');
          TMediaState.Playing:
            OutValue := TValue.From<string>('Playing');
          TMediaState.Stopped:
            OutValue := TValue.From<string>('Stopped');
          else
            OutValue := TValue.From<string>('UNKNOW!');
          end;
        end,
    sMediaPlayerStateToString,
    sMediaPlayerStateToString,
    sThisUnit, True, sMediaPlayerStateToStringDesc, FMX.Types.TStyledControl)  // fmx only
  );
end;

procedure UnregisterOutputConversions;
begin
  TValueRefConverterFactory.UnRegisterConversion(
    TypeInfo(TMediaState), TypeInfo(string));
end;

initialization
  RegisterOutputConversions;
finalization
  UnregisterOutputConversions;
end.

What this unit does is to register a Output Converter for the live bindings framework. More specifically one that takes a TMediaState as input and it outputs the equivalent string representation of such state.

The meat of this unit lies inside the procedure RegisterOutputConversions. It calls TValueRefConverterFactory.RegisterConversion with 3 parameters: the from type(), the to type and the converter (TConvertProc). A good unit to take a look at to further understand this is System.Bindings.Outputs.

Once you know what to use, providing the converter becomes a trivial programming task. My converter was defined in the parameter of the registration procedure. However, one can define it into a TConvertProc variable and so on. I should also point out that a good understanding of the RTTI helps a lot into writing solid converters.

I choose to place an assert at the top of the procedure to make sure during development nothing else would show up and break my converter later on.

So, whit that issue resolved I compile and run the project, and what do you know? It works. It sounds really weird but it works. It work on my Windows 7 VM and on my Mac. Cool!

I also realize that the media players only update the labels once, when the form is created. In order to resolve that I use an timer so that every second I go around the media players and issue a BindingsList1.Notify. That seems to work perfectly. I also use the timer event to rewind files that are done playing so the user can replay files in case of such files being sound effects.

Adding video

Then after I satisfy my inner geek playing around with songs and audio effects I turn around and ask once again, can I do more? Yes, I  can! I want to add video to my player and see if I can play 8 videos at once. Why? Because I can?!?

So I add a second form that has a GridLayout and 8 media players arranged on the grid. Write some quick code to resize the grid as the window gets resized do I can get “optimal”  viewing pleasure. I also make sure that the window can not be closed by the user. This form is also a descendent of my base form.

I also add a onCreate event on my main form and proceed to connect the media player components to the appropriate media player controls. Once again I use my GetObjectByName to accomplish that.

//------------------------------------------------------------------------------------------------------------
procedure TForm1.FormCreate(Sender: TObject);
var
  MediaPlayer,
  MediaPlayerControl: TComponent;
  I: Integer;
begin
  // Creates screens form
  fScreenWall := TfScreenWall.Create(nil);
  // Assigns all playes to all media controls
  for I := 1 to 8 do
  begin
    MediaPlayer := GetObjectByName(MEDIA_PLAYER, I);
    if Assigned(MediaPlayer) and (MediaPlayer is TMediaPlayer) then
    begin
      MediaPlayerControl := fScreenWall.GetObjectByName(MEDIA_PLAYER_CONTROL, I);
      if Assigned(MediaPlayerControl) and (MediaPlayerControl is TMediaPlayerControl) then
      begin
        (MediaPlayerControl as TMediaPlayerControl).MediaPlayer := (MediaPlayer as TMediaPlayer);
      end;
    end;
  end;

  // Shows the form
  fScreenWall.Show;
end;

I then run it and to my surprise it just works! I can play 8 720p videos on my VM with no perceptible slowdown and also runs on my Mac.

dplayer running

Application running 8 videos at once on my Windows VM

Closing thoughts

I want to make sure that everybody understands that this is not meant to be a fully working program and that there is lots of improvements that can be done. Even as I write these lines I am doing my best to not try to further improve the program because it serves it’s purpose as is – to demonstrate that I can have several media players active and playing at once on my projects while using live bindings for some of the information to flow from controls to labels and so on. As a side benefit, I learned how to create an output converter for my specific needs.

Source code

The source code is available at https://github.com/TheRealFletch/dplayer. Feel free to make contribuitions to the dplayer project and push your changes back to the repository.


Viewing all articles
Browse latest Browse all 1725

Trending Articles