vb.net 2010
Windows Pro 7

Create a Video Library with an Embedded Windows Media Player

I have close to 400 video clips in my home movie library. In order to quickly locate any given video I have added certain words into the file names. Even so, locating specific files is clumsy from the Explorer window and playing a clip requires starting an external application and flipping back and forth between Explorer and the media player. That's why I decided to embed the media player directly in the application.

Given a choice, I would have preferred to embed vlc Media Player, however, extensive searching on a procedure to do this proved fruitless. I followed the youtube videos to the letter and got only errors when trying to add the vlc control to my form. As such I was reduced to using Windows Media Player (which supports fewer formats).

I am going to present my application in four parts

  1. Introduction to Alternate Data Streams
  2. Comment text interface code
  3. Embedding Windows Media Player
  4. Adding the functionality

You can only store so much information in a file name before it becomes unwieldy. A better approach would be to store a few useful tags such as date/time, location and names in the file name, then store more descriptive text in a file comment. While Windows provides the capability of adding comments to files, this is available for only certain file types, and the comments cannot be manipulated via code. This is where Alternate Data Streams come in handy.

1. Introduction to Alternate Data Streams

If you've ever run a downloaded program and seen the dialog box titled Open File - Security Warning and the question Do you want to run this file? then you've come across an NTFS Alternate Data Stream (henceforth ADS). The downloaded file may have a name like

newApp-setup.exe

but hidden in the directory entry for that file is an ADS named

newApp-setup.exe:Zone.Identifier

This is a sort of hidden file which can contain anything that a regular file contains. A file can contain any number of alternate data streams as long as each has a unique name. The contents can be text, jpg or avi data, or even executable code. In this case it is just text and if you do

cat < newApp-setup.exe:Zone.Identifier

you'll see

[ZoneTransfer]
ZoneId=3

It is the presence of this ADS with the given text that results in the warning. I am going to create an ADS named :comment and use it to store a comment for each file. The bad news is that you cannot manipulate ADS using native vb.net I/O calls. The good news is that it is easily done using the Scripting.FileSystemObject. More on this in part 2.

2. Comment text interface code

Because I use the comment ADS in other applications I created a separate module containing the interface code. Let's put a little code at the start of the module

Const ADSPART As String = ":comment"

Private fso As New Scripting.FileSystemObject
Declare Function DeleteFile Lib "kernel32" Alias "DeleteFileA" (ByVal filename As String) As Long

ADSPART just defines the name of the ADS part of the file name.

fso is the Scripting.FileSystemObject. In order to use this you will have to add a COM reference to Microsoft Scripting Runtime.

The Kernel32 function DeleteFile is required to delete an existing ADS as for some strange reason this cannot be done via the FileScriptingObject which is capable of manipulating other ADS aspects..

The first function we are going to add will determine if a file has a comment ADS.

''' <summary>
''' Returns true if the given file has a non-blank comment, False otherwise.
''' </summary>
''' <param name="filename">Fully qualified name of file to check</param>
''' <returns>True if a comment exists, false otherwise</returns>
''' <remarks></remarks>
Public Function HasComment(filename As String) As Boolean
    Return Trim(GetComment(filename)) <> ""
End Function

In this case, if an ADS is present but empty I also return False. The next function retrieves a comment from a file.

''' <summary>
''' Returns the comment (if it exists) from the given file.
''' </summary>
''' <param name="filename">Fully qualified name of file</param>
''' <returns>The comment</returns>
''' <remarks></remarks>
Public Function GetComment(filename As String) As String

    'Returns the comment for the given file, or the null string if not present.
    'If there is a null ADS then the ADS is removed.

    Dim ads As String = filename & ADSPART
    Dim comment As String = ""

    If fso.FileExists(ads) Then
        Dim tso As Scripting.TextStream = fso.OpenTextFile(ads)
        If Not tso.AtEndOfStream Then
            comment = tso.ReadAll
            tso.Close()
        Else
            DeleteComment(filename)
        End If
    End If

    Return comment

End Function

The first step is to create the name of the ADS by appending ":comment" to the file name. If the ADS exists and can be read then we return the text. If there is a problem reading it then I just delete it because it's of no use if it's not readable.

The next function sets a file comment. Because I have already decided that a null comment is useless, if the comment text is null then I delete any existing comment ADS.

''' <summary>
''' Sets the comment for a file
''' </summary>
''' <param name="filename">Fully qualified name of file</param>
''' <param name="comment">Comment text (null to delete comment)</param>
''' <remarks></remarks>
Public Sub SetComment(filename As String, comment As String)

    Dim ads As String = filename & ADSPART
    If Not fso.FileExists(filename) Then Exit Sub

    If comment = "" Then
        DeleteComment(filename)
    Else
        Dim tso As Scripting.TextStream = fso.OpenTextFile(ads, Scripting.IOMode.ForWriting, True)
        tso.Write(Trim(comment))
        tso.Close()
    End If

