Tuesday, November 8, 2011

Video Gallery in Asp.Net


Screen shot of Gallery Server Pro

Introduction

Gallery Server Pro is a powerful and easy-to-use ASP.NET web application that lets you share and manage photos, video, audio, and other files over the web. The entire application is contained in a single ASCX user control for easy insertion into your own web sites.
  • Stable, production ready
  • Scalable to hundreds of thousands of objects
  • Use any web browser to organize your media files into albums you can easily add, edit, delete, rotate, rearrange, copy and move
  • Easily add files using one-click synchronize and ZIP file upload functions. Thumbnail and compressed versions are automatically created
  • Add links to external media objects such as YouTube videos
  • Powerful user security with flexible, per-album granularity
  • Available as a native DotNetNuke module
  • Metadata extraction
  • Search by title, caption, filename, and metadata
  • Image watermarking with your own text and/or image
  • AJAX-enabled for more responsive UI
  • Web-based installer makes installation painless
  • Your choice of database: Zero-configuration, mostly ACID-compliant SQLite database or Microsoft SQL Server for mission critical applications
  • Silverlight and Flash used to play compatible video and audio files
  • Uses ASP.NET Membership provider so you can integrate with your existing accounts, including Active Directory
  • Data access uses the ASP.NET Provider model, which allows other data stores such as MySQL, Microsoft Access, or Oracle to be used in addition to SQL Server
  • 100% managed code written in C# and ASP.NET 2.0
  • Source code is released under the open source GNU General Public License
  • All web pages target XHTML 1.0 Strict and CSS 2.1 standards to ensure maximum forward compatibility

Background

This project started in 2002 from my desire to share my photos over the web. I wanted my photos to remain on my own server, not somebody else's like Flicker or Shutterfly. Since there weren't any free solutions to choose from at the time, I wrote my own.
Version 1 was released to the world in January 2006. Since that time, there have been hundreds of thousands of downloads and a steady stream of releases. At the time of this writing, the latest version is 2.4.7. Checkgalleryserverpro.com for the latest version.
In this article, I present the overall architecture and major features of Gallery Server Pro. The topics presented here can help if you want to learn more about:
  • Implementing a web gallery to share photos, video, audio, and other documents
  • Using ASP.NET Membership, Roles and Profile API
  • Using the composite design pattern to manage infinitely hierarchical relationships. In this case, it is media objects and albums, but it also applies to employee/supervisor relationships, bill of materials, file/directory relationships, and other similar structured items.
  • When and how to use the strategy design pattern
  • Using the data provider model in ASP.NET
  • A flexible technique for rendering distinct HTML to a browser based on the type of browser and type of object being rendered
  • Extracting metadata from images using the .NET 2.0 technique and the new WPF classes in .NET 3.0+

Using Gallery Server Pro

Gallery Server Pro is a fully functional and stable web application ready for production use.
  1. Download the source code in this article and compile. Or download the compiled version.
  2. Use Internet Information Services (IIS) Manager to configure the directory as a web application.
  3. Use Windows Explorer to give the IIS application pool modify permission to the web application directory.
  4. Use a web browser to start the application. The install wizard will automatically start.
  5. Step through the install wizard. When finished, you have a working version of Gallery Server Pro.
Gallery Server Pro stores media objects such as photos, video, audio, and documents in albums. These files and albums are stored in a directory named gs\mediaobjects within the web application. (This can be changed to any location on the web server.) An album is really just a directory, so an album named Vacation Photos is stored as a similarly named directory.
There are two main techniques for adding media objects:
  1. Upload a ZIP file containing the media files. If the ZIP file contains directories, they are converted to albums.
  2. Copy your media files to the media objects directory, and then start a synchronization in Gallery Server Pro.
When adding a media object, the following steps occur:
  1. The file is saved to the media objects directory. (If adding a media object via the synchronization technique, then this step is already done.)
  2. A thumbnail image is created and saved to the hard drive.
  3. For images, a compressed, bandwidth-friendly version is created and metadata such as camera model and shutter speed is extracted.
  4. A record is added to the data store to represent this media object.
