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.
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.
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?
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.
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.