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