Media objects are streamed to the browser through an HTTP handler. Below you can see a photo and a video being displayed. If watermarking is enabled, the watermark is applied to the in-memory version of the image just before it is sent.
Screenshot - imageview.jpgScreenshot - videoview.jpg
If you click the View metadata toolbar item above a media object, a popup DIV window displays the image's metadata, as shown below:
Screenshot - metadata.jpg
By default, everyone can browse the media objects. However, you must log on to perform any action that modifies an album or media object. Authorization to modify data is configured by type of permission and the albums to which it applies. For example, you can set up user Soren to have edit permission to the album Soren's photos. Another userMargaret is given edit permission to the album Margaret's photos. Each user can administer his or her own album but cannot edit albums outside his or her domain.
To learn more about how to use Gallery Server Pro from an end-user perspective, read the Administrator's Guide. Otherwise, read on to learn about the architecture and programming techniques.

Solution Architecture

The Visual Studio solution in the download contains twelve projects. They are:
Project nameDescription
WebsiteUI layer - ASP.NET 2.0 web application
TIS.GSP.WebControlsContains custom web server controls used in the web app
TIS.GSP.BusinessBusiness layer logic
TIS.GSP.Business.ResourcesContains resource data to support the business layer
TIS.GSP.ProviderData provider. Defines the contract the data layer must comply with
TIS.GSP.Business.InterfacesDefines all interfaces used in the solution
TIS.GSP.ErrorHandlerProvides error handling support
TIS.GSP.ConfigurationProvides read/write access to custom configuration settings in web.config
TIS.GSP.Business.WpfProvides enhanced image metadata extraction through the use of new WPF classes available in .NET 3.0. Contained in a separate project and invoked through reflection in a way that degrades gracefully when .NET 3.0 is not present
TIS.GSP.Business.Wpf.ResourcesContains resource data to support the WPF project
TIS.GSP.Data.SQLiteSQLite data layer logic. Provides read/write access to data stored in SQLite
TIS.GSP.Data.SqlServerSQL Server data layer logic. Provides read/write access to data stored in SQL Server

User Management and Security

User accounts are managed through the ASP.NET Membership, Roles, and Profile APIs. By default, Gallery Server Pro is configured to use a locally stored SQLite database named galleryserver_data.sqlite in the App_Data directory. It interacts with this database by using SQLiteMembershipProvider for users, SQLiteRoleProviderfor roles, and SQLiteProfileProvider for profiles.
However, because of the flexibility offered by the provider model, you can use any data store that has a membership provider. For example, you can use SqlMembershipProvider to use SQL Server orActiveDirectoryMembershipProvider to plug Gallery Server Pro into your existing base of Active Directory users. The Administrator's Guide contains a section for Membership Configuration that provides more information.

Media Object, Albums and the Composite Pattern

Recall that each media object (photo, video, etc.) is stored in an album. Albums can be nested within other albums, with no restriction on the number of levels. This is similar to how files and directories are stored on a hard drive.
It turns out that albums and media objects have a lot in common. They both have properties such as IdTitle,DateAdded, and FullPhysicalPath; and they both have methods such as SaveDeleteCopyRemove, andValidateTitle. This is the ideal situation in which to use the "Composite" design pattern, where common functionality is defined in a base object. I start by defining two interfaces — IGalleryObject and IAlbum:
Screenshot - IGalleryObject_classdiagram.jpg
The IAlbum interface inherits from IGalleryObject and then adds a method and a few properties that are specific to albums. Then I create the abstract base class GalleryObject. It implements the IGalleryObject interface and provides default behavior that is common to albums and media objects. For example, here is the Title property:
public string Title
{
get
{
VerifyObjectIsInflated(this._title);
return this._title;
}
set
{
value = ValidateTitle(value);

this._hasChanges = (this._title == value ? _hasChanges : true);
this._title = value;
}
}
Now that the common functionality is defined in the abstract base class, I can create concrete classes to represent albums, images, video, audio, and other types of media objects:
With this approach, there is very little duplicate code, the structure is maintainable, and it is easy to work with. For example, when Gallery Server Pro wants to display the title and thumbnail image for all the objects in an album, there might be any combination of child albums, images, video, audio, and other documents. But I don't need to worry about all the different classes or about casting problems. All I need is the following code:
Screenshot - galleryobject_classdiagram.jpg
// Assume we are loading an album with ID=42

