How to use the API in a Windows service |
|
Microsoft Windows services enable you to create long-running executable applications that run in their own Windows sessions. These services can be automatically started when the computer boots, can be paused and restarted, and do not show any user interface. For security reasons, Windows Vista introduced a new concept called “Session 0 Isolation” which doesn't allow these services to create other windows nor communicate with existing ones through messages; for this reason Windows services can only communicate with other applications through a client/server mechanism such as remote procedure call (RPC) or Windows Communication Foundation (WCF) .
The limitation described above means that the API cannot be used to create the following user interface objects on an eventual client application that would use the Windows service as an audio server:
• | Visual feedbacks like VU-Meter, Spectrum, Waveform and Oscilloscope: (although VU-Meter and Spectrum visualizations could be still implemented on the client side by managing the CallbackForPlayersEvents delegate and by invoking the Spectrum.GetTable method and with partial support for analyzing the sound's waveform in order to create bitmaps of the waveform in various graphic formats) |
• | Waveform scroller |
• | Graphic bars manager |
• | Enhanced spectrum |
• | Curve designer |
• | Fader curves display |
• | Video player and mixer |
• | User interface of VST effects and instruments |
• | MIDI virtual keyboards |
If you should need using the mentioned objects, you would need to instance them directly into the client application.
The tutorial How to add the API to your projects instructs about steps needed for adding a reference to the API to a project: most of the steps described can be applied also when using the API for developing a Windows service. Once the reference to the API has been added, you can proceed normally as described inside the tutorial How to use the API in your projects.
The architecture of the multimedia engine (AdjMmsEng.dll and AdjMmsEng64.dll) of our audio components is internally based upon windows messages so, when instanced by one of our audio components from a Windows service, needs to be instructed to work differently: this can be achieved by invoking the ContainerIsWindowsService method; it's recommended invoking this method immediately before invoking the InitSoundSystem method ONLY when the container application is a Windows service.
When an application needs to connect to the Windows service containing the API for starting audio related operations and for receiving feedback about results and operations advancement, there is the need to create a bidirectional communication channel. The API's setup package comes with a C# sample (named "AudioDjWindowsService" available inside the folder "WindowsService") implementing client/server communication through the usage of Windows Communication Foundation (or WCF which can be implemented quite easily when developing in C# and Visual Studio) between a very basic Windows service and a client application having its own user interface; the communication is implemented through "named pipes" which are well wrapped within the WCF framework. Depending upon your background, you could obviously use a totally different way to let your application communicate with the Windows service.
Let's see a step by step procedure that could be followed when the Windows service needs to operate as a server for a client application:
Creating the project of the Windows service in Visual Studio
Defining and implementing a duplex contract inside the Windows service
Allowing the API to be invoked from the client
Creating the WCF endpoint (server side)
Defining and implementing the same duplex contract inside the client application
Running the Windows service and launching the client application
Creating the project of the Windows service in Visual Studio
Visual Studio comes with a template for creating projects that implement Windows services; the MSDN documentation can be used as a starting reference for this purpose. Once the project is created, we can start adding needed references and namespaces for accessing functionalities that help implementing WCF communication.
The first operation is to add a reference to the System.ServiceModel assembly, then we can add its namespace to the code of the Windows service:
Visual C# |
using System.ServiceModel;
|
Defining and implementing a duplex contract inside the Windows service
A duplex contract allows clients and servers to communicate with each other independently so that either can initiate calls to the other and consists of two one-way contracts between the client and the server and does not require that the method calls be correlated. Use this kind of contract when your service must query the client for more information or explicitly raise events on the client.
In our case the server side is implemented by the Windows service (hereinafter referred to as "server") and the client side is implemented by the application having its own user interface (hereinafter referred to as "client").
The first thing to do inside the code of the server is defining the interface that makes up the server side of the duplex contract.
Visual C# |
// WCF contract for sending commands from the client application to the Windows service [ServiceContract(CallbackContract = typeof(IAudioDjStudioServiceCallback), SessionMode = SessionMode.Allowed)] public interface IAudioDjStudioService { [OperationContract] enumErrorCodes PipeLoadSound(short nPlayer, string strPathname); [OperationContract] enumErrorCodes PipePlaySound(short nPlayer); [OperationContract] enumErrorCodes PipePauseSound(short nPlayer); [OperationContract] enumErrorCodes PipeStopSound(short nPlayer); [OperationContract] void PipeGetPlayerStatus (short nPlayer, ref enumPlayerStatus nStatus); [OperationContract] string PipeGetPlayerSoundPosition (short nPlayer); [OperationContract] string PipeGetPlayerSoundDuration (short nPlayer); [OperationContract] enumErrorCodes PipeSetVolume(short nPlayer, int nValue); [OperationContract] void PipeClose (); }
|
methods defined inside this contract, and implemented inside the server's code, will be invoked by the client application; as you can see, the interface of this contract, that we have named IAudioDjStudioService, is linked to another interface, named IAudioDjStudioServiceCallback, that makes up the client side of the duplex contract:
Visual C# |
// WCF contract for sending events from the Windows service to the client application [ServiceContract(SessionMode = SessionMode.Allowed)] public interface IAudioDjStudioServiceCallback { [OperationContract(IsOneWay=true)] void PipeSendPlayerStatus (short nPlayer, enumPlayerStatus nStatus); [OperationContract(IsOneWay=true)] void PipeSendVuMeterValues (short nPlayer, short nLeft, short nRight); }
|
Through this callback interface the server can send information to the client about the current status of the API, about advancement of lengthy operations and so on. As you may easily understand, methods of this interface will be implemented inside the code of the client.
Allowing the API to be invoked from the client
Now that the contract has been defined, we need to allow the API to be linked to the same. For this purpose we can create a class, arbitrarily named AudioDjStudioApiInstance, derived from our API AudioDjStudioApi.AudioDjStudioApiObj, that implements methods defined inside the IAudioDjStudioService interface of the contract:
Visual C# |
// class that implements the API and contract interfaces for WCF communication with the client application [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)] public class AudioDjStudioApiInstance : AudioDjStudioApi.AudioDjStudioApiObj, IAudioDjStudioService { .... }
|
Initialization of the API and connection to the client through the IAudioDjStudioServiceCallback callback interface can be implemented inside the constructor of the API:
Visual C# |
// class that implements the API and contract interfaces for WCF communication with the client application [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)] public class AudioDjStudioApiInstance : AudioDjStudioApi.AudioDjStudioApiObj, IAudioDjStudioService { // declare the WCF pipe channel callback IAudioDjStudioServiceCallback callback = null;
public AudioDjStudioApiInstance () { // initialize the API with 2 players this.ContainerIsWindowsService (); this.InitSoundSystem (2, 0, 0, 0, 0);
...
// set the WCF pipe channel callback callback = OperationContext.Current.GetCallbackChannel<IAudioDjStudioServiceCallback>(); } }
|
Once defined, we can implement methods of the primary IAudioDjStudioService interface inside the server's code and add management of events generated by the API (in this case we will add management for the CallbackForPlayersEvents delegate):
Visual C# |
// class that implements the API and contract interfaces for WCF communication with the client application [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)] public class AudioDjStudioApiInstance : AudioDjStudioApi.AudioDjStudioApiObj, IAudioDjStudioService { // declare the WCF pipe channel callback IAudioDjStudioServiceCallback callback = null;
// declare management functions for callback delegates CallbackForPlayersEvents addrCallbackForPlayersEvents;
void PlayerCallback(enumPlayerEvents nEvent, short nPlayer, Int32 nData1, Int32 nData2, float fData3, IntPtr pBufferUnicode, Int32 nBufferLength) { // notify the client application through the instance of the WCF pipe channel callback switch (nEvent) { case enumPlayerEvents.EV_SOUND_LOADED: callback.PipeSendPlayerStatus (nPlayer, enumPlayerStatus.SOUND_STOPPED); break; case enumPlayerEvents.EV_SOUND_STOPPED: callback.PipeSendPlayerStatus (nPlayer, enumPlayerStatus.SOUND_STOPPED); break; case enumPlayerEvents.EV_SOUND_PLAYING: callback.PipeSendPlayerStatus (nPlayer, enumPlayerStatus.SOUND_PLAYING); break; case enumPlayerEvents.EV_SOUND_PAUSED: callback.PipeSendPlayerStatus (nPlayer, enumPlayerStatus.SOUND_PAUSED); break; case enumPlayerEvents.EV_SOUND_CLOSED: callback.PipeSendPlayerStatus (nPlayer, enumPlayerStatus.SOUND_NONE); break; case enumPlayerEvents.EV_VUMETER: callback.PipeSendVuMeterValues (nPlayer, (short) nData1, (short) nData2); break; default: return; } }
{
// class constructor public AudioDjStudioApiInstance () { // initialize the API with 2 players this.ContainerIsWindowsService (); this.InitSoundSystem (2, 0, 0, 0, 0);
// create the empty VU meter so we will receive level notifications this.DisplayVUMeter.Create(0, IntPtr.Zero); this.DisplayVUMeter.Create(1, IntPtr.Zero);
// set the player's callback addrCallbackForPlayersEvents = new CallbackForPlayersEvents(PlayerCallback); this.CallbackForPlayersEventsSet(addrCallbackForPlayersEvents);
... do other stuffs
// set the WCF pipe channel callback callback = OperationContext.Current.GetCallbackChannel<IAudioDjStudioServiceCallback>(); }
public enumErrorCodes PipeLoadSound(short nPlayer, string strPathname) { // invoke the LoadSound method of the API return this.LoadSound (nPlayer, strPathname); } public enumErrorCodes PipePlaySound(short nPlayer) { // invoke the PlaySound method of the API return this.PlaySound (nPlayer); } public enumErrorCodes PipePauseSound(short nPlayer) { return this.PauseSound (nPlayer); } public enumErrorCodes PipeStopSound(short nPlayer) { return this.StopSound (nPlayer); } public void PipeGetPlayerStatus (short nPlayer, ref enumPlayerStatus nStatus) { nStatus = this.GetPlayerStatus (nPlayer); } public string PipeGetPlayerSoundPosition (short nPlayer) { Int32 nPositionInMs = this.GetCurrentPos (nPlayer); string strPos = this.FromMsToFormattedTime (nPositionInMs, true, true, ":", ".", 3); return strPos; } public string PipeGetPlayerSoundDuration (short nPlayer) { Int32 nDurationInMs = this.GetSoundOriginalDuration (nPlayer); string strDuration = this.FromMsToFormattedTime (nDurationInMs, true, true, ":", ".", 3); return strDuration; } public enumErrorCodes PipeSetVolume(short nPlayer, int nValue) { return this.StreamVolumeLevelSet (nPlayer, (float) nValue, enumVolumeScales.SCALE_LINEAR); } public void PipeClose () { // dispose the API and deallocate its internal resources this.Dispose(); } }
|
In production code it would be obviously recommended implementing some error checking that would detect eventual error codes returned by API's methods.
Creating the WCF endpoint (server side)
At this point the server can create the WCF endpoint that allows eventual clients to connect and request services; a good place to create and open the endpoint is the OnStart override function of the Windows service while the OnStop override function is a good place to close it:
Visual C# |
ServiceHost host = null;
protected override void OnStart (string[] args) { eventLog1.WriteEntry("In OnStart");
...
// create and open the WCF host and the endpoint for communication with client applications host = new ServiceHost(typeof(AudioDjStudioApiInstance), new Uri("net.pipe://localhost")); host.AddServiceEndpoint(typeof(IAudioDjStudioService), new NetNamedPipeBinding(), "PipeAudioDjStudio"); host.Open(); }
protected override void OnStop () { eventLog1.WriteEntry("In OnStop");
...
// close WCF host host.Close (); }
|
The snippet above, which implements WCF communication through "named pipes", defines an arbitrary name PipeAudioDjStudio for the endpoint: you would be obviously free to define your preferred name.
Defining and implementing the same duplex contract inside the client application
After having created the client application, that in our case will be a simple Winform application with a very basic user interface, we can add code needed to implement the duplex communication with the server. Although not elegant from a coding style perspective, the easier way to define the same duplex contract inside the client application is to simply copy & paste the definition of the IAudioDjStudioService and IAudioDjStudioServiceCallback interfaces inside the namespace of the client application. A more elegant solution, less prone to eventual errors, would be creating an assembly containing the definition of both interfaces and adding a reference to the new assembly in both client and server projects.
Also in this case we need to add a reference to the System.ServiceModel assembly, then we can add its namespace to the code of our client's form.
Once copied the definition of both interfaces inside the client's code, we need to make the class of our main application's form compatible with our duplex contract; for this reason we will have to change the declaration of the form's class from the following code:
Visual C# |
public partial class Form1 : Form { public Form1 () { InitializeComponent (); }
.... }
|
to the following code:
Visual C# |
// class that inherits contract interfaces for WCF communication with the client application [CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, UseSynchronizationContext = false)] public partial class Form1 : Form, IAudioDjStudioServiceCallback { public Form1 () { InitializeComponent (); }
.... }
|
Once defined, we can implement methods of the callback IAudioDjStudioServiceCallback interface and connect to the endpoint inside the client's code:
Visual C# |
// class that inherits contract interfaces for WCF communication with the client application [CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, UseSynchronizationContext = false)] public partial class Form1 : Form, IAudioDjStudioServiceCallback { public Form1 () { InitializeComponent (); }
// the pipe channel proxy for WCF communication IAudioDjStudioService pipeProxy = null;
bool m_bIsServiceAvailable = false;
private const int Player_1 = 0; private const int Player_2 = 1;
string m_strStatusPlayer1 = ""; string m_strStatusPlayer2 = ""; short m_nVuMeterValueLeft1 = 0; short m_nVuMeterValueLeft2 = 0; short m_nVuMeterValueRight1 = 0; short m_nVuMeterValueRight2 = 0;
public void PipeSendPlayerStatus (short nPlayer, enumPlayerStatus nStatus) { string strStatus = "Status: "; switch (nStatus) { case enumPlayerStatus.SOUND_NONE: strStatus += "No sound loaded"; break; case enumPlayerStatus.SOUND_PLAYING: strStatus += "Sound playing"; break; case enumPlayerStatus.SOUND_STOPPED: strStatus += "Sound stopped"; break; case enumPlayerStatus.SOUND_PAUSED: strStatus += "Sound paused"; break; } if (nPlayer == Player_1) m_strStatusPlayer1 = strStatus; else m_strStatusPlayer2 = strStatus; }
public void PipeSendVuMeterValues (short nPlayer, short nLeft, short nRight) { if (nPlayer == Player_1) { m_nVuMeterValueLeft1 = nLeft; m_nVuMeterValueRight1 = nRight; } else { m_nVuMeterValueLeft2 = nLeft; m_nVuMeterValueRight2 = nRight; } }
private void Form1_Load (object sender, EventArgs e) { // create the WCF service and related pipe channel var factory = new DuplexChannelFactory<IAudioDjStudioService>(new InstanceContext(this), new NetNamedPipeBinding(), new EndpointAddress("net.pipe://localhost/PipeAudioDjStudio")); pipeProxy = factory.CreateChannel();
// invoke one of the contract's methods in order to see if the WCF connection with the server was successful short nDevices = 0; try { enumPlayerStatus statusPlayer1 = enumPlayerStatus.SOUND_NONE; pipeProxy.PipeGetPlayerStatus (Player_1, ref statusPlayer1); } catch (Exception) { MessageBox.Show ("Audio DJ Studio API Windows Service has not been started."); Close (); return; }
m_bIsServiceAvailable = true; }
private void Form1_FormClosing (object sender, FormClosingEventArgs e) { // close the WCF pipe channel if (m_bIsServiceAvailable) pipeProxy.PipeClose (); }
.... }
|
Also the snippet above implements WCF communication through "named pipes", as seen for the server side code, and tries to establish a connection to the same endpoint named PipeAudioDjStudio.
You may notice that implementation of methods of the IAudioDjStudioServiceCallback callback interface store values of parameters into variables instead of using them to modify directly values of controls instanced on the user interface: this requirement is due to the fact that methods of the callback interface will be invoked from secondary threads a situation which doesn't allow accessing controls instanced on the user interface; as seen inside the sample of client application (named AudioDjWindowsServiceClient) installed by our setup package, a simple way to manage the situation would be instancing on the form a timer that would use these variables to update values of the controls on a timely basis.
Running the Windows service and launching the client application
Once both the Windows service and the client application have been compiled, you can install the Windows service on the target machine: for this purpose there are several solutions described inside the MSDN documentation. As mentioned before, the API's setup package comes with a C# sample (named "AudioDjWindowsService.exe") that is automatically installed inside the system during the installation phase: this service is not started automatically so, in order to test it in conjunction with the client application (named "AudioDjWindowsServiceClient.exe") you need to start it manually using the "Services" applet that can be launched through the "Start" Windows menu by selecting "All programs/Administrative tools/Services". Once launched, you will see something similar:
Click the evidenced item "Audio DJ Studio API Windows service" with the mouse's right button and select "Start" from the context menu: once the "Status" of the service is reported as "Started" you may launch the client application named "AudioDjWindowsServiceClient.exe" (more than one instance is allowed).
After closing the client application you may stop the Windows service by clicking again the evidenced item "Audio DJ Studio API Windows service" with the mouse's right button and by selecting "Stop" from the context menu.