Friday, November 21, 2008
Windows Media Attributes

I need to read the attributes of windows media files, audio and video. I can do it by automating the Media Encoder but we really don't want to force that 10MB full GUI onto the user machines, is there a way to do this directly?

 

"Directly"? Yeah, you can get the ASF/WM format spec and parse, but Perl or PHP would be the better tool for that (GetID3.org has the php libraries for reading lots of media formats). But since we're in MS land with higher level MS tools, just shift your code from the Media Encoder SDK to the MediaFormat SDK.

We made a demo app for you that shows the SDK attribute code ported to VB.Net, it should get you started ...


...and there's a little story behind the port that might help you out too.

About a year ago we had out first Windows Media project, we designed the whole architecture that included retail client apps used to create wmv video files and/or convert other formats to MBR stream files, all the server hardware and all of services from "secured" ftp to web services that would connect both sides of the whole. And of course we then had to make it all work.

It was truly a great gig, one of our very favorites because we got to learn about a lot of new and different things and, because it was a startup, we got the rush of having only a few months to go from nothing to live. We did it and from hardware to client app, it ran like a top. But there was one detail that was weak... the code for the attributes of the media files.

Our client apps that made and converted (and trimmed and compiled and and and) had to set various stock attribute values such as Author, Copyright, etc. Plus, rather than passing the videos and a separate xml or dat file up to the server with extended information we wanted to put that information right into the file itself, branding it with our special required company data (Detailed descriptions of the content, GUID strings provided by the server prior to transcoding that would link the uploaded videos to pre-created db records, etc.).

Like you, we preferred not installing the ram-heavy Media Encoder on every client machine. Being a RAD tool project with limited time we weren't in a position to do the DirectShow video processing from scratch in C++ so we tried out every COM wrapper we could find for the video capture and decided on VidCapX from Fathsoft. This widget is fantastic in that unlike the Media Encoder it shows realtime previews even if you're not actively encoding, plus it encodes and transcodes using custom Profiles, combines clips from different formats using marker locations and has nearly all of the other bells and whistles of WMFormat and the "Never-in-.Net" DirectShow.  What's more it checks in under a MB, runs on everything from Win98 up (the Encoder requires 2k or XP) and it processes with a very small load on the cpu (Media Encoder as everyone knows likes to live at 100% cpu even on a 3Ghz HT box). What it doesn't do is attributes. But that was ok because we saw in the Media Format SDK a c# sample of how to do this with a 21 function COM interface and since the sample wasn't specifically using Unsafe blocks it looked like an easy port to VB.Net.

Now, most tipsters just say to compile that sample code into a c# dll and call it from your VB.Net app. But we really didn't want to do that. VB.Net turned out to be the perfect choice for the client GUI using the Interopped widget, the various Windows Services, Web Services and db communication. Adding in a c# dll just for this part of the spec was like hanging all of the 20 foot paintings in a museum perfectly and letting one 8x10 photo dangle at an angle.

On the servers where attributes were being read by timed services, it took only a few minutes to get the VB.Net code running. Because we were working on the backend first, we used Encoder SDK to create some test files and without much effort used standard C dll style to declare the two required functions (they happened to be the first two of the interface) and gave it a quick test. It ran as desired and, with many other features to get runnning, we moved to other functionality with a mental ToDo note to come back and test it thoroughly.

We knew at the time that this was an important moment ... but under typical startup pressures we didn't notice the downside of our implementation.

Below is code based on what we put on the upload servers. VB.Net doing an iteration of the attributes. It calls into the WMVCore.dll file which is part of Media Player 9 & 10 package. For demonstration purposes we've made it fill in an Arraylist of a simple custom keyvalue objects so you can just databind the return to a grid. In our server system we only cared about certain attributes and so during the iteration we filled specific private variables and exposed them as readonly properties.

We've also included a couple of functions that are a bit hacky but they come in handy: IsWMFile and IsMBR. These are both Shared so you don't have to create an object instance to check them.

IsWMFile peeks at the file header and verifies that it's a Windows Media file.

IsMBR tells you whether the file has a MultipleBitRate profile (a single file with more than one set of audio or video streams, this type of file is used by Windows Media Services to send different bitrate streams to remote players depending on connection speeds). Counting the profiles and streams is a single function call using the Windows Media Encoder SDK, but to get it done quickly with Media Format SDK you seewejust try to read a stream at postion 3, if the file errors then there is no second set of audio/video (postions 0,1 and 2 would be the first video and left/right audiostreams). This isn't perfect because you might hit a file with two mono audio streams at different bitrates, but it handles most files you'll be working with, and if you need added verification you can just cross reference this information with the return of the standard "HasAudio" and "HasVideo" attributes.

This code works ... but we don't suggest that you use it as it's written. Take a look at it and see if you can spot its' main error.