IAlbum album = Factory.LoadAlbumInstance(42, true);
foreach (IGalleryObject galleryObject in album.GetChildGalleryObjects())
{
string title = galleryObject.Title;
string thumbnailPath = galleryObject.Thumbnail.FileNamePhysicalPath;
}
Beautiful, isn't it? But what happens when the functionality is slightly different between two types of objects? For example, Gallery Server Pro needs to enforce a maximum length of 200 characters for an album title and 1000 characters for the title of a media object (image, video, etc). Both types of objects need a Title property, but the validation is different. Does that mean we have to remove the Title property from the base class and put it in the derived classes?
Not at all! Refer back to the property definition for Title in the code we looked at earlier. Notice that in the setter there is a call to ValidateTitle. Here is what ValidateTitle looks like in the GalleryObject class:
protected virtual string ValidateTitle(string title)
{
// Validate that the title is less than the maximum limit.
// Truncate it if necessary.

int maxLength =
GalleryServerPro.Configuration.ConfigManager.
GetGalleryServerProConfigSection().DataStore.MediaObjectTitleLength;

if (title.Length > maxLength)
{
title = title.Substring(0, maxLength).Trim();
}
return title;
}
The procedure is defined as virtual, allowing a derived class to override it if needed. In fact, that is exactly what theAlbum class does:
protected override string ValidateTitle(string title)
{
int maxLength =
GalleryServerPro.Configuration.ConfigManager.
GetGalleryServerProConfigSection().DataStore.AlbumTitleLength;

if (title.Length > maxLength)
{
title = title.Substring(0, maxLength).Trim();
}

return title;
}
The end result is that there is a base implementation in the base class that provides functionality for most cases, and code that is unique to albums is contained in the Album class. There isn't any duplicate code and the logic is nicely encapsulated. It is a thing of beauty to behold.

Using the Strategy Pattern for Persisting to the Data Store

