Creating a Windows Media Player 10 Subscription Online Store
Jim Travis
Microsoft Corporation
December 2004
Applies to:
Microsoft® Windows Media® Player 10 SDK
Microsoft Windows Media Format 9.5 SDK
Microsoft Windows Media Rights Manager 10 SDK
Microsoft Windows Media Device Manager 10 SDK
Summary: Windows Media Player 10 offers support for integrated online stores. This article walks you through the technical aspects of using various Windows Media SDKs to implement a Windows Media Player 10 online store that offers digital media content on a subscription basis.
Download this article.
Contents
Introduction
The Scenario Business Model
The Online Store Architecture
Online Stores in Windows Media Player 10
Connecting the Plug-in to the User Interface
The Windows Media Player 10 Plug-in
Working with Licenses
Understanding Licenses
Issuing Licenses
Pre-delivering Licenses with Content
Updating Licenses in the Background
Preparing Licenses for Synchronization
Metering Content Usage
Creating the Metering Aggregation Service Page
Using the Plug-in for Metering
For More Information
Introduction
Consumer demand for digital audio and video is growing. Today, personal computers can faithfully reproduce sounds and images, providing a level of quality that exceeds analog technologies of the past. A wide variety of portable devices now enable users to enjoy digital music and video while on the go. These devices can store thousands of songs, adding up to many hours of music, TV shows, or entire feature films, while keeping the hardware size and weight small.
The Internet provides a natural mechanism for delivering digital media content. Broadband Internet connections are now more readily available to consumers, and audio and video compression technologies help to reduce file size and, therefore, transfer time. However, building a business that offers digital media content on the Internet presents challenges. Piracy is a real problem as users can create many copies of a digital media file very quickly, with no loss of quality, and easily distribute the pirated content to many people.
Windows Media digital rights management (DRM) provides a solution that helps to protect and securely deliver digital media content for playback on a computer, portable device, or network device. A license associated with the digital media content defines how content is protected, and licensed content will only work with certified software and devices. Windows Media DRM 10 provides great flexibility in how you can issue licenses and lets you tailor rights for playback and copying of digital media content for a variety of business scenarios.
Windows Media Player 10 supports Windows Media DRM 10 and also provides Internet content providers (ICPs) with an architecture for integrating digital media online stores with Windows Media Player. Users can view rich, extended information about digital media content, purchase it, play it, transfer it to a portable device, and burn it to a CD, all within Windows Media Player 10.
One way that online store providers can offer content is simply by selling an open license. In this scenario, users pay a one time fee to enjoy unlimited playback of the material (though one might still want to limit how many times the user can copy a file). This is the online store equivalent of a retail music store.
Another way to offer content is by subscription. In the subscription model, users pay a periodic fee, typically every month, to enjoy limited use of content from an online catalog. If the user chooses not to continue his or her subscription, the licenses for any downloaded content simply expire, disabling playback.
This article provides a technical walkthrough for implementing an example online store built on the subscription model. It discusses issues surrounding license delivery, keeping licenses current for subscribers, and tracking content usage (metering). You will learn about some of these topics in the context of creating a Windows Media Player 10 online store Component Object Model (COM) object, or plug-in, which enables you to integrate DRM code with Windows Media Player processes.
In addition to Windows Media Player 10, the following technologies are used to create the subscription service:
- Windows Media Player 10 Software Development Kit (SDK)
- Windows Media Rights Manager 10 SDK
- Windows Media Format 9.5 SDK
- Windows Media Device Manager 10 SDK
- C/C++
- COM
- Active Template Library (ATL)
- Hypertext Markup Language (HTML)
- Microsoft Visual Basic® Scripting Edition (VBScript)
- Microsoft JScript®
- Microsoft Internet Information Services (IIS)
- Active Server Pages (ASP).
Note that server-side DRM code requires the Microsoft Windows Server™ 2003 operating system, and client-side code requires Microsoft Windows® XP or Microsoft Windows Server 2003 Service Pack 1 (SP1).
This topics covered in this article apply to Windows Media DRM 10 and the devices that support it. This article does not discuss issues surrounding legacy devices or earlier versions of Windows Media Rights Manager.
To host an online store in Windows Media Player 10 your business must meet certain standards and then sign an agreement with Microsoft. See the Microsoft Web site (http://www.microsoft.com/windows/windowsmedia/default.aspx) for details.
A Note About Security
When creating a subscription online store, security should be a top priority. While Windows Media DRM helps protect your digital media content, you still have the responsibility to make sure the code you write is secure.
This article does not cover security issues. It is intended to show you how the various Windows Media components work together. The examples provided do nothing additional to secure the content or the online store. For example, the example code does not authenticate users or take steps to secure information that is transmitted using the Internet.
To learn more about security, see the Microsoft Security Developer Center on the MSDN Web site (http://msdn.microsoft.com/security/).
The Scenario Business Model
Within the subscription model there are a wide variety of business decisions to make. It is beyond the scope of this article to explore all possible combinations of these decisions. For an overview of using DRM with various business models, see the topic "Implementing Different Business Models" in the Windows Media Rights Manager 10 SDK documentation.
The remainder of this article assumes the following business model:
- Users pay a monthly fee to access the online store.
- When a user downloads content, the online store provides the required licenses at the same time.
- Users can play content as many times as they like until an expiration date is reached.
- Users can copy content to portable devices five times. If a user attempts to copy to a sixth device, the license is granted dynamically as a courtesy.
- Users can play content in a shared fashion during a peer-to-peer session.
- Users do not have permission to burn content to a CD.
- Users cannot backup and restore licenses.
- The online store updates licenses in the background whenever possible. When users pay their fees for the upcoming month, the transition between months should appear seamless. Making the subscriber deal with a license acquisition dialog box because a license expired is not desirable.
- Licenses are always issued to the active subscriber when requested. In practice, you might want to set a different policy. To learn about scenarios where you might need to reissue a license, see the topic "Determining Your Policies on Reissuing Licenses" in the Windows Media Rights Manager 10 SDK documentation.
- To enhance security, digital media files can only be played by individualized players. See the topic "Requiring Individualized Players" in the Windows Media Rights Manager 10 SDK documentation
- The online store periodically retrieves play count and copy count data and uses a metering aggregation service to keep track of the total values.
The Online Store Architecture
Achieving the goals of the online store requires using a combination of Windows Media SDKs. The SDKs work together in many cases. For example, the Windows Media Device Manager 10 SDK might retrieve some data from a device. This data is then processed by the Windows Media Rights Manager 10 SDK. The following SDKs are used by the online store:
- Windows Media Player 10 SDK. Provides the mechanisms for hosting the online store user interface inside the Windows Media Player 10 user interface. Windows Media Player 10 supports plug-ins for online stores. A plug-in is a COM object that exposes methods that Windows Media Player 10 calls to provide enhanced DRM support. For example, Windows Media Player 10 calls IWMPSubscriptionService2::prepareForSync each time a digital media file is about to be transferred to a portable device. This enables the online store to work with the DRM license before the transfer happens.
- Windows Media Rights Manager 10 SDK. Provides the functionality for protecting content, issuing licenses, and creating the metering aggregation service.
- Windows Media Device Manager 10 SDK. Provides the object and interface needed to request metering data from a portable device or a computer and to reset the metering data store on the device or computer.
- Windows Media Format 9.5 SDK. Provides the client-side support needed to query the license store for various state data. For example, you can use the Windows Media Format SDK to determine whether a particular digital media file has a valid license for playback.
The following architectural diagram shows how the pieces fit together.
.gif)
On the client computer, all of the online store activities take place in Windows Media Player 10. The online store user interface is displayed in special task panes in the Player. DRM-related tasks, such as checking files on the user's computer for expired licenses, are performed by the online store plug-in. Some of these tasks require the plug-in to send requests to one or more Internet-based servers running Windows Server 2003.
On the server computers, IIS serves Web pages that provide the online store user interface. ASP pages create Windows Media Rights Manager 10 objects to handle license requests and perform metering aggregation tasks.
The online store also needs to use database technology, such as Microsoft SQL Server, to maintain information about user accounts, digital media content, and metering statistics. (Providing detailed information about using database technology is beyond the scope of this article. Whenever use of such technology is needed in an example, the example code will call a placeholder function and the accompanying text will describe the intent of the function.)
Online Stores in Windows Media Player 10
From a technical standpoint, hosting an online store in Windows Media Player 10 is straightforward. When Windows Media Player 10 opens, the program retrieves a list of online stores from a server maintained by Microsoft. Each online store in the list provides a URL that points to a special XML document called ServiceInfo. The ServiceInfo document describes the online store. It contains URLs that point to Web pages, which Windows Media Player 10 displays in a browser control in certain parts of the Player user interface.
Within the ServiceInfo document, the ServiceTask1 element describes the first, or leftmost, task pane of up to three task panes that the online store can provide. The following example code shows what a ServiceTask1 element might look like in the ServiceInfo document for a fictional Proseware music service:
<ServiceTask1
URL = "http://www.proseware.com/service/html/Music.asp">
<ButtonText>Proseware\nMusic</ButtonText>
<ButtonTip>Proseware Music Store</ButtonTip>
</ServiceTask1>
Notice that the attribute named URL points to a Web page that displays in the task pane. The child elements, ButtonText and ButtonTip, specify the text that appears on the tab above the task pane and the ToolTip text associated with the tab.
The ServiceInfo document contains other information as well, such as description text and URLs for images that Windows Media Player 10 displays to identify the online store. For complete information about hosting an online store in Windows Media Player 10, see the Windows Media Player 10 Online Stores section of the Windows Media Player 10 SDK.
The Windows Media Player 10 Plug-in
Creating a Windows Media Player 10 plug-in for your online store is optional, but doing so offers some advantages. First, the plug-in must be installed on the user's computer, which means you can run native C++ code. This lets you take advantage C++-only features, such as using the Windows Media Device Manager 10 SDK to work with portable devices.
Second, the plug-in is tightly integrated with Windows Media Player 10. This means that the Player instantiates the plug-in at appropriate times, such as when the user tries to play content you provided. The Player also calls specific methods implemented by the plug-in, mainly to enable you to perform DRM-related tasks. In later sections of this article, you will see example code that illustrates using certain methods of the IWMPSubscriptionService and IWMPSubscriptionService2 interfaces within a plug-in.
- startBackgroundProcessing and stopBackgroundProcessing. Windows Media Player 10 calls these methods to enable you to perform background processing tasks you need for your online store. The Player uses these calls to start and stop your background tasks in a way that lets your code run when the Player is not performing its own background processing. The example code in this article uses these calls to manage a thread that updates licenses in the background.
- prepareForSync. Windows Media Player 10 calls this method to enable you to work with a digital media file before it is transferred to a portable device. The example code in this article uses this call to inspect the license associated with the content and make changes when appropriate.
- deviceAvailable. Windows Media Player 10 calls this method after portable device synchronization completes if the time elapsed since the last call is one week or more. The example code in this article uses this call to retrieve metering data from the device.
There are additional methods that you can choose to implement. You can also specify which methods Windows Media Player 10 calls by setting a registry key value. For more information about the methods of IWMPSubscriptionService, IWMPSubscriptionService2, and online store registration flags, see the Windows Media Player 10 SDK.
The Windows Media Player 10 SDK setup package installs a project wizard for Visual Studio .NET 2003 that generates an online store plug-in project for you. To use the wizard, you must follow the instructions in the Building the COM Component section of the Windows Media Player 10 SDK documentation. Creating your plug-in this way gives you a good starting point for your own plug-in. The code generated by the wizard includes implementations of the IWMPSubscriptionService and IWMPSubscriptionService2 interfaces. It also provides the infrastructure necessary for creating and registering a COM component using ATL.
The project also does the work to make sure the plug-in registers itself as the Windows Media Player 10 online store plug-in for your content distributor ID. The content distributor ID is the string you use to identify your online store and to tag your content in the WM/ContentDistributor field of the Windows Media file header (or the DRM header).
Connecting the Plug-in to the User Interface
Though most of the tasks for which you would want to use a plug-in happen in the background, you might decide that you want to connect your plug-in to your online store user interface. For example, suppose your business model specifies that users are authorized to play your content on only one computer at a time. You might want to provide a button that users can click to de-authorize a particular computer before attempting to acquire licenses on another. Doing so would require using the Windows Media Format 9.5 SDK to revoke all licenses on the first computer. Your plug-in could perform this work. For more information about revoking licenses, see Implementing License Revocation with Windows Media DRM 10 on the Microsoft Web site (http://www.microsoft.com/windows/windowsmedia/knowledgecenter/technicalarticles.aspx#digitalrightsmanagement).
The Web pages that comprise your online store user interface can instantiate your plug-in. One common way to do this is by using the HTML OBJECT element. The following HTML example code shows the OBJECT element in use. Note that the class ID shown is filled with the letter "x". In practice, you would provide the actual GUID for your object's class ID.
<OBJECT id = "plugin" height = 0 width = 0
classid = "CLSID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
</OBJECT>
However, for you plug-in to communicate with script code in the Web page, it must expose an interface that derives from IDispatch. The online store plug-in project wizard does not provide such an implementation for you. While a detailed discussion about COM automation is beyond the scope of this article, you can use the following steps as a guide.
- Add an Interface Definition Language (IDL) file to your plug-in project. The IDL should define a custom interface for your plug-in and a type library. The following example code shows a simple IDL that defines a custom dual interface derived from IDispatch. The interface contains a single method, named revokeAll. Note that GUIDs for the interface identifier (IID), type library identifier (LIBID), and class identifier (CLSID) are filled with the letter "x". In practice, these would contain unique GUID values that you generated.
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx),
dual,
helpstring("IProsewarePlugin interface"),
pointer_default(unique)
]
interface IProsewarePlugin : IDispatch
{
[id(1), helpstring("Revokes all licenses")] HRESULT revokeAll();
};
[
uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx),
version(1.0),
helpstring("ProsewarePlugin 1.0 Type Library")
]
library ProsewareLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx),
helpstring("ProsewarePlugin Class")
]
coclass ProsewarePluginClass
{
[default] interface IProsewarePlugin;
};
};
- Derive your plug-in class from IDispatchImpl. This ATL class provides a default implementation for the IDispatch portion of your custom interface. The following example code shows how you might do this.
class ATL_NO_VTABLE CProsewarePlugin :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CProsewarePlugin, &CLSID_ProsewarePlugin>,
public IWMPSubscriptionService2,
public IDispatchImpl<IProsewarePlugin, &IID_IProswarePlugin,
&LIBID_ProsewareLib>
{
// Wizard created class implementation goes here.
}
Note that the CLSID, IID, and LIBID must be defined in your plug-in project, either explicitly or by including the correct MIDL compiler output file.
- Update the COM map. The following example code shows how you might do this.
BEGIN_COM_MAP(CProsewarePlugin)
COM_INTERFACE_ENTRY(IWMPSubscriptionService)
COM_INTERFACE_ENTRY(IWMPSubscriptionService2)
COM_INTERFACE_ENTRY(IProsewarePlugin)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
- Implement the methods of your custom interface.
- Update your plug-in project's .rgs file to register your type library.
Once your plug-in exposes your custom interface in this manner, you can use script code in your Web page to call your custom method. The following JScript example code demonstrates this.
// Call the revokeAll() method of the
// object named "plugin"
plugin.revokeAll();
Important Be careful when exposing custom methods from your plug-in.
Your COM object can be instantiated by anyone who creates a Web site. This means that malicious Web sites can call the methods your plug-in exposes. You must plan for this possibility to protect the security of your online store. For example, the revokeAll method created in the previous example should not automatically revoke all licenses when called. At a minimum, it must prompt the user for permission to revoke the licenses.
Rather than instantiate a separate instance of your plug-in in your Web page, you might want to access the same instance that Windows Media Player 10 instantiates. To do this, you can make your plug-in object a COM singleton. You might remember that a singleton is a COM object that can be created exactly once in each process. The first client that uses CoCreateInstance to instantiate a singleton causes the singleton object's class factory to create the object. When subsequent clients in the same process cocreate the singleton object, they receive a pointer to the existing instance of the singleton—no additional copies of the object are instantiated.
To make your plug-in a COM singleton, you can add the following ATL macro code to your class.
DECLARE_CLASSFACTORY_SINGLETON(CProsewarePlugin)
It is also possible to create user interface elements in your plug-in. That way, when your plug-in is embedded in a Web page, the plug-in user interface becomes visible in the Web page. This directly exposes your plug-in to the user rather than connecting HTML elements to your plug-in by using script. To do this, you must understand ActiveX® controls and how they expose user interface elements.
Working with Licenses
One of the challenges of creating a subscription online store is managing licenses. Users can download many digital media files over time. Because licenses can expire, it is important to consider strategies for keeping licenses current so that users don't perceive a gap in service. If a user has paid his or her subscription fee for the upcoming month, it makes sense to renew licenses before the end of the current month, thereby avoiding the need to acquire a license when the user tries to play content. In the same way, you should try to avoid having licenses expire on the user's portable device.
Understanding Licenses
A new feature for the Windows Media Rights Manager 10 SDK is called a license chain. License chains let you divide licenses into two parts: root and leaf licenses. For the subscription model, this means the online store can quickly renew a subscription by reissuing a single root license. Each content item will be associated with an individual leaf license. Each leaf license contains an uplink ID that points to the root license using its key ID. It is the combination of the rights in the root license with the rights in the leaf license that determine whether the user can access the content.
Note that using a license chain to protect content requires that you specify the elements of the license chain by adding the correct uplink ID to the license. For details, see Specifying a License Chain When Protecting Content in the Windows Media Rights Manager 10 SDK documentation.
License pre-delivery is a method of delivering a license before protected content is accessed. It makes sense to pre-deliver licenses in the subscription model because you will always authenticate the user before permitting the user to download protected content. Pre-delivery makes the process of downloading and accessing content appear seamless because it eliminates any future delay that occurs when acquiring licenses, for example, during the first playback attempt. The example online store uses pre-delivery to issue the initial root and leaf licenses as well as to reissue licenses.
To pre-deliver a license in this example, you need to know the following information.
- Root key ID. The key ID value you use to generate the key for the root license. In this example, there is one root license, so there is a single root key ID which you will provide as a hard-coded value.
- Leaf key ID. The key ID value you use to generate the key for the leaf licenses. Each content item is associated with one leaf license, so each item will have its own key ID which you will retrieve dynamically.
- License key seed. The value you provide with the key ID to generate a key. You can use the same seed for all keys. You will provide this as a hard-coded value.
- Content ID. Each content item should have a unique identifier which is stored in the DRM header. You will use the content ID to retrieve the correct leaf key ID before issuing a leaf license for a particular digital media file. You will retrieve this value from the DRM header on the client and send it as a query string parameter to the ASP page that issues the license.
- Content server public key. This value is provided by the content packager.
- License revocation public key. This value is added to licenses to enable you to revoke them. You will provide this as a hard-coded value.
- User ID. This value is added to licenses to enable you to revoke them for a particular user.
- Individualization version. When you receive a license request, you will retrieve this value from the content header and then use it to set the WMRMLicGen.IndividualizationVersion property.
- Client version. When you receive a license request, you will retrieve this value from the WMRMChallenge object and then use it to determine whether the client version number for the Windows Media DRM subsystem is 10.
- Metering certificate. This string is used to authorize Windows Media DRM to store metering data for content you provide.
You can see these values in use in the next section.
Issuing Licenses
You issue licenses by using the Windows Media Rights Manager 10 SDK objects in an ASP page. The following example code creates an ASP page that uses VBScript to pre-deliver licenses for the example subscription online store. Note that supporting regular license acquisition would require additional code.
You should also note that this code makes no effort to authenticate the user. In practice, you should take steps to ensure that you only issue licenses to current subscribers.
<%
Response.Buffer = True
Response.Expires = 0
On Error Resume Next
' """""""""""""""""""""""""""""""""""""""""""""""""""""
' Declare variables.
' """""""""""""""""""""""""""""""""""""""""""""""""""""
Dim ChallengeObj ' WMRMChallenge object
Dim HeaderObj ' WMRMHeader object
Dim KeyObj ' WMRMKeys object
Dim RightsObj ' WMRMRights object
Dim RestrictObj ' WMRMRestrictions object
Dim LicenseObj ' WMRMLicGen object
Dim ResponseObj ' WMRMResponse object
Dim ContentDistributor ' Online store key name.
Dim ClientVersionInfo ' Version of the client
Dim ContentID ' Content ID
Dim ContentServerPubKey ' Public key of the content server
Dim Delivery ' Delivery flag
Dim IndiVersion ' Security version of the DRM component
Dim LeafKey ' Key for the leaf license
Dim LeafKID ' Key ID of the leaf license
Dim RootKey ' Key for the root license
Dim RootKID ' Key ID of the root license
Dim License ' License to deliver
Dim Rights ' Rights string for the license
Dim LicResponse ' License response
Dim LicRevPubKey ' Public key for license revocation
Dim PlayRestrictions ' Playback restrictions
Dim CopyRestrictions ' Copy restrictions
Dim Seed ' License key seed
Dim strLicenseRequested ' License request string
Dim varClientInfo ' Client information
Dim Licensetype ' Flag for root or leaf license
Dim UserID ' User ID of the client
Dim NeedsCopies ' License requested for additional copy count
Dim CopyCount ' Number of copies to grant
Dim MeterCert ' Metering certificate
Do
' """""""""""""""""""""""""""""""""""""""""""""""""""""
' Set variables.
' """""""""""""""""""""""""""""""""""""""""""""""""""""
' Variables passed by query string parameters.
ContentID = Request("cid")
UserID = Request("uid")
NeedsCopies = Request("NeedsCopies")
RootKID = "<Replace this with the key ID for the root.>"
ContentDistributor = "Proseware"
Delivery = ""
ContentServerPubKey = "<Replace this with the content server public key.>"
Seed = "<Replace this with the key seed.>"
LicRevPubKey = "<Replace this with the license revocation public key.>"
MeterCert = "<Replace this with the metering certificate.>"
' """""""""""""""""""""""""""""""""""""""""""""""""""""
' Determine the type of license requested.
' """""""""""""""""""""""""""""""""""""""""""""""""""""
If (ContentID = "root") Then
Licensetype = "root"
Else
Licensetype = "leaf"
' Retrieve the key ID for the leaf license.
' GetLeafKID() is a custom function not implemented
' in this example. In practice, this function would
' retrieve the key ID from a database.
LeafKID = GetLeafKID(ContentID)
' If the leaf is requested with the NeedsCopies flag set to true
' grant 1 additional right to copy.
' If the flag is set to false, then issue a new leaf with a
' copy count of 5.
If NeedsCopies = "true" Then
CopyCount = 1
Else
CopyCount = 5
End If
End If
' """""""""""""""""""""""""""""""""""""""""""""""""""""
' Create objects.
' """""""""""""""""""""""""""""""""""""""""""""""""""""
Set ChallengeObj = Server.CreateObject("WMRMObjs.WMRMChallenge")
Set HeaderObj = Server.CreateObject("WMRMObjs.WMRMHeader")
Set KeyObj = Server.CreateObject("WMRMObjs.WMRMKeys")
Set RightsObj = Server.CreateObject("WMRMObjs.WMRMRights")
Set RestrictObj = Server.CreateObject("WMRMObjs.WMRMRestrictions")
Set ResponseObj = Server.CreateObject("WMRMObjs.WMRMResponse")
Set LicenseObj = Server.CreateObject("WMRMObjs.WMRMLicGen")
' """""""""""""""""""""""""""""""""""""""""""""""""""""
' Get the license challenge.
' """""""""""""""""""""""""""""""""""""""""""""""""""""
strLicenseRequested = Request("challenge")
If (strLicenseRequested = "") Then Exit Do
ChallengeObj.Challenge = strLicenseRequested
If (Err.Number <> 0) Then Exit Do
varClientInfo = ChallengeObj.ClientInfo
If (Err.Number <> 0) Then Exit Do
HeaderObj.Header = ChallengeObj.Header
If (Err.Number <> 0) Then Exit Do
' Verify the header with the public key
lResult = HeaderObj.Verify(ContentServerPubKey)
If (lResult = 0) Then Exit Do
IndiVersion = HeaderObj.IndividualizedVersion
If (Err.Number <> 0) Then Err.clear
' GetCurrentIndiv() is a placeholder function for a custom
' routine that retrieves the minimum inidividualization
' version you want to support.
If IndiVersion < GetCurrentIndiv() Then Exit Do
' """""""""""""""""""""""""""""""""""""""""""""""""""""
' Check the version of the client player.
' """""""""""""""""""""""""""""""""""""""""""""""""""""
ClientVersionInfo = ChallengeObj.ClientVersion
If (Err.Number <> 0) Then Exit Do
ClientVersionArray = Split(ClientVersionInfo, ".")
If (ClientVersionArray(0) < 10) Then
' The user needs to upgrade the Player version.
Exit Do
End If
' """""""""""""""""""""""""""""""""""""""""""""""""""""
' Generate the keys. A root key is required for both
' leaf and root licenses.
' """""""""""""""""""""""""""""""""""""""""""""""""""""
KeyObj.Seed = Seed
If (Err.Number <> 0) Then Exit Do
If (Licensetype = "leaf") Then
KeyObj.KeyID = LeafKID
If (Err.Number <> 0) Then Exit Do
LeafKey = KeyObj.GenerateKey()
If (Err.Number <> 0) Then Exit Do
End If
KeyObj.KeyID = RootKID
If (Err.Number <> 0) Then Exit Do
RootKey = KeyObj.GenerateKeyEx(8)
If (Err.Number <> 0) Then Exit Do
' """""""""""""""""""""""""""""""""""""""""""""""""""""
' Set the rights for leaf or root licenses.
' """""""""""""""""""""""""""""""""""""""""""""""""""""
RightsObj.AllowPlay = True
RightsObj.AllowCopy = True
RightsObj.AllowCollaborativePlay = True
RightsObj.AllowBackupRestore = False
RightsObj.AllowPlaylistBurn = False
RightsObj.MinimumClientSDKSecurity = 1000
RightsObj.MinimumSecurityLevel = 1000
If (Licensetype = "leaf") Then ' Set rights for a leaf license
RightsObj.CopyCount = CopyCount
' Set copy restrictions.
Call RestrictObj.AddRestriction(6, 0)
CopyRestrictions = RestrictObj.GetRestrictions
RightsObj.CopyRestrictions = CopyRestrictions
If (Err.Number <> 0) Then Exit Do
Elseif (Licensetype = "root") Then ' Set rights for a root license
' GetExpirationDateUser() is a placeholder function that retrieves
' the next expiration date for the current subscriber's account.
RightsObj.ExpirationDate = GetExpirationDateUser(UserID)
RightsObj.DeleteOnClockRollback = False
RightsObj.DisableOnClockRollback = True
End If
Rights = RightsObj.GetAllRights()
If (Err.Number <> 0) Then Exit Do
'"""""""""""""""""""""""""""""""""""""""""""""""""""""
' Generate the license.
'"""""""""""""""""""""""""""""""""""""""""""""""""""""
If (Licensetype = "leaf") Then
LicenseObj.KeyID = LeafKID
If (Err.Number <> 0) Then Exit Do
Call LicenseObj.SetKey("", LeafKey)
If (Err.Number <> 0) Then Exit Do
Elseif (Licensetype = "root") Then
LicenseObj.KeyID = RootKID
If (Err.Number <> 0) Then Exit Do
Call LicenseObj.SetKey("", RootKey)
If (Err.Number <> 0) Then Exit Do
End If
' Only meter on the leaf license.
If(LicenseType = "leaf") Then
LicenseObj.MeteringCertificate = MeterCert
If (Err.Number <> 0) Then Exit Do
End If
LicenseObj.Attribute("ContentDistributor") = ContentDistributor
If (Err.Number <> 0) Then Exit Do
' UID and LGPUBKEY are needed only when you want to support
' license revocation.
LicenseObj.Attribute("UID") = UserID
If (Err.Number <> 0) Then Exit Do
LicenseObj.Attribute("LGPUBKEY") = LicRevPubKey
If (Err.Number <> 0) Then Exit Do
LicenseObj.Rights = Rights
If (Err.Number <> 0) Then Exit Do
LicenseObj.ClientInfo = varClientInfo
If (Err.Number <> 0) Then Exit Do
LicenseObj.IndividualizedVersion = IndiVersion
If (Err.Number <> 0) Then Exit Do
' GetNextPriorityUser is a placeholder function
' that retrieves the correct priority value.
LicenseObj.Priority = GetNextPriorityUser(UserID)
If (Err.Number <> 0) Then Exit Do
LicenseObj.BindToPubKey = ContentServerPubKey
If (Err.Number <> 0) Then Exit Do
If (Licensetype = "leaf") Then
' Link to the root license
' for license chaining.
LicenseObj.UplinkKid = RootKid
LicenseObj.UplinkKey = RootKey
If (Err.Number <> 0) Then Exit Do
End If
License = LicenseObj.GetLicenseToDeliver
If (Err.Number <> 0) Then Exit Do
Delivery = "deliver"
Loop While False
' This page handles pre-delivery only.
If (Delivery = "deliver") Then
Call ResponseObj.AddLicense("2.0.0.0", License)
LicResponse = ResponseObj.GetLicenseResponse()
Response.Write LicResponse
End if
' """""""""""""""""""""""""""""""""""""""""""""""""""""
' Clear the objects.
' """""""""""""""""""""""""""""""""""""""""""""""""""""
Set ChallengeObj = Nothing
Set HeaderObj = Nothing
Set KeyObj = Nothing
Set RightsObj = Nothing
Set RestrictObj = Nothing
Set LicenseObj = Nothing
Set ResponseObj = Nothing
%>
Note that this example code pre-delivers licenses that support metering. You will learn more about metering in a later section.
The placeholder function GetCurrentIndiv retrieves an individualization version value from a database. It is important to keep the individualization version that you use current. For the latest individualization version information, see the <A HREF="http://go.microsoft.com/fwlink/?linkid=1675" TARGET="_blank"Microsoft Web site (http://licenseserver.windowsmedia.com/).
The placeholder function GetExpirationDateUser retrieves a date value from a database. This value should correspond to the expiration date of the subscriber's account.
The placeholder function GetNextPriorityUser retrieves a priority value. This value should increment once per month for each user. Using increasing priority values ensures that the subscriber always has the most currently issued license on his or her portable device. See the documentation for WMRMLicGen.Priority in the Windows Media Rights Manager 10 SDK for more information.
Pre-delivering Licenses with Content
Now that you can issue licenses, you can write code to make license requests. You can use HTML and JScript code in your online store Web page to pre-deliver licenses for content that users download. To do this, your Web page must embed the RMGetLicense object. The following example Web page embeds this object.
<HTML>
<BODY>
Simple Web page that embeds the RMGetLicense object
<OBJECT name = RMGetLicense
id = RMGetLicense
classid = clsid:A9FC132B-096D-460B-B7D5-1DB0FAE0C062
height = 0 width = 0>
</OBJECT>
</BODY>
</HTML>
To pre-deliver a license when downloading content to the user's computer, you can use code like the following JScript example.
<SCRIPT Language = "JScript">
// Pre-deliver a root or leaf license.
// cid is the content ID for the digital media
// file being downloaded. A value of "root" will
// cause a root license to be issued.
// uid is a unique user ID.
function GetLicenseFromURL(cid, uid)
{
var PreDelURL = "http://www.proseware.com/Predeliver.asp?";
PreDelURL += "cid=";
PreDelURL += cid;
PreDelURL += "&uid=";
PreDelURL += uid;
try
{
RMGetLicense.GetLicenseFromURL("", PreDelURL);
}
catch
{
// GetLicenseFromURL doesn't return error codes.
// However, calling GetLicenseFromURL can cause
// an exception to be raised from ouside Windows Media
// Rights Manager. This empty catch block
// intentionallydiscards these
// exceptions so the call fails silently.
}
}
</SCRIPT>
You might notice that no value is appended to the query string for the NeedsCopies parameter. In this scenario, your code is pre-delivering the initial license with the content download, so omitting this value will cause leaf licenses to be issued with a full copy count of 5, which is the desired behavior.
Keep in mind that you will only request the root license if one has not yet been issued for the current month. This means you will need to keep track of when root licenses are issued by using a database.
Updating Licenses in the Background
You can run client-side code to test whether the root license is about to expire. As part of this background process, it is also a good idea to check for missing or damaged leaf licenses. This functionality should be included in your Windows Media Player 10 online store plug-in.
Note that the code described in this section assumes that the user has individualized his or her installation of Windows Media Player. If the Player has not yet been individualized, this code may silently fail to update licenses.
The code that updates licenses will perform the following tasks:
The following diagram shows the logical flow of the code that updates licenses in the background.
.gif)
Checking for a Valid License
Before you can request updated licenses, you will need to test whether licenses are missing or near expiration. To make this task easier, you can create a helper class that encapsulates the required functionality.
The following example declares a class named CLicenseExpirationChecker. Keep in mind when using the Windows Media Format SDK that you will need to reference the correct .lib files.
#include <stdio.h>
#include <windows.h>
#include <wmsdk.h> // Windows Media Format 9.5 SDK header
#include <atlbase.h>
#include <atlcom.h>
#define CNS_PER_DAY 864000000000 // 100ns units per day.
class CLicenseExpirationChecker
{
public:
HRESULT Initialize();
HRESULT CheckExpiration(BSTR bstrFileName,
DWORD dwExpirationWindow,
BSTR* pbstrUpdateContentID);
private:
HRESULT GetContentID(BSTR* pbstrContentID);
CComPtr<IWMMetadataEditor> m_spEditor;
CComPtr<IWMDRMEditor> m_spDRMEditor;
};
The Initialize method contains the code to create the metadata editor object and to QueryInterface for the IWMDRMEditor interface, as the following example code demonstrates.
HRESULT CLicenseExpirationChecker::Initialize()
{
HRESULT hr = S_OK;
// Create the metadata editor object.
hr = WMCreateEditor(&m_spEditor);
if(SUCCEEDED(hr))
{
// Get the DRM metadata editor interface.
hr = m_spEditor.QueryInterface(&m_spDRMEditor);
}
return hr;
}
The CheckExpiration method takes two input parameters. bstrFileName contains the path to the digital media file to inspect. dwExpirationWindow contains the number of days against which the method will check the expiration date. The output parameter, pbstrUpdateContentID, is used to return one of three values:
- NULL if the license is valid. This means no further work is required for the current content item.
- "root" if the license expiration date is within the expiration window.
- The content ID value from the DRM header if the leaf license is missing or not valid, or if both the leaf license and the root license are missing or not valid.
The following example code shows an implementation for the CheckExpiration method:
HRESULT CLicenseExpirationChecker::CheckExpiration(
BSTR bstrFileName,
DWORD dwExpirationWindow,
BSTR* pbstrUpdateContentID)
{
// Check for initialization.
if(!m_spDRMEditor)
{
return NS_E_INVALID_REQUEST;
}
// Parameter checking.
if(NULL == bstrFileName ||
0 == SysStringLen(bstrFileName) ||
NULL == pbstrUpdateContentID)
{
return E_INVALIDARG;
}
HRESULT hr = S_OK;
// Time variables to hold the current and expiration times.
FILETIME CurrentTime;
ZeroMemory(&CurrentTime, sizeof(FILETIME));
FILETIME* pExpireTime = NULL;
ULARGE_INTEGER uliCurrent,
uliExpire;
ZeroMemory(&uliCurrent, sizeof(ULARGE_INTEGER));
ZeroMemory(&uliExpire, sizeof(ULARGE_INTEGER));
ULONGLONG uliDiff = 0;
// Variables for retrieving the license state information.
WM_LICENSE_STATE_DATA* pWMLicState = NULL;
WMT_ATTR_DATATYPE DataType = WMT_TYPE_DWORD;
BYTE* pbData = NULL;
WORD cbData = 0;
BOOL bProtected = FALSE;
CComBSTR bstrRoot;
hr = bstrRoot.Append(L"root");
if(SUCCEEDED(hr))
{
// Ensure we are working with protected content.
hr = WMIsContentProtected((WCHAR*)bstrFileName, &bProtected);
}
if(SUCCEEDED(hr))
{
if(FALSE == bProtected)
{
hr = NS_E_INVALID_REQUEST;
}
}
if(SUCCEEDED(hr))
{
// Open the file for metadata editing.
hr = m_spEditor->Open((WCHAR*)(bstrFileName));
}
if(SUCCEEDED(hr))
{
// Get the current time from the system.
GetSystemTimeAsFileTime(&CurrentTime);
// Get the license state data.
// First get the size of the data.
hr = m_spDRMEditor->GetDRMProperty(
g_wszWMDRM_LicenseState_Playback,
&DataType,
NULL,
&cbData);
}
// License state data attributes should always be
// a binary data type.
if(SUCCEEDED(hr) && DataType == WMT_TYPE_BINARY)
{
// Allocate memory for the data.
pbData = new BYTE[cbData];
if(pbData == NULL)
{
hr = E_OUTOFMEMORY;
}
if(SUCCEEDED(hr))
{
// Get the actual data.
hr = m_spDRMEditor->GetDRMProperty(
g_wszWMDRM_LicenseState_Playback,
&DataType,
pbData,
&cbData);
}
if(SUCCEEDED(hr))
{
// Set the license state data pointer.
pWMLicState = (WM_LICENSE_STATE_DATA*)pbData;
// Depending on the category of restriction,
// there will be a varying number
// of dates in the license state data.
switch(pWMLicState->stateData->dwCategory)
{
case WM_DRM_LICENSE_STATE_UNTIL:
case WM_DRM_LICENSE_STATE_COUNT_UNTIL:
{
if(pWMLicState->stateData->dwNumDates != 1)
{
hr = E_UNEXPECTED;
break;
}
pExpireTime = &pWMLicState->stateData->datetime[0];
break;
}
case WM_DRM_LICENSE_STATE_FROM_UNTIL:
case WM_DRM_LICENSE_STATE_COUNT_FROM_UNTIL:
{
if(pWMLicState->stateData->dwNumDates != 2)
{
hr = E_UNEXPECTED;
break;
}
pExpireTime = &pWMLicState->stateData->datetime[1];
break;
}
// If the state category is NORIGHT, then either
// the leaf license or the root license needs
// to be acquired. In this case, set the string to
// the content ID.
case WM_DRM_LICENSE_STATE_NORIGHT:
{
hr = GetContentID(pbstrUpdateContentID);
break;
}
default:
{
hr = E_UNEXPECTED;
break;
}
}
if(pExpireTime)
{
// Copy the FILETIME values to their
// ULARGE_INTEGER counterparts.
memcpy((void*)&uliCurrent, (void*)&CurrentTime,
sizeof(uliCurrent));
memcpy((void*)&uliExpire, (void*)pExpireTime,
sizeof(uliCurrent));
// Compare the dates.
if(uliExpire.QuadPart > uliCurrent.QuadPart)
{
// Get the difference between the current time
// and the expiration time.
uliDiff = uliExpire.QuadPart - uliCurrent.QuadPart;
// If the difference is less than the number of days
// specified in the call to this function, the file
// needs an update.
if(uliDiff <=
(((ULONGLONG)CNS_PER_DAY) * dwExpirationWindow))
{
hr = bstrRoot.CopyTo(pbstrUpdateContentID);
}
}
else // Expiration date is in the past.
{
hr = bstrRoot.CopyTo(pbstrUpdateContentID);
}
}
}
}
// Clean up.
m_spEditor->Close();
if(pbData)
{
delete[] pbData;
pbData = NULL;
}
pWMLicState = NULL;
pExpireTime = NULL;
return hr;
}
The helper function named GetContentID retrieves the content ID attribute value from the file loaded into the editor. The output parameter, pbstrContentID, points to the address of a BSTR that contains the content ID string. Remember that this string will need to be freed later. (In this example, the string will be returned by using a CComBSTR that will free the string when it goes out of scope. You will see this code in a later section.)
The following example code shows an implementation for GetContentID:
HRESULT CLicenseExpirationChecker::GetContentID(BSTR* pbstrContentID)
{
// Check for initialization.
ATLASSERT(m_spDRMEditor);
ATLASSERT(pbstrContentID);
HRESULT hr = S_OK;
WMT_ATTR_DATATYPE DataType = WMT_TYPE_DWORD;
WORD cbContentID = 0;
WCHAR *pwszContentID = NULL;
// Get the size needed for the attribute.
hr = m_spDRMEditor->GetDRMProperty(g_wszWMDRM_DRMHeader_ContentID,
&DataType,
NULL,
&cbContentID);
// Verify that the attribute is a string.
if(SUCCEEDED(hr) &&
(DataType != WMT_TYPE_STRING || cbContentID % sizeof(WCHAR) != 0))
{
hr = E_UNEXPECTED;
}
if(SUCCEEDED(hr))
{
// Allocate memory for the string.
pwszContentID = new WCHAR[cbContentID / sizeof(WCHAR)];
if(pwszContentID == NULL)
{
hr = E_OUTOFMEMORY;
}
}
if(SUCCEEDED(hr))
{
// Get the attribute.
hr = m_spDRMEditor->GetDRMProperty(g_wszWMDRM_DRMHeader_ContentID,
&DataType,
(BYTE*)pwszContentID,
&cbContentID);
}
if(SUCCEEDED(hr))
{
// Allocate and return the BSTR.
*pbstrContentID = SysAllocString(pwszContentID);
if(NULL == pbstrContentID)
{
hr = E_OUTOFMEMORY;
}
}
if(pwszContentID)
{
delete[] pwszContentID;
pwszContentID = NULL;
}
return hr;
}
Managing the Background Thread
You can create the background thread any way you like. In this example, only one background thread for updating licenses will run at a time and the entire process will happen once per session. This means that you will only create the thread if no thread exists yet. If the thread does exist, you will signal an event to pause the thread when Windows Media Player 10 calls IWMPSubscriptionService2::stopBackgroundProcessing and signal it to resume when the Player calls IWMPSubscriptionService::startBackgroundProcessing. You will also use an event to force the thread to exit when the plug-in is destroyed.
The following example code declares variables to store the event handles. It also declares the variable to store the thread handle. You should declare these variables as members of your plug-in class and remember to initialize them to NULL in your class constructor.
public:
// Event handles
HANDLE m_hExitThreadsEvent;
HANDLE m_hPauseThreadsEvent;
private:
// Thread handle
HANDLE m_hLicenseUpdateThread;
The following code creates the event objects. You can include this code in the implementation of FinalConstruct in your plug-in. You will also use these events to manage other threads that you create later in this example.
if(SUCCEEDED(hr))
{
// Create an event to signal running background
// threads to exit.
m_hExitThreadsEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if(0 == m_hExitThreadsEvent)
{
hr = HRESULT_FROM_WIN32(GetLastError());
}
}
if(SUCCEEDED(hr))
{
// Create an event to signal running background
// threads to pause.
m_hPauseThreadsEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if(0 == m_hPauseThreadsEvent)
{
hr = HRESULT_FROM_WIN32(GetLastError());
}
}
It is a good idea to explicitly close these handles because the plug-in does not run in its own process. You should do this as part of your clean-up code in your implementation of FinalRelease.
To ensure that the background process happens once per session, you will need to create a global flag that indicates when the process has run. You should declare the following variable as private member of your plug-in class.
// Flag to only run license update thread once per session.
BOOL m_bRanLicenseUpdate;
Remember to initialize the flag to FALSE in your constructor.
Here is the declaration of the thread procedure for the background thread:
// Thread function for background license updating.
DWORD WINAPI LicenseUpdateThread(void *pArg);
The following example code shows an implementation for startBackgroundProcessing. This method contains the logic for creating or resuming the background thread.
HRESULT CProsewarePlugin::startBackgroundProcessing(HWND hwnd)
{
// This example code runs a background thread once
// per session. You might choose to perform this operation
// less frequently.
HRESULT hr = S_OK;
// Check whether the background thread is live
if(m_hLicenseUpdateThread)
{
DWORD dwExitCode = 0;
if(GetExitCodeThread(m_hLicenseUpdateThread, &dwExitCode))
{
if(STILL_ACTIVE == dwExitCode)
{
// Signal the event to restart the
// license background thread.
ResetEvent(m_hPauseThreadsEvent);
}
}
}
else if(!m_bRanLicenseUpdate)
{
m_hLicenseUpdateThread = CreateThread(NULL, 0,
LicenseUpdateThread,
(LPVOID)this, NULL, 0);
if(NULL == m_hLicenseUpdateThread)
{
hr = HRESULT_FROM_WIN32(GetLastError());
}
else
{
// Set the flag.
m_bRanLicenseUpdate = TRUE;
}
}
return hr;
}
The following code shows an implementation for stopBackgroundProcessing. This method contains the logic to pause the background thread.
HRESULT CProsewarePlugin::stopBackgroundProcessing(void)
{
// Check whether the background thread is live
if(m_hLicenseUpdateThread)
{
DWORD dwExitCode = 0;
if(GetExitCodeThread(m_hLicenseUpdateThread, &dwExitCode))
{
if(STILL_ACTIVE == dwExitCode)
{
// Signal the event to pause the
// license background thread
SetEvent(m_hPauseThreadsEvent);
}
}
}
return S_OK;
}
Using the Plug-in to Request License Pre-delivery
You can create a helper class to manage license pre-delivery in your plug-in. The following example code shows such a class, including inline implementation. The method named Init simply creates the RMGetLicense object. The preDeliverLicense method requests the license pre-delivery.
#include <msnetobj.h> // Header required for RMGetLicense object.
const WCHAR kszPredeliveryURL[] =
L"http://www.proseware.com/MusicStoreLicense.asp";
class CLicenseDelivery
{
private:
CComPtr<IRMGetLicense> m_spGetLicense;
public:
HRESULT Init()
{
return m_spGetLicense.CoCreateInstance(
__uuidof(RMGetLicense),
0,
CLSCTX_INPROC_SERVER);
}
HRESULT preDeliverLicense(BSTR bstrCID,
BSTR bstrUID,
bool bNeedsCopies)
{
if(!m_spGetLicense)
{
return NS_E_INVALID_REQUEST;
}
if(NULL == bstrCID)
{
return E_INVALIDARG;
}
HRESULT hr = S_OK;
CComBSTR bstrURL;
hr = bstrURL.Append(kszPredeliveryURL);
if(SUCCEEDED(hr))
{
hr = bstrURL.Append(L"?cid=");
}
if(SUCCEEDED(hr))
{
hr = bstrURL.AppendBSTR(bstrCID);
}
if(SUCCEEDED(hr))
{
hr = bstrURL.Append(L"&uid=");
}
if(SUCCEEDED(hr))
{
hr = bstrURL.AppendBSTR(bstrUID);
}
if(SUCCEEDED(hr))
{
hr = bstrURL.Append(L"&NeedsCopies=");
}
if(SUCCEEDED(hr))
{
if(true == bNeedsCopies)
{
hr = bstrURL.Append(L"true");
}
else
{
hr = bstrURL.Append(L"false");
}
}
if(SUCCEEDED(hr))
{
hr = m_spGetLicense->GetLicenseFromURL(CComBSTR(NULL), bstrURL);
}
return hr;
}
};
Writing the Background Thread Code
The background thread function brings the pieces together. The Windows Media Player 10 objects enable you to access content you provided on the user's computer and determine the file paths. The license expiration checker class helps you to determine which licenses to request. The license delivery class enables you to request pre-delivery of root and leaf licenses.
From the Format SDK perspective, it is important to understand that there is no concept of leaf and root licenses; there is either a right to play or no right to play. If there is no right to play because the right has expired, the Windows Media Format SDK can convey this information using the DRM_LICENSE_STATE_CATEGORY enumeration. However, when the value returned is simply WM_DRM_LICENSE_STATE_NORIGHT, you cannot know whether this is because the leaf license or the root license is invalid, or both are invalid. This means that when the license expiration checker class returns a content ID value, you must first attempt to retrieve the root license and then attempt to retrieve the leaf license for the same content item.
The following example code shows an implementation for the background license update thread function. The comments will help you to understand the process. Take note of the calls to WaitForSingleObject at the end of the loop block to understand how the thread pauses, resumes, and exits in response to events that are set in the plug-in class.
DWORD WINAPI LicenseUpdateThread(void *pArg)
{
// Do parameter validation.
if(!pArg)
{
SetLastError(E_INVALIDARG);
return 1;
}
// Threads must always call CoInitialize.
CoInitialize(NULL);
// Declare and initialize variables.
HRESULT hr = S_OK;
CComBSTR bstrUID(kszUserID);
CComPtr<IWMPCore> spPlayer; // Smart pointer to IWMPCore interface.
CComPtr<IWMPMediaCollection> spMediaCollection;
CComPtr<IWMPPlaylist> spPlaylist;
CProsewarePlugin *pPlugin = (CProsewarePlugin*)pArg;
long lCount = 0;
bool bGotRoot = false; // Flag to store whether the root
// license has been acquired.
CLicenseDelivery *pLicenseDelivery = NULL;
CLicenseExpirationChecker *pLicExpChk = NULL;
// Variables to query for our content from the library.
CComBSTR bstrAttName; // "WM/ContentDistributor"
CComBSTR bstrAttVal; // "Proseware"
hr = bstrAttName.Append(L"WM/ContentDistributor");
if(SUCCEEDED(hr))
{
hr = bstrAttVal.Append(L"Proseware");
}
if(SUCCEEDED(hr))
{
// Create an instance of the license expiration checker class.
pLicExpChk = new CLicenseExpirationChecker();
if(NULL == pLicExpChk)
{
hr = E_OUTOFMEMORY;
}
}
if(SUCCEEDED(hr))
{
// Create an instance of the license delivery class.
pLicenseDelivery = new CLicenseDelivery;
if(NULL == pLicenseDelivery)
{
hr = E_OUTOFMEMORY;
}
}
if(SUCCEEDED(hr))
{
// Initialize license expiration checker.
hr = pLicExpChk->Initialize();
}
ATLASSERT(pPlugin);
if(SUCCEEDED(hr))
{
// Create an instance of the Windows Media Player 10
// object. We do this here because the plug-in does not
// have a pointer to IWMPCore and creating the object in
// the worker thread avoids the need to marshal COM pointers
// across apartment boundaries.
hr = spPlayer.CoCreateInstance(__uuidof(WindowsMediaPlayer),
0, CLSCTX_INPROC_SERVER);
}
if(SUCCEEDED(hr))
{
// Retrieve a pointer to the media collection.
hr = spPlayer->get_mediaCollection(&spMediaCollection);
}
if(SUCCEEDED(hr))
{
// Retrieve a playlist filled with online store media
// from the user's library.
hr = spMediaCollection->getByAttribute(bstrAttName,
bstrAttVal,
&spPlaylist);
}
if(SUCCEEDED(hr))
{
hr = spPlaylist->get_count(&lCount);
}
if(SUCCEEDED(hr))
{
// Initialize the license delivery class
hr = pLicenseDelivery->Init();
}
if(SUCCEEDED(hr))
{
// We need to cache the last contentID so we can
// handle the case where
// we have the leaf license, but the root is missing
// or corrupted.
CComBSTR bstrLastCID;
// Loop through the playlist.
for(long i = 0; i < lCount; i++)
{
CComPtr<IWMPMedia> spMedia;
CComBSTR bstrURL;
CComBSTR bstrCID;
// Retrieve a pointer to the next media item.
hr = spPlaylist->get_item(i, &spMedia);
if(SUCCEEDED(hr))
{
// Retrieve the path to the media item.
hr = spMedia->get_sourceURL(&bstrURL);
}
if(SUCCEEDED(hr) && bstrURL.Length())
{
// Check the license for expiration within a week.
hr = pLicExpChk->CheckExpiration(bstrURL, 7, &bstrCID);
}
// Test whether the license expiration checker is requesting
// a root license and the root has already been retrieved.
if(SUCCEEDED(hr) && bstrCID.Length() &&
0 == _wcsicmp(bstrCID, L"root") &&
true == bGotRoot)
{
// Cache the CID
bstrLastCID.Empty();
hr = bstrLastCID.AppendBSTR(bstrCID);
// Don't do any license acquisition
// in this iteration.
bstrCID.Empty();
}
if(SUCCEEDED(hr) && bstrCID.Length())
{
// Test whether this CID is a repeat of the last.
// If not, acquire a root instead of a leaf.
// Note: The "!=" operator here is the overloaded one
// from the CComBSTR class. It compares strings,
// not pointers.
if(bstrCID != bstrLastCID &&
false == bGotRoot &&
0 != _wcsicmp(bstrCID, L"root"))
{
// Cache the CID
bstrLastCID.Empty();
hr = bstrLastCID.AppendBSTR(bstrCID);
if(SUCCEEDED(hr))
{
bstrCID.Empty();
bstrCID = L"root";
}
}
else
{
bstrLastCID.Empty();
hr = bstrLastCID.AppendBSTR(bstrCID);
}
if(SUCCEEDED(hr))
{
// The usual scenario is that bstrCID equals "root"
// because the root is expired or missing.
// This call would then get the root license.
// It's possible bstrCID contains a content ID.
// In that case, this call would get the leaf license.
hr = pLicenseDelivery->preDeliverLicense(bstrCID,
bstrUID,
false);
}
if(SUCCEEDED(hr) && 0 == _wcsicmp(bstrCID, L"root"))
{
// Check this item
// again to ensure we have a leaf.
i--;
// Set the flag.
bGotRoot = true;
}
}
// Sleep if the pause event is signaled.
while(WAIT_OBJECT_0 == WaitForSingleObject(
pPlugin->m_hPauseThreadsEvent, 1000))
{
// Keep waiting for the event to be non-signaled.
}
// Exit if the exit event is signaled.
if(WAIT_OBJECT_0 == WaitForSingleObject(
pPlugin->m_hExitThreadsEvent, 0))
{
break;
}
}
}
if(pLicExpChk)
{
delete pLicExpChk;
pLicExpChk = NULL;
}
if(pLicenseDelivery)
{
delete pLicenseDelivery;
pLicenseDelivery = NULL;
}
CoUninitialize();
return SUCCEEDED(hr)?0:1;
}
Exiting the Thread
If the background thread completes its work, it will exit when the thread procedure returns. However, the plug-in may be destroyed before this happens. For example, the user might close Windows Media Player 10. The plug-in should handle this case by explicitly signaling the thread to exit when the plug-in closes. You can add code like the following example to your implementation of FinalRelease.
// Signal threads to exit.
SetEvent(m_hExitThreadsEvent);
// Wake up sleeping background threads.
ResetEvent(m_hPauseThreadsEvent);
HANDLE hThreads[1] = {m_hLicenseUpdateThread};
// Wait for the threads to exit.
WaitForMultipleObjects(1, hThreads, TRUE, 15000);
You will modify this example code in a later section to force shutdown of additional threads that you create.
Preparing Licenses for Synchronization
You can write client-side code that inspects licenses associated with content that Windows Media Player 10 is transferring to a portable device. You might want to do this to ensure that subscription content being transferred isn't about to expire. To make this work, you will need to write C++ code in your Windows Media Player 10 online store plug-in.
Windows Media Player 10 calls IWMPSubscriptionService2::prepareForSync just before synchronization happens. The parameters to the call provide you with a BSTR containing the path to the digital media file about to be transferred, a BSTR containing the canonical name for the device being synchronized, and an IWMPSubscriptionServiceCallback pointer, which you must use to signal Windows Media Player that you are finished working with the file. Part of the Player synchronization process includes synchronizing licenses between the license store on the computer and the license store on the portable device, if the license on the device is near expiration. The strategy in this example is simply to make sure that the license on the computer is up-to-date before the synchronization happens. That way, you can avoid the need to work directly with licenses on the device. This means that the device canonical name information will not be used.
To avoid blocking Windows Media Player 10, it is important that the work to inspect and update licenses in this scenario be done by using a worker thread. This thread should contain a Windows message loop. Because the Player may call prepareForSync multiple times during the same synchronization operation, you should post a custom Windows message to the thread each time the method is called. This technique enables you to use the message queue to serialize the calls to the worker thread, which means the worker thread processes one digital media file at a time. The process works as follows:
- Windows Media Player 10 calls prepareForSync. Here, the online store plug-in creates and fills in a structure that contains information that the worker thread uses to inspect and update the license for the specified digital media file. The structure includes the IWMPSubscriptionServiceCallback pointer that the Player supplied.
- The plug-in posts a custom Windows message to the worker thread's message queue. It remains there until the thread removes it for processing. At this point, the plug-in returns from prepareForSync. The message contains a pointer to the structure created in step 1.
- Eventually, the thread message loop processes the custom Windows message and accesses information in the structure to do the license work.
- The thread inspects and updates the licenses, as needed.
- When the work is completed, the thread posts a different custom Windows message to a window that the plug-in created. The message contains the IWMPSubscriptionServiceCallback pointer.
- The plug-in uses the callback pointer to signal Windows Media Player 10 that work is completed for the digital media file.
The following diagram shows how the process works.
.gif)
The following sections provide the details.
Member Variables
The following example code declares member variables you will need in this scenario. You should declare them as members of your plug-in class and remember to initialize them to NULL in your class constructor.
private:
UINT m_uMarshalCustom; // Window message to complete the callbacks.
HWND m_hPluginWindow; // Handle of the plug-in's hidden window.
HANDLE m_hPrepareForSyncThread; // Thread handle
UINT m_uExitPrepareForSync; // Window message to exit the thread.
DWORD m_dwPrepareForSyncThreadId; // Thread ID.
UINT m_uPrepareMediaSync; // Window message for license work.
About the Plug-in Window
Your online store plug-in should create a hidden window. You will use this window to ensure that callbacks made to Windows Media Player 10 happen on the same thread on which the callback pointer was provided. Sending a window message from your worker thread to this window (which is created on your plug-in's main thread) is a convenient way to make this happen.
Your code should create the hidden window in your implementation of FinalConstruct and destroy the window in your implementation of FinalRelease. How you create the window is up to you. The important thing is that the window handle is available as a member of your plug-in class.
About the Custom Window Messages
You will require three custom window messages for this example: one to post an item to the worker thread for processing, another to send callbacks from the worker thread to the main thread, and the third to cause the worker thread to exit. You should create these window messages by calling the RegisterWindowMessage Windows function, passing a unique name for each message. This function guarantees a unique, new window message throughout the system the first time you call it. Subsequent calls using the same message string will return the same message value as the initial call.
The following example code shows three calls made in FinalConstruct to register the three custom window messages used by the plug-in class.
m_uMarshalCustom = RegisterWindowMessage(_T("ProsewarePlugin_CB"));
if(0 == m_uMarshalCustom)
{
hr = HRESULT_FROM_WIN32(GetLastError());
}
if(SUCCEEDED(hr))
{
m_uExitPrepareForSync = RegisterWindowMessage(
_T("ProsewarePlugin_ExitPrepareForSync"));
if(0 == m_uExitPrepareForSync)
{
hr = HRESULT_FROM_WIN32(GetLastError());
}
}
if(SUCCEEDED(hr))
{
m_uPrepareMediaSync = RegisterWindowMessage(
_T("ProsewarePlugin_PrepareMediaSync"));
if(0 == m_uPrepareMediaSync)
{
hr = HRESULT_FROM_WIN32(GetLastError());
}
}
Keep in mind that you will need to make similar calls in other contexts. For example, your plug-in window will need to handle the ProsewarePlugin_CB message, so you will need to call RegisterWindowMessage in your window code using that same string.
Managing the Worker Thread
This example uses the following custom function:
STDMETHODIMP prepareMedia(BSTR bstrFile,
IWMPSubscriptionServiceCallback *pCB,
HWND hWndCB);
The prepareMedia function does the work of creating the worker thread, if needed, and posting the message to the worker thread to add the new digital media file for license processing. You call prepareMedia from your implementation of prepareForSync. The parameter bstrFile is the file name provided by Windows Media Player 10, the parameter pCB is the callback pointer provided by Windows Media Player 10, and the parameter hWndCB is the handle of the plug-in's hidden window.
This example uses the following custom structure to pass data from the plug-in to the worker thread:
struct ThreadParams{
BSTR bstrName; // Canonical name of device or file name
void *pCB; // Callback pointer
HWND hWndCB; // HWND to send callback pointer to
HANDLE hInit; // Event handle to signal thread has started
HRESULT hr; // Return code from thread initialization
};
For this scenario, the bstrName member corresponds to the file name. The example implementation of prepareMedia creates a new instance of the ThreadParams structure, fills in its members, and posts a message containing a pointer to the structure to the worker thread. The worker thread frees this memory when processing is completed.
The plug-in must not post messages to the thread until the thread has finished initialization. The hInit member provides a handle to an event object that the thread can set to signal when it is ready to receive messages. The code that creates the thread waits for this event to become signaled before proceeding with further processing. You can see this in the following example implementation of the prepareMedia function.
HRESULT CProsewarePlugin::prepareMedia(
BSTR bstrFile,
IWMPSubscriptionServiceCallback *pCB,
HWND hWndCB)
{
if(NULL == hWndCB ||
NULL == pCB)
{
return E_INVALIDARG;
}
HRESULT hr = S_OK;
BOOL bResult = FALSE;
CComBSTR bstrFileName;
bstrFileName.Append(bstrFile);
// Create the thread if none exists.
if(!m_hPrepareForSyncThread)
{
ThreadParams *pTP = NULL;
HANDLE hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);
if(NULL == hEvent)
{
hr = HRESULT_FROM_WIN32(GetLastError());
}
if(SUCCEEDED(hr))
{
pTP = new ThreadParams();
if(!pTP)
{
hr = E_OUTOFMEMORY;
}
}
if(SUCCEEDED(hr))
{
ZeroMemory(pTP, sizeof(ThreadParams));
pTP->hInit = hEvent;
// Create the thread.
// There should only be one thread of this type per session.
m_hPrepareForSyncThread = CreateThread(
NULL,
0,
PrepareForSyncThread,
(void*)pTP, NULL,
&m_dwPrepareForSyncThreadId);
if(m_hPrepareForSyncThread)
{
// Wait for the thread to finish initialization.
if(WAIT_OBJECT_0 != WaitForSingleObject(
pTP->hInit, 30000))
{
// Failed to create thread
CloseHandle(m_hPrepareForSyncThread);
m_hPrepareForSyncThread = NULL;
hr = pTP->hr;
}
// Clean up.
CloseHandle(pTP->hInit);
pTP->hInit = NULL;
delete pTP;
pTP = NULL;
}
}
}
// If we have a worker thread, fill in the ThreadParams struct
// and post a message.
if(m_hPrepareForSyncThread)
{
// Prepare the message
ThreadParams *pThreadParams = new ThreadParams();
if(!pThreadParams)
{
hr = E_OUTOFMEMORY;
}
if(SUCCEEDED(hr))
{
// Increment the reference count on the
// callback pointer.
pCB->AddRef();
// Fill in the ThreadParams struct.
ZeroMemory(pThreadParams, sizeof(ThreadParams));
bstrFileName.CopyTo(&pThreadParams->bstrName);
pThreadParams->hWndCB = hWndCB;
pThreadParams->pCB = reinterpret_cast<void*>(pCB);
if(m_dwPrepareForSyncThreadId)
{
// Send the struct pointer to the thread's message queue.
bResult = PostThreadMessage(m_dwPrepareForSyncThreadId,
m_uPrepareMediaSync, 0,
(LPARAM)pThreadParams);
}
if(FALSE == bResult)
{
hr = HRESULT_FROM_WIN32(GetLastError());
}
}
}
return hr;
}
The following example code shows the declaration of the thread procedure for the worker thread.
// Thread function for prepare for sync.
DWORD WINAPI PrepareForSyncThread(void *pArg);
When the worker thread starts up, it performs the following steps:
- Creates an instance of the CLicenseDelivery helper class described in a previous section.
- Creates an instance of the CLicenseExpirationChecker helper class described in a previous section.
- Registers two of the custom window messages.
- Specifies a return code and sets the event to signal the plug-in that the thread has completed initialization.
- Starts the message loop.
The message loop only processes two of the custom window messages created for the example. One is the message to prepare the digital media file for synchronization. The other is the message that signals the thread to exit. Any other messages are simply dispatched.
When the thread receives a window message to prepare a file for synchronization, it calls a custom function to do the work.
When the thread receives the message to exit, the code empties the thread message queue. For each message in the queue, it posts a callback message containing a failure code to the hidden window plug-in window. This signals Windows Media Player 10 that processing was not completed for the associated digital media files.
The following example code shows an implementation for the thread procedure.
DWORD WINAPI PrepareForSyncThread(void *pArg)
{
// Parameter checking.
if(NULL == pArg)
{
SetLastError(E_INVALIDARG);
return 1;
}
CoInitialize(NULL);
MSG msg;
ZeroMemory(&msg, sizeof(MSG));
HRESULT hr = S_OK;
CLicenseExpirationChecker *pLicExpChk = NULL;
ThreadParams *pTP = reinterpret_cast<ThreadParams*>(pArg);
UINT uExitPrepareForSync = 0;
UINT uPrepareMediaSync = 0;
// Create an instance of the license deliver class.
CLicenseDelivery *pLicenseDelivery = new CLicenseDelivery();
if(NULL == pLicenseDelivery)
{
hr = E_OUTOFMEMORY;
}
if(SUCCEEDED(hr))
{
hr = pLicenseDelivery->Init();
}
if(SUCCEEDED(hr))
{
// Create an instance of the license expiration checker class.
pLicExpChk = new CLicenseExpirationChecker();
if(NULL == pLicExpChk)
{
hr = E_OUTOFMEMORY;
}
}
if(SUCCEEDED(hr))
{
hr = pLicExpChk->Initialize();
}
if(SUCCEEDED(hr))
{
uExitPrepareForSync = RegisterWindowMessage(
_T("ProsewarePlugin_ExitPrepareForSync"));
uPrepareMediaSync = RegisterWindowMessage(
_T("ProsewarePlugin_PrepareMediaSync"));
}
// Set the return code.
pTP->hr = hr;
// Signal that we're ready to pump messages.
SetEvent(pTP->hInit);
if(SUCCEEDED(hr))
{
BOOL bRet = FALSE;
while((bRet = ::GetMessage(&msg, 0, 0, 0)) != 0)
{
if(-1 == bRet)
{
hr = HRESULT_FROM_WIN32(GetLastError());
break;
}
if(uPrepareMediaSync == msg.message)
{
if(msg.lParam != NULL)
{
PrepareMediaForSync(
pLicenseDelivery,
pLicExpChk,
(LPVOID)msg.lParam);
}
}
else if(uExitPrepareForSync == msg.message)
{
UINT uMarshalCustom = RegisterWindowMessage(
_T("ProsewarePlugin_CB"));
if(NULL == uMarshalCustom)
{
hr = HRESULT_FROM_WIN32(GetLastError());
}
if(SUCCEEDED(hr))
{
// Empty the message queue.
while(0 == PeekMessage(
&msg,
(HWND)INVALID_HANDLE_VALUE,
0, 0, PM_REMOVE))
{
if(uPrepareMediaSync == msg.message)
{
ThreadParams *pPrep =
reinterpret_cast<ThreadParams*>(msg.lParam);
ATLASSERT(pPrep);
HWND hwndCB = pPrep->hWndCB;
void *pCB = pPrep->pCB;
if(hwndCB && pCB)
{
// Do the callback
PostMessage(hwndCB,
uMarshalCustom,
(WPARAM)NS_E_USER_STOP,
(LPARAM)pCB);
}
SysFreeString(pPrep->bstrName);
delete pPrep;
pPrep = NULL;
}
else
{
DispatchMessage(&msg);
}
}
}
break;
}
else
{
DispatchMessage(&msg);
}
}
}
if(pLicExpChk)
{
delete pLicExpChk;
pLicExpChk = NULL;
}
CoUninitialize();
return SUCCEEDED(hr)?0:1;
}
Preparing Digital Media for Synchronization
When the message loop in the worker thread receives a message to prepare a digital media file for synchronization, it calls the following custom function.
void PrepareMediaForSync(CLicenseDelivery *pLicenseDelivery,
CLicenseExpirationChecker *pLicExpChk,
void* pv)
The PrepareMediaForSync function does the work of inspecting and updating the licenses. The pLicenseDelivery parameter is a pointer to the CLicenseDelivery class that the worker thread created. The pLicExpChk parameter is a pointer to the CLicenseExpirationChecker class that the worker thread created. The pv parameter points to the ThreadParams structure contained in the custom window message.
The work performed by the function is similar to that done by the background license update thread. It uses the CLicenseExpirationChecker class to determine whether the license needs to be updated. This happens twice, for the same reasons described in the previous section. The code also calls a new function you must add to the CLicenseExpirationChecker class. This function, named CheckCopyCount, inspects the license to determine whether the license has any remaining rights to copy. Keep in mind that the copy count right refers to the count of successful license transfers to devices, not the number of times the user has attempted to transfer the content.
The signature and usage of the CheckCopyCount function is similar to the signature of the CheckExpiration function. The following example code shows an implementation for CheckCopyCount.
HRESULT CLicenseExpirationChecker::CheckCopyCount(
BSTR bstrFileName,
DWORD dwCountWindow,
BSTR* pbstrUpdateContentID)
{
// Check for initialization.
if(!m_spDRMEditor)
{
return NS_E_INVALID_REQUEST;
}
// Parameter checking.
if(NULL == pbstrUpdateContentID ||
NULL == bstrFileName ||
0 == SysStringLen(bstrFileName))
{
return E_INVALIDARG;
}
///// Local variables
HRESULT hr = S_OK;
// Variables for retrieving the license state information.
WM_LICENSE_STATE_DATA* pWMLicState = NULL;
WMT_ATTR_DATATYPE DataType = WMT_TYPE_DWORD;
BYTE* pbData = NULL;
WORD cbData = 0;
BOOL bProtected = FALSE;
// Make sure the content is DRM protected.
hr = WMIsContentProtected((WCHAR*)bstrFileName, &bProtected);
if(SUCCEEDED(hr))
{
if(FALSE == bProtected)
{
hr = NS_E_INVALID_REQUEST;
}
}
if(SUCCEEDED(hr))
{
// Open the file for metadata editing.
hr = m_spEditor->Open((WCHAR*)bstrFileName);
}
if(SUCCEEDED(hr))
{
// Get the license state data.
// First get the size of the data.
hr = m_spDRMEditor->GetDRMProperty(g_wszWMDRM_LicenseState_Copy,
&DataType,
NULL,
&cbData);
}
// License state data attributes should always be a binary data type.
if(SUCCEEDED(hr) && DataType != WMT_TYPE_BINARY)
{
hr = E_UNEXPECTED;
}
if(SUCCEEDED(hr))
{
// Allocate memory for the data.
pbData = new BYTE[cbData];
if(pbData == NULL)
{
hr = E_OUTOFMEMORY;
}
}
if(SUCCEEDED(hr))
{
// Get the actual data.
hr = m_spDRMEditor->GetDRMProperty(g_wszWMDRM_LicenseState_Copy,
&DataType,
pbData,
&cbData);
}
if(SUCCEEDED(hr))
{
// Set the license state data pointer.
pWMLicState = (WM_LICENSE_STATE_DATA*)pbData;
switch(pWMLicState->stateData->dwCategory)
{
// If the state category is NORIGHT, then either the
// leaf license or the root license needs to be acquired.
// In this case, set the string to the content ID.
case WM_DRM_LICENSE_STATE_NORIGHT:
{
hr = GetContentID(pbstrUpdateContentID);
break;
}
case WM_DRM_LICENSE_STATE_COUNT:
case WM_DRM_LICENSE_STATE_COUNT_FROM:
case WM_DRM_LICENSE_STATE_COUNT_UNTIL:
case WM_DRM_LICENSE_STATE_COUNT_FROM_UNTIL:
{
// Compare the count with the specified window.
if(pWMLicState->stateData->dwCount[0] < dwCountWindow)
{
hr = GetContentID(pbstrUpdateContentID);
}
break;
}
default:
{
hr = E_UNEXPECTED;
break;
}
}
}
m_spEditor->Close();
if(pbData)
{
delete[] pbData;
pbData = NULL;
}
pWMLicState = NULL;
return hr;
}
The following example code shows an implementation for the PrepareMediaForSync function. Notice that the third parameter to CLicenseDelivery::preDeliverlicense is set to true when the copy count has decremented to zero. This is the NeedsCopies parameter. In this case, the licensing server needs to issue one additional license to copy. You should recall that the business rules enumerated at the outset of this article specified this behavior.
void PrepareMediaForSync(CLicenseDelivery *pLicenseDelivery,
CLicenseExpirationChecker *pLicExpChk,
void* pv)
{
ATLASSERT(pLicenseDelivery);
ATLASSERT(pLicExpChk);
ATLASSERT(pv);
HRESULT hr = S_OK;
static bool bHaveRoot = false; // Only get the root once per session.
ThreadParams *pPrep = reinterpret_cast<ThreadParams*>(pv);
CComBSTR bstrFileName;
bstrFileName.Attach(pPrep->bstrName);
CComBSTR bstrUID;
hr = bstrUID.Append(kszUserID);
HWND hwndCB = pPrep->hWndCB;
void *pCB = pPrep->pCB;
CComBSTR bstrCID;
// Free the ThreadParams struct.
if(pPrep)
{
delete pPrep;
pPrep = NULL;
}
hr = pLicExpChk->CheckExpiration(bstrFileName, 7, &bstrCID);
if(SUCCEEDED(hr) && bstrCID.Length())
{
// The normal scenario is that bstrCID == "root" when the root
// is expired.
// This code would then get the root license.
// It's possible bstrCID contains a content ID. In that case,
// This code would get the leaf license.
hr = pLicenseDelivery->preDeliverLicense(bstrCID, bstrUID, false);
if(SUCCEEDED(hr))
{
// Check a second time in case the root is invalid.
// If the first pre-delivery got a leaf, we might still
// need a root.
bstrCID.Empty();
hr = pLicExpChk->CheckExpiration(bstrFileName, 7, &bstrCID);
}
if(SUCCEEDED(hr) &&
bstrCID.Length() &&
false == bHaveRoot)
{
// Retrieve a root license.
hr = pLicenseDelivery->preDeliverLicense(CComBSTR("root"),
bstrUID, false);
if(SUCCEEDED(hr))
{
bHaveRoot = true;
}
}
}
bstrCID.Empty();
// Test whether this content has copy rights left.
hr = pLicExpChk->CheckCopyCount(bstrFileName, 1, &bstrCID);
if(SUCCEEDED(hr) && bstrCID.Length())
{
// Set the third parameter to true to signal the
// server code that you're asking for a leaf because
// the copy count is 0.
// This gives the server a chance to apply
// business rules to determine whether
// the user should be granted additional copies.
hr = pLicenseDelivery->preDeliverLicense(bstrCID, bstrUID, true);
}
// Post the callback message back to the main thread.
if(hwndCB && pCB)
{
UINT uMarshalCustom = RegisterWindowMessage(
_T("ProsewarePlugin_CB"));
if(uMarshalCustom)
{
// Post a message to the plug-in window to make
// the callback to Windows Media Player.
PostMessage(hwndCB, uMarshalCustom, (WPARAM)hr, (LPARAM)pCB);
}
}
}
Making the Callback
The final section of the preceding example code posts the custom window message for the plug-in window to handle. Windows Media Player 10 requires you to call IWMPSubscriptionServiceCallback::onComplete using pointer that it provided. This signals Windows Media Player that processing is complete for the digital media file associated with the callback pointer. If you return a success HRESULT code, Windows Media Player proceeds with the file transfer to the portable device; otherwise, Windows Media Player displays an error message in its user interface. Posting the custom message to the plug-in window forces this call to happen on the plug-in thread instead of the worker thread. You will see this same technique used again in an upcoming section about metering.
(It is interesting to note that COM interface pointers usually must be marshaled when they are passed between apartments. When the callback pointer is passed to the worker thread, it crosses an apartment boundary. However, because the callback pointer is never used by the worker thread, no marshaling is required.)
The following example code shows an implementation for a function named OnMarshalCB. This is an example of the function your window might call in response receiving the ProsewarePlugin_CB window message.
LRESULT OnMarshalCB(UINT nMsg,
WPARAM wParam,
LPARAM lParam,
BOOL& bHandled)
{
CComPtr<IWMPSubscriptionServiceCallback> spCB;
// Note that the smart pointer will call Release() on the
// callback pointer (the LPARAM) when out of scope.
spCB.Attach(
reinterpret_cast<IWMPSubscriptionServiceCallback*>(lParam));
if(spCB)
{
spCB->onComplete((HRESULT)wParam);
}
return 0;
}
Exiting the Thread
You must now modify your code in FinalRelease to ensure that the worker thread exits when the plug-in shuts down. The following example code modifies the exit thread example code shown in the previous section. It adds a call to PostThreadMessage to signal the worker thread to exit and adds the worker thread handle to the array of handles passed to WaitForMultipleObjects.
// Wake up sleeping background threads.
ResetEvent(m_hPauseThreadsEvent);
// Signal threads to exit.
SetEvent(m_hExitThreadsEvent);
PostThreadMessage(m_dwPrepareForSyncThreadId,
m_uExitPrepareForSync,
0,
0);
HANDLE hThreads[2] = {m_hLicenseUpdateThread,
m_hPrepareForSyncThread};
// Wait for the threads to exit.
WaitForMultipleObjects(2, hThreads, TRUE, 15000);
Metering Content Usage
You can use your online store plug-in to perform metering functions. For an overview of how metering works and why you would want to do it, see Metering the Use of Digital Media Content with Windows Media DRM 10 on the Microsoft Web site (http://www.microsoft.com/windows/windowsmedia/howto/articles/metering_with_drm10.aspx).
In general, the metering process works as follows:
- The plug-in retrieves data from the license store on the user's computer or portable device. This data is called the metering challenge. This step requires the Windows Media Device Manager 10 SDK.
- The data is transmitted using HTTP to a metering aggregation service. This service is implemented as an ASP page on a server running Windows Server 2003. The service requires the Windows Media Rights Manager 10 SDK.
- The metering aggregation service uses the metering challenge to retrieve the metering data and store it.
- The service generates a metering response and transmits it back to the client computer (the plug-in in this example).
- The plug-in uses the metering response to reset the license store. This step is performed using the Windows Media Device Manager 10 SDK.
You can transmit the metering challenge and receive the response using any technology you choose. In this example, it is assumed the metering challenge is sent to the ASP page as an HTML FORM POST operation. This means that the ASP page retrieves challenge as a name/value pair. The ASP page returns the metering response by calling Response.Write.
It is up to you to write the code that uses HTTP to transmit the challenge to an ASP page and receive the response. The key point to remember is that the challenge and response strings must remain unchanged after each transmission for the processing to work.
Creating the Metering Aggregation Service Page
You perform metering aggregation by using the Windows Media Rights Manager 10 SDK objects in an ASP page. The following example code creates an ASP page for the metering aggregation service for the example subscription online store.
<%@ LANGUAGE="VBScript"%>
<%
Response.Buffer = True
Response.Expires = 0
Do
On Error Resume Next
'"""""""""""""""""""""""""""""""""""""""""""""""""""""
' Declare variables
'"""""""""""""""""""""""""""""""""""""""""""""""""""""
Dim MObj ' WMRMMetering object
Dim MDataObj ' WMRMMeteringData object
Dim MContentCollObj ' WMRMMeteringContentCollection object
Dim MContentObj ' WMRMMeteringContent object
Dim MActionCollObj ' WMRMMeteringActionCollection object
Dim MActionObj ' WMRMMeteringAction object
Dim MeterChallenge ' Metering challenge from the client
Dim MASPrivateKey ' Private key
Dim MeterCert ' Metering certificate
Dim MeterID ' Metering ID
Dim TransID ' Transaction ID
Dim ContentCollLength ' Number of items in the
' content collection
Dim ContentKeyID ' Key ID for a content item
Dim ActionCollLength ' Number of items in the action collection
Dim ActionName ' Action name
Dim ActionValue ' Action count
Dim MeterResponseString ' Metering response string
Dim x, y, a, b ' Counters
Dim ActionPlay ' Metered play count
Dim ActionCopy ' Metered copy count.
'"""""""""""""""""""""""""""""""""""""""""""""""""""""
' Set variables.
'"""""""""""""""""""""""""""""""""""""""""""""""""""""
' This example assumes that the metering challenge was transmitted
' using the name "mchall".
MeterChallenge = Request.form("mchall")
MeterCert = "<Replace this with the metering certificate>"
MASPrivateKey = "<Replace this with the private key.>"
'"""""""""""""""""""""""""""""""""""""""""""""""""""""
' Extract metering data as a WMRMMeteringData object and as a string.
'"""""""""""""""""""""""""""""""""""""""""""""""""""""
Set MObj = Server.CreateObject("WMRMObjs.WMRMMetering")
MObj.ServerPrivateKey = MASPrivateKey
MObj.Challenge = MeterChallenge
Set MDataObj = MObj.GetMeteringData
MeterID = MDataObj.MeteringId
TransID = MDataObj.TransactionId
'"""""""""""""""""""""""""""""""""""""""""""""""""""""
' Retrieve the collection of content items.
'"""""""""""""""""""""""""""""""""""""""""""""""""""""
Set MContentCollObj = MDataObj.ContentCollection
ContentCollLength = MContentCollObj.length
'"""""""""""""""""""""""""""""""""""""""""""""""""""""
' Retrieve the key ID and action data for each content item.
'"""""""""""""""""""""""""""""""""""""""""""""""""""""
For x = 0 To (ContentCollLength - 1)
ActionPlay = 0
ActionCopy = 0
Set MContentObj = MContentCollObj.item(x)
ContentKeyID = MContentObj.KeyID
' Retrieve the collection of actions for the current content item.
Set MActionCollObj = MContentObj.Actions
ActionCollLength = MActionCollObj.length
' Retrieve each action and its value.
For y = 0 To ActionCollLength - 1
Set MActionObj = MActionCollObj.item(y)
ActionName = MActionObj.Name
ActionValue = MActionObj.Value
If (ActionName = "Play") then ActionPlay = ActionValue
If (ActionName = "Copy") then ActionCopy = ActionValue
Next
' AggregateData() is a custom function not implemented
' in this example. In practice, this function would
' increment play count and copy count values in a database.
' Including the transaction ID ensures that actions
' are not counted multiple times.
' You should adjust counts to use the most
' recent totals for a given transaction ID.
AggregateData(ContentKeyID, TransID, ActionPlay, ActionCopy)
Next
'"""""""""""""""""""""""""""""""""""""""""""""""""""""
' Generate the metering response.
'"""""""""""""""""""""""""""""""""""""""""""""""""""""
MObj.MeteringCertificate = MeterCert
MeterResponseString = MObj.GetMeteringResponse
response.write MeterResponseString
Loop while false
%>
Using the Plug-in for Metering
Like the other processes described in this article, client-side metering work should happen on a separate thread. This example creates a single thread for metering operations when Windows Media Player 10 calls IWMPSubscriptionService2::deviceAvailable. If a metering operation is in progress, the example code simply rejects any subsequent calls to perform metering, which means only one metering operation at a time is permitted. You might decide to use a thread pool in order to support concurrent metering operations.
Like IWMPSubscription2::prepareForSync, the call to deviceAvailable provides an IWMPSubscriptionServicCallback pointer. In this scenario, your code will once again post a message to the plug-in's hidden window to complete the callback.
To use the Windows Media Device Manager 10 SDK objects and interfaces in this example, you must include the following files:
// WMDM 10 includes.
#include <wmdrmdeviceapp.h>
#include <Scclient.h>
#include <mswmdm_i.c>
#include "wmdrmdeviceapp_i.c"
The files Wmdrmdeviceapp.h, Scclient.h, and Mswmdm_i.c are installed with the Windows Media Format 9.5 SDK in the \WMDM\Inc folder. The file Wmdrmdeviceapp_i.c defines the CLSID, LIBID, and IIDs for the WMDRMDeviceApp object. You generate this file by including WMDRMDeviceApp.idl (which imports WMDM.idl) in your project and compiling it by using the MIDL compiler. You must also add a reference to the file Mssachlp.lib, which can be found in the \WMDM\Lib folder.
Member Variables
The following example code declares a member variable you will need in this scenario. You should declare this variable as a member of your plug-in class and remember to initialize it to NULL in your class constructor.
// Thread handle for the
// metering worker thread.
HANDLE m_hMeteringThread
Managing the worker thread
This example uses the following custom function:
STDMETHODIMP doMetering(BSTR bstrDeviceName,
IWMPSubscriptionServiceCallback *pCB,
HWND hWndCB);
The function doMetering does the work of creating the worker thread to start the metering process. You call prepareMedia from your implementation of deviceAvailable. The parameter bstrDeviceName is the canonical name of the device provided by Windows Media Player 10. The parameter pCB is the callback pointer provided by Windows Media Player 10. The parameter hWndCB is the handle of the plug-in's hidden window.
Your metering code will reuse the ThreadParams structure you defined in a previous section to pass information to the worker thread. For this scenario, the bstrName member corresponds to the device name. The example implementation of doMetering creates a new instance of the ThreadParams structure, fills in its members, and passes a pointer to the structure as an argument to CreateThread. The worker thread frees this memory when processing is completed.
The following example code shows an implementation for doMetering.
HRESULT CProsewarePlugin::doMetering(BSTR bstrDeviceName,
IWMPSubscriptionServiceCallback *pCB,
HWND hWndCB)
{
HRESULT hr = S_OK;
CComBSTR bstrDevName;
hr = bstrDevName.Append(bstrDeviceName);
void *pvCB = NULL;
if(SUCCEEDED(hr) && m_hMeteringThread)
{
DWORD dwExitCode = 0;
// Test whether the thread handle represents
// an active metering thread. If not, close
// it so we can create a new thread.
if(GetExitCodeThread(m_hMeteringThread, &dwExitCode))
{
if(STILL_ACTIVE != dwExitCode)
{
CloseHandle(m_hMeteringThread);
m_hMeteringThread = NULL;
}
}
}
if(SUCCEEDED(hr) &&
NULL == m_hMeteringThread)
{
ThreadParams *pTP = new ThreadParams;
if(NULL == pTP)
{
hr = E_OUTOFMEMORY;
}
if(SUCCEEDED(hr))
{
if(pCB)
{
pCB->AddRef();
pvCB = reinterpret_cast<void*>(pCB);
}
// Fill in the struct.
ZeroMemory(pTP, sizeof(ThreadParams));
bstrDevName.CopyTo(&pTP->bstrName);
pTP->pCB = pvCB;
pTP->hWndCB = hWndCB;
m_hMeteringThread = CreateThread(NULL, 0,
MeteringThread, (LPVOID)pTP, NULL, 0);
}
}
else
{
hr = E_ACCESSDENIED;
}
return hr;
}
The thread procedure performs the following basic steps:
- Cocreates the WMDRMDeviceApp object.
- Cocreates the MediaDevMgr object.
- Retrieves a pointer to the IWMDeviceManager2 interface.
- Retrieves the canonical name of the device.
- Verifies the device status.
- Generates a metering challenge.
- Transmits the challenge to the metering aggregation service and receives a response.
- Processes the metering response.
The following example code shows an implementation for the thread procedure.
// Variable to contain metering certificate.
const WCHAR *METERCERT = L"<Replace this with the metering certificate.>";
DWORD WINAPI MeteringThread(void *pArg)
{
if(!pArg)
{
return 1;
}
CoInitialize(NULL);
HRESULT hr = S_OK;
// Get the members from the param struct.
// It is valid for all of these to be NULL
// except pThis.
ThreadParams *pTP = reinterpret_cast<ThreadParams*>(pArg);
void *pCB = pTP->pCB;
CComBSTR bstrDeviceName;
bstrDeviceName.Attach(pTP->bstrName);
HWND hwndCB = pTP->hWndCB;
CComPtr<IWMDRMDeviceApp> spDeviceApp;
CComPtr<IWMDeviceManager> spDevMgr;
CComPtr<IWMDeviceManager2> spDevMgr2;
CComPtr<IWMDMDevice> spDevice;
CComBSTR bstrMeterCert;
hr = bstrMeterCert.Append(METERCERT);
CComBSTR bstrMeterURL;
CComBSTR bstrMeteringData;
CComBSTR bstrResponse;
DWORD dwProcessFlags = 0;
DWORD dwBufferSize = 0;
if(SUCCEEDED(hr))
{
// Create the WMDRMDeviceApp object.
hr = spDeviceApp.CoCreateInstance(CLSID_WMDRMDeviceApp, 0,
CLSCTX_ALL);
}
if(SUCCEEDED(hr))
{
// Create the Device Manager object.
hr = spDevMgr.CoCreateInstance(CLSID_MediaDevMgr, 0, CLSCTX_ALL);
}
// Test whether called with a device name.
if(bstrDeviceName.Length() > 0)
{
// QueryInterface for IWMDeviceManager2.
hr = spDevMgr.QueryInterface(&spDevMgr2);
if(SUCCEEDED(hr))
{
// Retrieve a pointer to the device.
hr = spDevMgr2->GetDeviceFromCanonicalName(bstrDeviceName,
&spDevice);
}
if(SUCCEEDED(hr) && spDevice)
{
// Verify that the device supports metering.
CComPtr<IWMDMDevice2> spDevice2;
DWORD dwFlags = 0;
hr = spDevice->QueryInterface(&spDevice2);
if(SUCCEEDED(hr))
{
hr = spDeviceApp->QueryDeviceStatus(spDevice2, &dwFlags);
}
if(SUCCEEDED(hr))
{
if(!(dwFlags & WMDRM_DEVICE_ISWMDRM))
{
hr = NS_E_DEVICE_NOT_WMDRM_DEVICE;
}
}
}
}
if(SUCCEEDED(hr) &&
spDeviceApp)
{
// NULL device pointer means to meter the PC license store.
hr = spDeviceApp->GenerateMeterChallenge(spDevice,
bstrMeterCert,
&bstrMeterURL,
&bstrMeteringData);
}
if(SUCCEEDED(hr) &&
bstrMeteringData.Length())
{
// Call the function that sends the challenge to the ASP
// page using HTTP and receives the metering response.
// This is a placeholder function for this example.
hr = doChallengeResponse(bstrMeterURL,
bstrMeteringData,
&bstrResponse);
}
if(SUCCEEDED(hr) &&
bstrResponse.Length())
{
dwBufferSize = bstrResponse.ByteLength();
BYTE *pbyte = new BYTE[dwBufferSize];
if(NULL == pbyte)
{
hr = E_OUTOFMEMORY;
}
if(SUCCEEDED(hr))
{
// Process the metering response to reset the
// the metering store.
memcpy(pbyte, bstrResponse.m_str, dwBufferSize);
hr = spDeviceApp->ProcessMeterResponse(spDevice, pbyte,
dwBufferSize,
&dwProcessFlags);
}
if(pbyte)
{
delete[] pbyte;
pbyte = NULL;
}
}
if(hwndCB && pCB)
{
UINT uMarshalCustom = RegisterWindowMessage(
_T("MSSampleMusicPlugin_CB"));
if(uMarshalCustom)
{
// Post a message to the plug-in window to make the callback
// to Windows Media Player.
PostMessage(hwndCB, uMarshalCustom, (WPARAM)hr, (LPARAM)pCB);
}
}
if(pTP)
{
delete pTP;
pTP = NULL;
}
ATLASSERT(hr);
CoUninitialize();
return SUCCEEDED(hr)? 0:1;
}
In the preceding code, doChallengeResponse is a custom function that transmits the metering challenge to the metering aggregation service and retrieves the response string. This article does not provide an example implementation for this function.
Note that the value returned in the fourth parameter of the call to ProcessMeterResponse (dwProcessFlags) may indicate that the metering response is a partial one. This sample ignores this value, processing only whatever data is returned in the single call. You might want to use this information to request and process data repeatedly.
You should notice that the thread posts a window message to the plug-in's hidden window to complete the callback to Windows Media Player 10. This is the same mechanism used in the previous section about preparing for synchronization. In this case, the callback signals Windows Media Player 10 that you have completed working with the portable device. The HRESULT code you return is not conveyed to the user. You should always return S_OK in this instance.
Exiting the Thread
You must now modify your code in your plug-in class implementation of FinalRelease to ensure that the worker thread exits when the plug-in shuts down. The following example code modifies the exit thread example code shown in the previous section. It adds the metering worker thread handle to the array of handles passed to WaitForMultipleObjects.
HANDLE hThreads[3] = {m_hLicenseUpdateThread,
m_hPrepareForSyncThread,
m_hMeteringThread};
// Wait for the threads to exit.
WaitForMultipleObjects(3, hThreads, TRUE, 15000);
For More Information
- For general information about Windows Media technologies, see the Windows Media Web page (http://www.microsoft.com/windows/windowsmedia/).
- To download the latest setup packages for the Windows Media SDKs, see the Windows Media Downloads page on the MSDN Web site (http://msdn.microsoft.com/downloads/list/winmedia.asp).
- To view the latest Windows Media SDK documentation online, see the About the Windows Media SDK Components page on the MSDN Web site (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnanchor/html/anch_winmedsdk.asp).