Imports System.Runtime.InteropServices


Public Class WMFFunctions


   Public Declare Auto Function WMCreateEditor Lib "WMVCore.dll" Alias _
      "WMCreateEditor" (ByRef ppMetadataEditor As IWMMetadataEditor) As UInt32

   <Guid("96406BD9-2B2B-11d3-B36B-00C04F6108FF"),_
       InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
         Public Interface IWMMetadataEditor

      Function Open(ByVal pwszFilename As String) As UInt32

      Function Close() As UInt32

      Function Flush() As UInt32

   End Interface


   <Guid("15CC68E3-27CC-4ecd-B222-3F5D02D80BD5"), _
       InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
      Public Interface IWMHeaderInfo3

      Function GetAttributeCount(ByVal wStreamNum As UInt16, _
         ByRef pcAttributes As UInt16) As UInt32

      Function GetAttributeByIndex(ByVal wIndex As UInt16, _
         ByRef pwStreamNum As UInt16, _
         <MarshalAs(UnmanagedType.LPWStr)> ByVal pwszName As String, _
         ByRef pcchNameLen As UInt16, _
         ByRef pType As WMT_ATTR_DATATYPE, _
         <MarshalAs(UnmanagedType.LPArray)> ByVal pValue As Byte(), _
         ByRef pcbLength As UInt16) As UInt32

   End Interface

   Public Enum WMT_ATTR_DATATYPE
      WMT_TYPE_DWORD = 0
      WMT_TYPE_STRING = 1
      WMT_TYPE_BINARY = 2
      WMT_TYPE_BOOL = 3
      WMT_TYPE_QWORD = 4
      WMT_TYPE_WORD = 5
      WMT_TYPE_GUID = 6
   End Enum


End Class



Public Class WMValidator

   Private m_AttributeDump As ArrayList