We just saw how to override a method in the base class when we need to alter its behavior. I could have done something similar when it comes to saving the albums and media objects to the database. The Save method in theGalleryObject class could have been defined as virtual, and I could have overridden the method in each of the derived classes. But since the classes ImageVideoAudioGenericMediaObject, andExternalMediaObject all represent objects that get stored in the same table (gs_MediaObject), that would have meant writing the same code in all four classes, with only the Album class being different.
I could eliminate the problem of duplicate code by providing a default implementation in the Save method in theGalleryObject class. In that method, I save to the media object table, and then depend on the Album class to override the behavior, much like we did with the ValidateTitle method. However, this is putting a substantial amount of behavior in a base class that doesn't really belong there. We should limit the base class to contain state and behavior that applies to ALL derived objects.
You might argue that I violated this rule when I provided a default implementation of the ValidateTitle method that I overrode in the Album class. You are absolutely right. But I justify it by suggesting that implementing the title validation in every derived class creates undesirable duplicate code, and refactoring it to use the strategy pattern is overkill. These are not hard and fast rules. Architecting an application is as much art as it is science, and you must weigh the pros and cons of each approach.
Getting back to our challenge of persisting data to the data store, the approach I came up with was to use thestrategy pattern to encapsulate behavior. First, I defined an interface ISaveBehavior :
public interface ISaveBehavior { void Save(); }
Then I wrote two classes that implemented the interface: AlbumSaveBehavior and MediaObjectSaveBehavior. The Save method takes care of persisting the object to the hard drive and data store. For example, here is the Savemethod in AlbumSaveBehavior:
public void Save()
{
if (this._albumObject.IsVirtualAlbum)
return; // Don't save virtual albums.


// Save to disk.

PersistToFileSystemStore(this._albumObject);

// Save to the data store.

GalleryServerPro.Provider.DataProviderManager.Provider.Album_Save
(this._albumObject);
}
Notice that there is a call to PersistToFileSystemStore, which is a private method that ensures a directory exists corresponding to this album. Then there is a call to the Album_Save method of the Provider class, which persists the data to the gs_Album table in SQLite. If you use a data provider other than the defaultSQLiteMembershipProvider, then the method delegates to that provider. We'll talk more about the data provider model later in this article.
OK, we have two classes for saving data to the data store — one for albums and one for media objects. How do we invoke the appropriate Save method from the GalleryObject base class?
Recall that the GalleryObject class is abstract, so it can never be directly instantiated. Instead, we instantiate an instance of the AlbumImageVideoAudioGenericMediaObject, or ExternalMediaObject class. The constructor for each of these classes assigns the appropriate save behavior. For example, in the constructor of theAlbum class, we have:
this.SaveBehavior = Factory.GetAlbumSaveBehavior(this);
The GetAlbumSaveBehavior method just returns an instance of the AlbumSaveBehavior class:
public static ISaveBehavior GetAlbumSaveBehavior(IAlbum albumObject)
{
return new AlbumSaveBehavior(albumObject);
}
The SaveBehavior property of the GalleryObject class is of type ISaveBehavior. Since both classes implement this interface, we can assign instances of either class to the property.
The Save method in the GalleryObject class simply calls the Save method on the SaveBehavior property. It has no idea whether the property is an instance of AlbumSaveBehavior or MediaObjectSaveBehavior, and it doesn't care. All that matters is that each class knows how to save its designated object.
This is an example of using the strategy pattern. Specifically, the strategy pattern is defined as a family of algorithms that are encapsulated and interchangeable. In our case, we have two save behaviors that are self-contained and can both be assigned to the same property (interchangeable). It is a powerful pattern and has many uses.

Rendering HTML for Video, Images, Audio and More

Browsers, by themselves, typically cannot play video, audio, or render many kinds of documents like Adobe PDF or Microsoft Word files. These types of objects require plug-ins which may or may not be installed in your users' browsers.
Gallery Server Pro is flexible so that administrators can customize the HTML output rendered by Gallery Server. You, as the web site administrator, may feel comfortable having a dependence on a particular plug-in, such as Silverlight or Flash, while others may not. Also, some users might prefer that multimedia files are rendered with <object> tags in order to pass XHTML validation, while others might prefer <embed> for maximum backward compatibility. Lastly, different browsers require different syntax, and new versions of browsers are frequently released, potentially breaking something that works today.
Gallery Server Pro uses a combination of automatic browser sniffing and HTML and script templates stored in the database. The table gs_BrowserTemplate contains templates for each type of media object. For example, rendering the HTML for an image is pretty straightforward:
<div class="gsp_i_c" style="width:{Width}px;">
<img id="mo_img" src="{MediaObjectUrl}" class="{CssClass}" alt="{TitleNoHtml}"
title="{TitleNoHtml}" style=" width:{Width}px;height:{Height}px;" />
</div>
The text in brackets – like {MediaObjectUrl} – are placeholders that are replaced with dynamically generated content at runtime. For example, {Height} is replaced with the height of the image. A full list and descriptions of these placeholders can be found below.
When Gallery Server Pro uses the above template to render an image to the browser, it ends up looking something like this:
<div class="gsp_i_c" style="width:86px;">
<img id="mo_img"
src=http://www.codeproject.com/gs/handler/getmediaobject.ashx?moid=5&dt=2&g=1
class=""
alt="Grand Canyon" title="Grand Canyon" style="width:86px;height:115px;" />
</div>
If desired, one can tweak this template to change how <img> tags are rendered. For example, if you wanted to use the width and height attributes instead of a style, update the template to this:
<div class="gsp_i_c" style="width:{Width}px;">
<img id="mo_img" src="{MediaObjectUrl}" class="{CssClass}" alt="{TitleNoHtml}"
title="{TitleNoHtml}" height="{Width}" width="{Height}"> />

