smithvoice.com
 Y'herd thisun? 

“Thus far, radiation has not been a major problem for crews on the shuttle, whose maximum stay [prior to EDO - smith] has not exceeded 10 days. Skylab crews did not suffer ill effects from radiation even on the longest voyage of 84 days. And cosmonauts who have been in space for a full year have not shown evidence of damage from radiation. The radiation dosage received by astronauts (and cosmonauts) is routinely monitored”

from Space Commerce, Materials Processing by Secretary of the Air Force, Dr. John L. McLucas

Windows Media Attributes

TaggedCoding, VB, MediaData

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"

 

 



home     who is smith    contact smith     rss feed π
Since 1997 a place for my stuff, and it if helps you too then all the better smithvoice.com