Public ReadOnly Property AttributeDump() As ArrayList Get If m_AttributeDump Is Nothing Then m_AttributeDump = New ArrayList End If Return m_AttributeDump End Get End Property Public Function ReadAttributes(ByVal FilePath As String, _ optional ByVal StreamNum As Integer = 0) As Boolean Dim MetadataEditor As WMFFunctions.IWMMetadataEditor Dim HeaderInfo3 As WMFFunctions.IWMHeaderInfo3 Dim wAttributeCount As UInt16 m_AttributeDump = New ArrayList Try WMFFunctions.WMCreateEditor(MetadataEditor) MetadataEditor.Open(FilePath) HeaderInfo3 = MetadataEditor HeaderInfo3.GetAttributeCount(Convert.ToUInt16(StreamNum), wAttributeCount) For wAttribIndex As Integer = 0 To Convert.ToInt16(wAttributeCount) - 1 Dim wAttribType As WMFFunctions.WMT_ATTR_DATATYPE Dim pwszAttribName As String = Nothing Dim pbAttribValue As Byte() = Nothing Dim wAttribNameLen As UInt16 Dim wAttribValueLen As UInt16 'get the attribute "filler" information HeaderInfo3.GetAttributeByIndex(Convert.ToUInt16(wAttribIndex), _ Convert.ToUInt16(StreamNum), _ pwszAttribName, _ wAttribNameLen, _ wAttribType, _ pbAttribValue, _ wAttribValueLen) Dim x As Byte() ReDim x(Convert.ToInt32(wAttribValueLen)) pbAttribValue = x pwszAttribName = New String("0", Convert.ToInt32(wAttribNameLen)) 'now get the actual values HeaderInfo3.GetAttributeByIndex(Convert.ToUInt16(wAttribIndex), _ Convert.ToUInt16(StreamNum), _ pwszAttribName, _ Convert.ToUInt16(wAttribNameLen), _ wAttribType, _ pbAttribValue, _ Convert.ToUInt16(wAttribValueLen)) Dim MyVal As String = "" 'we'll convert all to strings Select Case wAttribType Case WMFFunctions.WMT_ATTR_DATATYPE.WMT_TYPE_STRING MyVal = bytearraytostring(pbAttribValue) Case WMFFunctions.WMT_ATTR_DATATYPE.WMT_TYPE_BOOL If BitConverter.ToBoolean(pbAttribValue, 0) Then MyVal = "True" Else MyVal = "False" End If Case WMFFunctions.WMT_ATTR_DATATYPE.WMT_TYPE_DWORD Dim dwValue As UInt32 = BitConverter.ToUInt32(pbAttribValue, 0) MyVal = dwValue.ToString() Case WMFFunctions.WMT_ATTR_DATATYPE.WMT_TYPE_QWORD Dim qwValue As UInt64 = BitConverter.ToUInt64(pbAttribValue, 0) MyVal = qwValue.ToString Case WMFFunctions.WMT_ATTR_DATATYPE.WMT_TYPE_BINARY MyVal = "BIN: " & BitConverter.ToString(pbAttribValue, 0) Case Else MyVal = "unsupported type" 'ok for me End Select Dim Attributename As String = _ pwszAttribName.Substring(0, _ pwszAttribName.Length - 1) m_AttributeDump.Add(New AttributeKeyValue(Attributename, MyVal)) Next Return True Catch ex As System.Runtime.InteropServices.COMException Throw ex Return False Catch ex As Exception Throw ex Return False Finally If Not MetadataEditor Is Nothing Then MetadataEditor.Close() End If End Try End Function Private Function bytearraytostring(ByRef barray() As Byte) As String Dim enc As System.Text.UnicodeEncoding = New System.Text.UnicodeEncoding Return enc.GetString(barray, 0, UBound(barray) - 1) End Function Public Shared Function IsWMFile(ByVal FilePath As String) As Boolean 'checks file header value of "0&²uŽfÏ" 'could use crypto namespace and compare hash 'but that would do also 15 iterations and 'require the extra library import 'so this way is fine Dim bFileExists As Boolean = False Dim f As IO.File bFileExists = f.Exists(FilePath) f = Nothing If Not bFileExists Then Dim ex As New IO.FileNotFoundException Throw ex f = Nothing Else Dim fs As New IO.FileStream(FilePath, IO.FileMode.Open) Dim arMagic() As Byte = _ {48, 38, 178, 117, 142, 102, 207, 17, 166, 217, 0, 170, 0, 98, 206, 108} Dim arCheck(arMagic.Length - 1) As Byte fs.Read(arCheck, 0, arMagic.Length) fs.Close() fs = Nothing Dim bValuesEqual As Boolean = True For i As Integer = 0 To arMagic.Length - 1 If arCheck(i) <> arMagic(i) Then bValuesEqual = False Exit For End If Next If bValuesEqual Then Return True Else Return False End If End If End Function Shared Function IsMBRFile(ByVal FilePath As String) As Boolean 'this is called after a check of IsWMV from the file header 'if the file has more than 3 streams then it is an MBR Dim MetadataEditor As WMFFunctions.IWMMetadataEditor Dim HeaderInfo3 As WMFFunctions.IWMHeaderInfo3 Dim wAttributeCount As UInt16 WMFFunctions.WMCreateEditor(MetadataEditor) MetadataEditor.Open(FilePath) HeaderInfo3 = MetadataEditor Try 'this is where we're looking for a failure HeaderInfo3.GetAttributeCount(Convert.ToUInt16(3), wAttributeCount) Return True Catch ex As System.ArgumentException 'this is the one Return False Finally If Not HeaderInfo3 Is Nothing Then HeaderInfo3 = Nothing End If If Not MetadataEditor Is Nothing Then MetadataEditor.Close() End If End Try End Function End Class Public Class AttributeKeyValue Private m_Key As String Private m_Value As String Public ReadOnly Property Key() As String Get Return m_Key End Get End Property Public ReadOnly Property Value() As String Get Return m_Value End Get End Property Public Sub New(ByVal Key As String, ByVal Value As String) m_Key = Key.Trim m_Value = Value.Trim End Sub End Class

 