Rendering Browser-specific HTML

Let us jump in with a real world example of how Gallery Server Pro sends one browser different HTML than other browsers for a particular media type – in this case, images.
Gallery Server Pro renders images in a white border with rounded corners and a drop shadow. CSS is used to provide the rounded corner and drop shadow effect, but Internet Explorer 8 and earlier does not support these CSS effects. For these versions, we use a different drop shadow technique that requires a more complicated HTML structure:
<div class="gsp_floatcontainer">
<div class="op1">
<div class="op2">
<div class="sb">
<div class="ib">
<img id="mo_img" src="{MediaObjectUrl}" class="{CssClass}"
alt="{TitleNoHtml}" title="{TitleNoHtml}"
style="height:{Height}px;width:{Width}px;" />
</div>
</div>
</div>
</div>
</div>
In other words, when displaying images in the browser, we need one HTML pattern for IE 1-8 and another HTML pattern for all other browsers, including IE 9. We achieve this by having two rows in the gs_BrowserTemplatetable – one to handle the default rendering for images and another for IE 1-8 users:
Screenshot - HTML templates for image rendering
Gallery Server Pro automatically matches users with Internet Explorer 1-8 and matches them to the template with the browser ID "ie1to8". Everyone else gets the default template.
Browse the remaining records in this table to see how Gallery Server Pro handles other media types. There is aScriptTemplate column that can contain JavaScript to execute for each media object. This is an important column for Flash and Silverlight rendering. Feel free to modify the templates to suit your requirements. Note that Gallery Server Pro caches these records, so recycle the application pool after any changes to the table.

Browser ID Reference

Below are two lists of valid browser IDs – one for .NET 2.0 – 3.5 and the other for .NET 4.0.
Screenshot - .NET 2.0 - 3.5 Browser Definitions
Screenshot - .NET 4.0 Browser Definitions
The browser IDs are hierarchical, which means you can use one ID to match a subset of browsers. For example, entering the browser ID "Opera" will match all versions of Opera, while entering "Opera8to9" will match only Opera 8-9.
Notice that in .NET 4, there are new browser IDs for BlackBerry, iPhone, iPod, and IEMobile, allowing one to send targeted HTML to those systems. The current version of Gallery Server Pro does not take advantage of this, but you can by adding rows to gs_BrowserTemplate.

Side Note: Identifying Internet Explorer 1-8

You may have noticed the browser definitions shown above don't contain "IE1to8", which is what we used in our example. By default, Microsoft doesn't provide any way to target IE 1 to 8, so Gallery Server Pro does a little extra processing to check for this condition so we can use it as a valid browser ID.

Data Provider Model

One of the cool new features of ASP.NET 2.0 is the "provider model." In Gallery Server Pro, I used the provider model to define the API for reading and writing data to the data store. This allows one to use any source for data storage as long as a provider is written for it. Gallery Server Pro contains two providers - SQL Server and SQLite. Additional providers can be written that use MySQL, Oracle, Microsoft Access, or even an XML file as the data store.
The diagram below shows the SQLiteGalleryServerProProvider and DataProvider classes. TheDataProvider class is an abstract class that inherits from the Microsoft .NET Framework classSystem.Configuration.Provider.ProviderBase. It doesn't contain any behavior; it only defines the methods that must be implemented by the "real" data provider, which in this case is SQLiteGalleryServerProProvider. All data access in Gallery Server Pro passes through one of the methods in SQLiteGalleryServerProProvider(except user account functions that pass through the other providers — membership, roles, and profile).
Screenshot - providerclassdiagram.jpg
To use an alternative data store such as MySQL, Oracle, Microsoft Access, or something else, write a new class that inherits from the DataProvider abstract base class. This is best done in a new class library project. If you use a tool such as Visual Studio, it will automatically define all the methods that must be implemented. For example, here is the skeleton for the method to delete an album:
public override void Album_Delete(IAlbum album)
{
}
Gallery Server Pro will call this method whenever an album is to be deleted. It is your job, as the writer of this custom provider, to write the code that will delete the album record from your data store. How you do it is up to you.
Note: Refer to the code in the SQLiteGalleryServerProProvider class to ensure you provide similar behavior. For example, when deleting an album the SQLiteGalleryServerProProvider class executes a stored procedure that recursively deletes all child albums of the specified album. Your custom provider should behave similarly.
Once you have implemented all the methods and compiled your code, you are ready to configure Gallery Server Pro to use your provider. Copy the DLL containing your provider into the bin directory of the Gallery Server Pro web application. Update the data provider in the <galleryServerPro> section of web.config. For example, if your provider is in a class named OracleDataProvider that is in an assembly named GalleryServerPro.Data.Oracle.dll, the data provider section might look like this:
<galleryServerPro>
<core galleryResourcesPath="gs"/>
<dataProvider defaultProvider="OracleDataProvider">
<providers>
<clear/>
<add name="OracleDataProvider"
type="GalleryServerPro.Data.Oracle.OracleDataProvider,GalleryServerPro.Data.Oracle"
applicationName="Gallery Server Pro" connectionStringName="OracleDbConnection" />
</providers>
</dataProvider>
</galleryServerPro>