End Sub

Lastly, let's have a function to delete any existing comment ADS.

''' <summary>
''' Delete a file comment
''' </summary>
''' <param name="filename">Fully qualified name of file</param>
''' <remarks></remarks>
Public Sub DeleteComment(filename As String)

    Dim ads As String = filename & ADSPART
    If Not fso.FileExists(ads) Then Exit Sub

    DeleteFile(ads)

End Sub

Aside from some header comments and the Module and End Module statements, the module is complete.

3. Embedding Windows Media Player

To add a Windows Media Player control to your form you must do the following:

  1. Right click on the vb.net toolbox
  2. Select Choose Items... from the context menu
  3. Select the Com Components tab
  4. Select Windows Media Player
  5. Click OK

You will see Windows Media Player added to the Common Controls section of the toolbox. You can drag and drop the control onto your form just like any other control. But before you do that, let's create some other controls first.

Start a new project and size the new form to 800, 600. Add a SplitContainer.

Add a listbox control named lbxFiles to the left panel and set the Dock property to Fill.

Add a Panel control to the right panel and set the Dock property to Top. Set the height of the panel to 32.

Add two button controls to the left side of the panel and name them btnAll and btnAny. Set the button widths to 33 and set the button text to All and Any.

Add a textbox control named txtWords to the right of btnAny and let it fill the remaining width of the panel. Anchor it to Top, Left & Right.

Add a textbox control named txtComment to the bottom of the right panel. Make it MultiLine and Dock it to Bottom. Set the height to around 87 (actual height may vary depending on the font).

Now you can drag and drop a Windows Media Player control named wmp to the middle of the right panel and set Dock to Fill.

Your form should now look like

6d76a408cee74895abe5b260a53327bc

The way to play a video in the control is

  1. tell the control the fully qualified file name of the media file
  2. (optional) tell the player to fill the window

    wmp.URL = SomeFileName
    wmp.stretchToFit = True

The video starts to play automatically when loaded. By setting the stretchToFit property, resizing the window will automatically resize the video as well. As a bonus, you can toggle the video between windowed and full screen view by double clicking. No extra coding is required.

4. Adding the functionality

Now lets add the rest of the bells and whistles. To make the typing a little easier lets add

Imports WMPLib
Imports AxWMPLib
Imports System.IO.Path

Also, let's add a few globals.

Public Version As String = "Version 2.0.0"

'default path for videos
Private Root As String = "E:\Home Movies"
Private fldBrowser As New FolderBrowserDialog

'array of valid video files for Windows Media Player
Private FileMask() As String = "*.avi *.mp4 *.mkv *.divx *.mpg *.mov *.mp4 *.mpeg".Split

'key = unqualified file name    
'val = fully qualified file name
Private FileList As New Dictionary(Of String, String)

'key = unqualified file name    
'val = comment
Private Comments As New Dictionary(Of String, String)

'set this to true during file renaming
Private Renaming As Boolean = False

'set true to play video on selection
Private AutoPlay As Boolean = True

Private CurrFull As String = ""     'current fully qualified file name  
Private CurrFile As String = ""     'current unqualified file name      

FileList will contain the unqualified and fully qualified file names of all media files in the current folder (defined by Root).

Comments will contain the unqualified file names and the comment (if any) associated with each file.

CurrFull and CurrFile could just as easily be retrieved from lbxFiles and FileListbut I prefer a simpler way to refer to them rather than lbxFiles.SelectedItem.blah.blah.blah.

I also like to bring the application up in (more or less) the state that I left it so I define a few Settings variables.

LastRoot - the last used media folder

LastLocn - the last screen location of this app

LastSize - the last size of the app window

LastSplit - the last size of the split container left panel

AutoPlay - the last state of the autoplay option

The main form is named frmMain and has the following properties set

KeyPreview = True
Text = "Home Movie Viewer"

Event handlers for frmMain are

Private Sub frmMain_Load(sender As Object, e As System.EventArgs) Handles Me.Load

    'load last used settings

    With My.Settings
        Me.Location = .LastLocn
        Me.Size = .LastSize
        Me.Root = .LastRoot
        Me.AutoPlay = .AutoPlay
        SplitContainer1.SplitterDistance = .LastSplit
    End With

    'set folder browser dialog defaults

    If My.Computer.FileSystem.DirectoryExists(Root) Then
        fldBrowser.SelectedPath = Root
    Else
        fldBrowser.SelectedPath = "C:\"
    End If

    fldBrowser.ShowNewFolderButton = False

    ReadFiles()                 'get a list of all media files  
    ClearComment()              'clear the displayed comment    
    btnAll.PerformClick()       'display the current file list  

End Sub

Private Sub frmMain_FormClosing(sender As Object, e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing

    'save current settings for next session

    With My.Settings
        .LastLocn = Me.Location
        .LastSize = Me.Size
        .LastRoot = Me.Root
        .AutoPlay = Me.AutoPlay
        .LastSplit = SplitContainer1.SplitterDistance
    End With

    SaveComment()               'save any modified comments     
    BackupComments()            'save all comments to text file 

End Sub

SaveComment will save any changes that have been made to the comment for the current media file.

BackupComments saves all comments for the current media folder in a file named ".comments.txt" in the current folder. Alternate Data Streams are not supported on other file systems, and some copy programs do not copy ADS even between NTFS volumes so I like to keep a copy of the comments in a plain text file (I have a general purpose commenting app that makes use of this backup). Feel free to remove this call if you like.

I make frequent use of hotkeys so let's add a handler for those

Private Sub frmMain_KeyPress(sender As System.Object, e As System.Windows.Forms.KeyPressEventArgs) Handles MyBase.KeyPress

    'Process global hotkeys

    Select Case Asc(e.KeyChar)

        Case 1      'ctrl-a - select all if comment has focus otherwise toggle AutoPlay

            If txtComment.Focused Then
                txtComment.Select(0, 99999)
            Else
                AutoPlay = Not AutoPlay
                Me.Text = "AutoPlay is " & IIf(AutoPlay, "ON", "OFF")
            End If

            e.Handled = True

        Case 6      'ctrl-f select new media folder

            'ask the user to select a new media folder

            If fldBrowser.ShowDialog = Windows.Forms.DialogResult.OK Then

                SaveComment()       'save any changed comments      
                BackupComments()    'backup comments for this folder
                ClearComment()      'clear the displayed comment    

                Root = fldBrowser.SelectedPath
                txtWords.Text = ""
                ReadFiles()
                btnAll.PerformClick()

            End If

            e.Handled = True

        Case 18     'ctrl-r rename the currently selected file

            If lbxFiles.SelectedIndex < 0 Then Exit Sub

            Dim oldbase As String = GetFileNameWithoutExtension(CurrFile)
            Dim oldextn As String = GetExtension(CurrFile)
            Dim newname As String = Trim(InputBox("Enter new name", "Rename File", oldbase))

            'Copy the new name to the clipboard. That way if the user enters
            'a name that is not valid he/she can just paste the invalid name
            'back in and modify it instead of starting from scratch.        

            If newname <> "" Then
                My.Computer.Clipboard.SetText(newname)
                If My.Computer.FileSystem.FileExists(Combine(Root, newname & oldextn)) Then
                    MsgBox("There is already a file with that name", vbOKOnly)
                Else
                    RenameFile(CurrFull, newname & oldextn.ToLower)
                End If
            End If

            e.Handled = True

    End Select

End Sub

Before you can play a file you have to see what files are available. Let's add the code to read the file list.

''' <summary>
''' Read all media files in the current folder
''' </summary>
''' <remarks></remarks>
Private Sub ReadFiles()

    SaveComment()
    ClearComment()

    'clear the filelist and comments dictionaries for rebuilding

    FileList.Clear()
    Comments.Clear()

    CurrFile = ""
    CurrFull = ""

    'If a video file does not have a comment then one will be added consisting  
    'of the file name without the extension. That means we will only have to    
    'search comment text rather than comment text and file names.               

    For Each fullname In My.Computer.FileSystem.GetFiles(Root, FileIO.SearchOption.SearchTopLevelOnly, FileMask)

        Dim filename As String = System.IO.Path.GetFileName(fullname)
        Dim basename As String = System.IO.Path.GetFileNameWithoutExtension(filename)

        FileList.Add(filename, fullname)

        If HasComment(fullname) Then
            Comments.Add(filename, GetComment(fullname))
        Else
            Comments.Add(filename, basename & vbCrLf & vbCrLf)
            SetComment(fullname, basename & vbCrLf & vbCrLf)
        End If

    Next

End Sub

When the user selects a new file it will begin playing immediately unless explicitly stopped.

Private Sub lbxFiles_SelectedIndexChanged(sender As System.Object, e As System.EventArgs) Handles lbxFiles.SelectedIndexChanged

    'User has selected a new file (ignore during rename operation)

    If Renaming Then Exit Sub

    Dim lbx As ListBox = sender
    If lbx.SelectedIndex = -1 Then Exit Sub

    SaveComment()

    'Display the file name in the title bar and begin playback

    CurrFile = lbx.SelectedItem
    CurrFull = FileList(CurrFile)

    If Comments.ContainsKey(CurrFile) Then
        txtComment.Text = Comments(CurrFile)
    Else
        txtComment.Text = ""
    End If

    txtComment.Tag = txtComment.Text

    Me.Text = CurrFull

    'play the current file unless autoplay is disabled.

    wmp.URL = CurrFull
    If Not AutoPlay Then wmp.Ctlcontrols.stop()
    wmp.stretchToFit = True

End Sub

Now we can add the filtering capability. We'll add some flexibility here. I boiled down the options to two basics, All and Any. The user will type words into a textbox and click All to display only those files containing all of the words, or Any to display the files containing any of the words. I wanted a little more flexibility so instead of a strict word match I made it a "starts with" match. That way I could enter Adam and get videos containing Adam at the zoo as well as Adam's first birthday. A strict word match would not have matched the second file. Also, for a little extra I decided that if you hold the control key while clicking it would do the matching by string rather than by word (ear would match bearing, for example).

Private Sub btnAll_Click(sender As System.Object, e As System.EventArgs) Handles btnAll.Click

    'Show only files with comments containing all of the given strings

    wmp.Ctlcontrols.stop()
    ClearComment()

    Dim mode As String = IIf(My.Computer.Keyboard.CtrlKeyDown, "STRING", "WORD")

    Dim words() As String = txtWords.Text.Split()
    lbxFiles.Items.Clear()

    For Each file As String In Comments.Keys

        Dim all As Boolean = True

        For Each word As String In words
            If Not FoundIn(Comments(file), word, mode) Then
                all = False
                Exit For
            End If
        Next

        If all Then
            lbxFiles.Items.Add(file)
        End If

    Next

    Me.Text = lbxFiles.Items.Count & " matching files in " & Root

End Sub

Private Sub btnAny_Click(sender As System.Object, e As System.EventArgs) Handles btnAny.Click

    'Show only files with comments containing any of the given words.

    wmp.Ctlcontrols.stop()
    ClearComment()

    Dim mode As String = IIf(My.Computer.Keyboard.CtrlKeyDown, "STRING", "WORD")

    Dim words() As String = LCase(txtWords.Text).Split()
    lbxFiles.Items.Clear()

    For Each file As String In Comments.Keys

        For Each word As String In words
            If FoundIn(Comments(file), word, mode) Then
                lbxFiles.Items.Add(file)
                Exit For
            End If
        Next

    Next

    Me.Text = lbxFiles.Items.Count & " matching files in " & Root

End Sub

''' <summary>
''' Look for occurrences of a word in the given text
''' </summary>
''' <param name="text">A text string to search</param>
''' <param name="word">The word to search for</param>
''' <param name="mode">Match type = "STRING" or "WORD"</param>
''' <returns>True if a match was found, False otherwise</returns>
''' <remarks>Search is case insensitive</remarks>
Private Function FoundIn(text As String, word As String, mode As String) As Boolean

    'Return true only if the given text contains the given word.

    Select Case mode

        Case "WORD"   'match if any word in text starts with the given word

            'look for word in wordlist from file tags

            For Each tag As String In text.Split()
                If tag.StartsWith(word, StringComparison.OrdinalIgnoreCase) Then Return True
            Next

            Return False

        Case "STRING"     'match if the given word is found anywhere in text

            'look for word anywhere in filename string

            Return InStr(text, word, CompareMethod.Text) > 0

        Case Else

            Return False

    End Select

End Function

And because I am allowing some tags in the file names it makes sense to allow the user to rename a file (CTRL-R in the hotkeys).

''' <summary>
''' Rename the current file
''' </summary>
''' <param name="oldname">Fully qualified old file name</param>
''' <param name="newname">Unqualified new file name</param>
''' <remarks></remarks>
Private Sub RenameFile(oldname As String, newname As String)

    'Rename the currently selected file. We want to set the global Renaming     
    'flag because during the renaming process the lbxFiles.SelectedIndexChanged 
    'event will be triggered and we want to ignore these events unless triggered
    'explicitly by the user selecting a file.                                   

    Renaming = True

    Try

        My.Computer.FileSystem.RenameFile(oldname, newname)

        'update FileList and Comments dictionaries to reflect new file name

        FileList.Remove(CurrFile)
        FileList.Add(newname, oldname)

        Comments.Add(newname, Comments(CurrFile))
        Comments.Remove(CurrFile)

        'update displayed file list

        lbxFiles.Items(lbxFiles.SelectedIndex) = newname
        lbxFiles.SelectedIndex = lbxFiles.FindString(newname)

        CurrFile = newname
        CurrFull = Combine(Root, newname)

        Me.Text = CurrFull

    Catch ex As Exception
        MsgBox(ex.Message, vbOKOnly, "Could not rename file")
    End Try

    Renaming = False

End Sub

Now we can add the few remaining housekeeping routines.

Private Sub ClearComment()

    'clear the current comment display

    txtComment.Text = ""
    txtComment.Tag = ""

End Sub

Private Sub SaveComment()

    'save the current comment if changed by user

    If CurrFull <> "" And txtComment.Text <> txtComment.Tag Then
        SetComment(CurrFull, txtComment.Text)
        txtComment.Tag = txtComment.Text
        Comments(CurrFile) = txtComment.Text
    End If

End Sub

''' <summary>
''' Copies all file comments to .comments.txt
''' </summary>
''' <remarks></remarks>
Private Sub BackupComments()

    Dim buffer As New System.Text.StringBuilder

    'We could just write the comments for the video files but that may overwrite comments
    'in the .comments.txt file for files of other types.

    For Each file In My.Computer.FileSystem.GetFiles(Root, FileIO.SearchOption.SearchTopLevelOnly)
        If HasComment(file) Then
            Dim comment As String = GetComment(file).Replace(vbCrLf, "\n").Replace(vbTab, "\t")
            Dim line As String = GetFileName(file) & "*" & comment
            buffer.Append(line & vbCrLf)
        End If
    Next

    System.IO.File.WriteAllText(Combine(Root, ".comments.txt"), buffer.ToString)

End Sub

That's the entire application. Strictly speaking, to embed Windows Media Player, all you really need is to add the control to the toolbox (section 3), include

Imports WMPLib
Imports AxWMPLib

and add the line

wmp.URL = SomeFileName

But where would be the fun in stopping there? I've attached the zipped project folder. It contains a little extra code such as a help window and a handler so that pressing ENTER in the txtWords control automatically clicks All.

As always, constructive comments are appreciated.

Edited 2 Years Ago by Dani: Changed to tutorial

Comments
Great!

Computer languages in which I have developed applications

Assembler (DEC, Data General, 8080, GE, SEL, IBM 360)
WATFOR (Waterloo FORTRAN)
FORTRAN (SEL)
APL (IBM 360, IBM VSAPL)
PL/1
C/C++
Borland Paradox
VB.net
vbScript

Very interesting. As I use C# most of the time, did not find much info on the net about a fileSystemObject. Can it only be used in a scripting context or could I give System.IO, FileInfo and DirectoryInfo a try?

You can get the Microsoft doc for the scripting runtime here. I would imagine that because C# and vb.net use the same core libraries that the native C# functions would also not be able to manage alternate data streams. I've written hundreds of full fledged apps in vbScript over the years, mostly for automation of infrastructure (move files hither and yon, populate databases, monitoring, etc) and most of them are still in service more than 10 years later. vbScript is perfect for these types of apps because

  1. the development environment is so easy to set up (notepad/textpad)
  2. you always know what version is being run (no compiling)
  3. it's easy to patch at 3:00 am on a callout

Edited 2 Years Ago by Reverend Jim

For something like this you might want to look at indexing the words for efficient searching. I didn't notice if you've assigned each file a unique id but this will work with collection indexes as well. A Dictionary(Of String, List(Of Integer) is a simple structure to hold which videos contain each word. Finding a particular phrase is simply a matter of finding the videos that contain all the words in the phrase. Every video you add you add the words and/or video index to your indexed collection. An .xml file would work well to store the indexed collection. I'm pretty sure I have some code for this if you'd like.

Edited 2 Years Ago by tinstaafl

Considering that even with 400 videos, entering three search terms and clicking Any brings up a list of the matching files (essentially) instantly I think that there would be very little to gain for the extra effort and complexity. I wrote a variant of this program to use with my photo library. The difference is that I have 9000 photos and they are not all in one folder. The same test on the photos (with 2500 matches) brings up the list in under a second.

Also, my xml skills are essentially non-existent ^_^

But the idea has merit, especially for large collections. I have ACDSee Pro 3 which supports categories. The problem is that you have to manually create the categories, and the indexes are stored in a database. My implementation allows ad hoc categories (and searches) and does not require a database.

But thanks for the suggestion.

The article starter has earned a lot of community kudos, and such articles offer a bounty for quality replies.