The thing about the code above is that it does not return *all* of the stock attributes. If you run the above (or it's c# counterpart ShowAttributes in the WMFormat SDK) against the sample wma file on your XP box "C:\Documents and Settings\All Users\Documents\My Music\Sample Music\Beethoven's Symphony No. 9 (Scherzo).wma" you will get a count of 33 attributes. However, if you run the c# sample ShowAttributes3 which uses GetAttributeCountEx you will get a count of 35 attributes. Note: in both cases you want to use zero for the streamnum so that you get the details for the file, not for a specific stream.

The documentation for GetAttributeCountEx in the WMFormat 9.5 sdk states that this function is to be used instead of the older GetAttributeCount function, and now you can see why.

A few weeks after originally creating the server code, we opened it again intending to quickly follow that advice... and couldn't get GetAtrtributeCountEx to work with VB.Net . Again using simple C dll function style, we added the function to the interface section, right under the existing working functions, which was very simple because it appears to have the exact same signature as GetAttributeCount. When we called it though, we got the "Invalid Pointer" error. That seemed odd since the other functions were working and they used pointers. While pointers are not fully supported by the CLS compliant VB.Net 1x, it doesn't have the same No-Pointer rule as VBClassic (which could be gotten around in some cases using the unsupported VarPtr, StrPtr and ObjPtr)

The 9.5 SDK docs don't specifically show the GetAttributeCount function, but we were in the 9.0 days when the help file did list it and clearly stated that the pcAttributes argument in both GetAttributeCount and GetAttributeCountEx are both pointers to a WORD (Uint16/ushort). Granted the basic function was an Out pointer and the Ex was in In,Out but again, the Indexes function used In,Outs for byte arrays without problem. So why was the Ex version failing?

Because of the "Invalid Pointer" error, our synapses aligned to the kneejerk logic that it might have to do with the combination of the unsigned int16 and an In,Out. Because VB.Net 1x is/was CLS-compliant, it doesn't natively work the unsigned integers, you can create them using Convert.ToUIntxxx but prior to VB2005 VB didn't have all of the UInt functionality that c# was given (given because of that language's main target of C++ folks who would be upset without the traditional type even though it does break .Net rules). This quick logic embedded itself in our minds and became the foundation for a downward spiral of frustration later in the project. At this point, because the basic functions were reading all of the attributes we cared about, we put it on the back burner and moved on to the end-user client app development.

The client app came together mostly without problem, using the WMFormat widget we got the capture/edit/playback functionality wrapped up in just days and the db-hitting web services and ftp code connecting to our SSL protected domain synched right up. As is too typical, the most time was spent being sidetracked with re-re-reworking the GUI as the designers and corporate managers waged war over colors and button shapes. And then the floor dropped out, SetAttibute would not Set the custom Attributes. As with GetAttributeCountEx we were getting an Invalid Pointer exception and so while colors were argued about on the East Coast, we quietly focused on figuring out what the problem was in our c# to VB.Net port. FYI: At the time we chose SetAttribute rather than AddAttribute/ModifyAttribute because Set adds a named atrribute if it is not found in the files and modify it if it is found, killing two birds with one stone and with fewer arguments (note that Set, while still working in v10 with all simple types is now deemed 'depreciated' and Add/Modify are now the advised functions to use).

Telling ourselves that a fresh approach would nail it, we started nearly from scratch, We made a new c# project without Unsafe switches, copied the sdk dll function file into it and created a harness that called both of the Count and the SetAttribute functions. And they worked, just like in the sdk sample.

Then, (here's the third human error, dovetailing on the original) we copied our working VB.Net -ported functions to a new VB.Net project, added the CountEx and SetAttribute functions and created a harness to test them. Again, the first two functions ran fine but CountEx and Set both failed.

As a quick check of the UInt16 pointers, we opened the code in an early build of VB2005 which does natively work with unsigned integers and it also failed (throwing a NullReferenceException which either was a bug in this very early version or meant that we had a null pointer)

And all of a sudden it hit us in the face. We were implementing an interface in a way that we wouldn't dream of doing with a VBClassic interface or a VB.Net managed object interface.

Remember the words "In C dll style"? We were only partially implementing. If you tried to do this with VB6\COM or VB.Net \.Net the compiles would fail because the IDE would be checking those interfaces in the background and forcing you to get every function, if only as a stub. But c# and VB.Net don't go out of the way to verify the interfaces of COM dlls specified in the manner above, the Unsafe switch isn't required but you don't get the automatic assistance either. Our original code working was a false-positive based on the luck that the functions we used just happened to be the first two of the 21 that made up the whole interface.

All it took to be able to use those other functions was to add the rest of the functions, including all of the ones that weren't going to be used. In addition they had to be added in the correct order as shown in the SDK samples.

Things happen in the rush of a startup, minds get sidetracked and stupid mistakes are easily made. It's human. Luckily, it's just those experiences, especially the ones that drive you most crazy, that are not forgotten and they add up to making you worth more for your later clients.

To get the tiny demo that uses the adapted VB.Net port of the c# sample, click here. After you get the idea of the uses, just send us an email with a valid return address that can accept zips and we'll be happy to send you the full source code.

Requirements: .Net1.1 runtimes. Microsoft Media Player 10 is recommended but the MediaFormat SDK redistributable wmfdist.exe should take care of WMP9 user machines (not having WMP10 or wmfdist.exe will still give you the smaller attribute count on WMP9 boxes). For deployable apps, the MediaFormat redistributable from the sdk should be shelled from your installation scripts.

We hope you get some miles out of both the code and the story of its' creation.

Robert Smith
Kirkland, WA

October 2004 update: While this reads and writes the simple datatypes (strings, bools, ints) it doesn't do the extended attributes such as WM\Lyrics_Synhronised and Images. A good discussion started about this on October 4th in the microsoft.public.windowsmedia.sdk newsgroup and ended up with c# and VB.Net code that rose to the occassion. Use your Google Groups search to get that thread, using the questioner, the answerer and the thread subject: "Vincas", "Allesandro" and "About obscurity of some extended attributes structures"

 

added to smithvoice September 2004

 


Print  

pagecomment
  Add Comment



Submit Comment
  View Ratings
50.00%0
40.00%0
30.00%0
20.00%0
10.00%0

Number of Comments 0 , Average of Ratings
  View Comments
No comment.


Privacy Statement  |  Terms Of Use
Copyright 2008 by Robert C. Smith