Image Metadata Extraction

Image files, most commonly JPGs, can contain metadata such as camera model and shutter speed. In addition, utilities such as Vista's Photo Gallery allow users to add keywords, titles, ratings, and more. Gallery Server Pro can extract this data in any of the following formats: EXIF, XMP, tEXt, IFD, and IPTC.
The code to extract metadata is based on the Code Project article A Library to Simplify Access to Image Metadata, which itself was based on the article Photo Properties. I'd like to thank these authors for their hard work. The techniques in these articles are based on parsing the metadata that is accessible through the PropertyItemsproperty of a System.Drawing.Image object. I refactored much of the code to make it easier to understand (and therefore maintain), faster, more flexible, and more robust.
The introduction of the System.Windows.Media.Imaging namespace in .NET 3.0 provided an improved method of extracting metadata, including the ability to get data not previously accessible — most notably titles and keywords.
So now there are two ways to get metadata from an image — the .NET 2.0 way and the .NET 3.0 way. While the .NET 3.0 technique is better, I wanted Gallery Server Pro to work on a system without .NET 3.0 installed. As a result, the metadata is extracted using the following process:
  1. If .NET 3.0 is installed on the web server, use the BitmapMetadata class in theSystem.Windows.Media.Imaging namespace to extract as much metadata as possible into a custom collection named GalleryObjectMetadataItemCollection.
  2. Instantiate the image into a System.Imaging.Image object. Use the PropertyItems property to extract as much metadata as possible. For each metadata item (e.g. shutter speed), add it to theGalleryObjectMetadataItemCollection collection, but only if it wasn't already added in step 1.
In other words, if the same metadata item is extracted using both techniques, we keep the data from the .NET 3.0 method and discard the .NET 2.0 version.
The logic for extracting metadata is hidden behind the business layer class MediaObjectMetadataExtractor. When an image is added to the gallery, the following code is executed in theGalleryServerPro.Business.Image constructor:
Metadata.MediaObjectMetadataExtractor metadata =
new Metadata.MediaObjectMetadataExtractor(imageFile.FullName);
this.MetadataItems.AddRange(metadata.GetGalleryObjectMetadataItemCollection());
The variable imageFile is an instance of System.IO.FileInfo that refers to the image. The MetadataItemsproperty is a GalleryObjectMetadataItemCollection collection. Once the metadata is extracted and saved, one can easily iterate through the items and get the name/value pairs:
// Assume we are loading an image with ID=27

IGalleryObject image =
GalleryServerPro.Business.Factory.LoadMediaObjectInstance(27);
foreach (IGalleryObjectMetadataItem metadataItem in image.MetadataItems)
{
string name = metadataItem.Description; // e.g. Camera model, Shutter speed

string value = metadataItem.Value; // e.g. F5.7, 1/350 sec
}