Asynchronous Video Downloader
I've been exploring asynchronous operations in ASP.NET 2.0 and while I don't have anything original to contribute you might find how I used this technique interesting. Even when the code is well documented it can be useful to know how it might be applicable to a specific problem.
Awhile ago I figured out how to authenticate for Seesmic API calls using ASP.NET 2.0 based on some PHP sample code. I continued to work with the Seesmic API and expanded my code to get user videos after successfully authenticating. I noticed that in addition to providing a thumbnail of the user video, Seesmic also gives you the url to a FLV file (i.e. Flash Video). This is probably intended for a video player but I decided that I wanted my code to download the videos.
Since downloading a video file can be time consuming and may cause an ASP.NET page to timeout, I wanted the video download to occur asynchronously. However this required making asynchronous web requests in a loop and all of the sample code I found did not attempt to make more than one asynchronous web service request. Initially I tried the old method of doing asynchronous operations which requires a callback delegate. However my ASP.NET page was timing out and there is a new way to do this in ASP.NET 2.0, see the MSDN article Asynchronous Pages in ASP.NET 2.0. I had better luck with the new method of creating asynchronous pages. I was able to successfully download 20 video files totaling 29 MB without causing the ASP.NET page to timeout. I think that is pretty impressive.
Below is my sample code for this little experiment. You will need a Seesmic account and since it is still an "alpha release" this means you have to request an account. If the code appears cut off remember that it is still there, merely hidden, and will be there when you copy and paste. There is an username hard coded in the GetUserVideos function so you may want to change that.
I'm now working on a new project involving converting a PHP script, which uses the GD Library to create dynamic images, into ASP.NET. So far it looks doable and it should be quite innovative.
ASPX Code
1: <%@ Page Language="VB" AutoEventWireup="false" CodeFile="SeesmicAuth.aspx.vb" Inherits="SeesmicAuth" title="Seesmic API Authentication" Async="true" AsyncTimeout="3000" %>
2: <asp:Content ID="Content1" ContentPlaceHolderID="cphFull" Runat="Server">
3: <div class="Box">
4: <div class="BoxTitle">
5: <asp:Label ID="Label1" runat="server">Seesmic API Authentication Login</asp:Label>
6: </div>
7: <div class="BoxContent">
8: <!-- page content begins -->
9: <asp:PlaceHolder ID="phLoginForm" runat="server">
10: <h2>Please login using your seesmic account</h2>
11: <div style="width:200px;float:left;text-align:right;">
12: <p>Enter username: <asp:TextBox ID="txtUserName" runat="server"></asp:TextBox></p>
13: <p>Enter password: <asp:TextBox ID="txtPassword" runat="server"></asp:TextBox></p>
14: <asp:Button ID="btnSubmit" Text="Submit" runat="server" />
15: <br clear="all" /><br />no information is stored on this server in this process
16: </asp:PlaceHolder>
17: <asp:PlaceHolder ID="phSuccess" runat="server" Visible="false">
18: <h2>You have authenticated successfully!</h2>
19: Your current sid is: <asp:Label ID="lblSID" Font-Bold="true" runat="server"></asp:Label> and the session_id is <asp:Label ID="lblSessionID" Font-Bold="true" runat="server"></asp:Label>
20: </asp:PlaceHolder>
21: <asp:PlaceHolder ID="phFailure" runat="server" Visible="false">
22: <h2 style="color:red;">Seesmic login failed</h2>
23: </asp:PlaceHolder>
24: <br /><br />
25: <asp:PlaceHolder ID="phVideos" runat="server" Visible="false">
26: </asp:PlaceHolder>
27: <!-- page content ends -->
28: </div>
29: </div>
30: </asp:Content>
31:
32:
VB.NET Code
1: Imports System.Net
2: Imports System.IO
3: Imports System.Text
4: Imports System.Security.Cryptography
5: Imports System.Web.Script.Serialization
6: Imports System.Diagnostics
7: Imports System.Collections.Generic
8: Imports System.Xml
9: Imports System.Threading
10:
11: Partial Class SeesmicAuth
12: Inherits System.Web.UI.Page
13:
14: Private Delegate Sub DownloadFLVDelegate(ByVal urlFlv As String)
15:
16: Protected Sub btnSubmit_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnSubmit.Click
17: Dim seesmicAuthUrl As String = ""
18: seesmicAuthUrl = "http://api.seesmic.com/login/auth?username=" & txtUserName.Text & "&password=" & GenerateMD5Hash(txtUserName.Text, txtPassword.Text)
19: Debug.WriteLine(seesmicAuthUrl, "seesmicAuthUrl")
20:
21: ' Make a web request
22: Dim HttpSite As Uri = New Uri(seesmicAuthUrl)
23: Dim objHttpWebRequest As HttpWebRequest = CType(WebRequest.Create(HttpSite), HttpWebRequest)
24: objHttpWebRequest.KeepAlive = False
25: objHttpWebRequest.Timeout = 30000
26:
27: ' Get the response
28: Dim objWebResponse As WebResponse = objHttpWebRequest.GetResponse()
29: Dim objStreamReader As StreamReader = New StreamReader(objWebResponse.GetResponseStream(), System.Text.Encoding.UTF8)
30:
31: Dim strJsonResponse = String.Empty
32: Dim js As New JavaScriptSerializer
33: Dim objDictionary As Dictionary(Of String, Object)
34:
35: ' Read the response using a stream reader
36: strJsonResponse = objStreamReader.ReadToEnd()
37: ' Deserialize the JSON string into a dictionary object
38: objDictionary = js.DeserializeObject(strJsonResponse)
39: Debug.WriteLine(strJsonResponse, "strJsonResponse")
40:
41: ' The value of the login key is another dictionary object
42: Dim objSubDictionary As Dictionary(Of String, Object) = objDictionary("login")
43:
44: ' Note: Make sure the dictionary contains a key before trying to reference it!
45: If objSubDictionary.ContainsKey("success") Then
46: If objSubDictionary("success") = "true" Then
47: ' Authenticated, show sid and session_id values
48: phLoginForm.Visible = False
49: phSuccess.Visible = True
50: If objSubDictionary.ContainsKey("sid") Then
51: lblSID.Text = objSubDictionary("sid")
52: End If
53: If objSubDictionary.ContainsKey("session_id") Then
54: lblSessionID.Text = objSubDictionary("session_id")
55: End If
56: ' Make an API request
57: GetUserVideos(objSubDictionary("sid"))
58: Else
59: ' Authentication failed, show a different message
60: phLoginForm.Visible = False
61: phFailure.Visible = True
62: End If
63: End If
64:
65: ' Debugging authentication failures
66: If objSubDictionary.ContainsKey("success") Then
67: Debug.WriteLine(objSubDictionary("success"), "success")
68: End If
69: If objSubDictionary.ContainsKey("reason") Then
70: Debug.WriteLine(objSubDictionary("reason"), "reason")
71: End If
72:
73: ' Code to figure out what objects JavaScriptSerializer.DeserializeObject returns
74: 'For Each obj As Object In d
75: ' Debug.WriteLine(obj.GetType(), "Object Type")
76: ' Dim c As KeyValuePair(Of String, Object) = DirectCast(obj, KeyValuePair(Of String, Object))
77: ' Debug.WriteLine(c.Key, "Key")
78: ' Debug.WriteLine(c.Value, "Value")
79: ' Dim f As Dictionary(Of String, Object) = c.Value
80: ' Debug.WriteLine(f.Count, "Value")
81: ' Debug.WriteLine(f("reason"), "reason")
82: 'Next
83:
84: objStreamReader.Close()
85: objWebResponse.Close()
86: End Sub
87:
88: Private Function GenerateMD5Hash(ByVal username As String, ByVal password As String) As String
89: 'Create an instance of the MD5CryptoServiceProvider class
90: Dim md5Hasher As New MD5CryptoServiceProvider()
91: 'The array of bytes that will contain the encrypted value
92: Dim hashedBytes As Byte()
93: 'The encoder class used to convert plain text to an array of bytes
94: Dim encoder As New UTF8Encoding()
95: 'Call ComputeHash, passing in the plain-text string as an array of bytes
96: 'The return value is the encrypted value, as an array of bytes
97: hashedBytes = md5Hasher.ComputeHash(encoder.GetBytes(username & password))
98: Debug.WriteLine(BitConverter.ToString(hashedBytes).Replace("-", "").ToLower(), "BitConverter")
99: Return BitConverter.ToString(hashedBytes).Replace("-", "").ToLower()
100: End Function
101:
102: Public Sub GetUserVideos(ByVal strSid As String)
103: Dim seesmicAPIUrl As String = ""
104: seesmicAPIUrl = "http://api.seesmic.com/users/renetto/videos.xml?sid=" & strSid
105: Debug.WriteLine(seesmicAPIUrl, "seesmicAPIUrl")
106:
107: ' Make a web request
108: Dim HttpSite As Uri = New Uri(seesmicAPIUrl)
109: Dim objHttpWebRequest As HttpWebRequest = CType(WebRequest.Create(HttpSite), HttpWebRequest)
110: objHttpWebRequest.KeepAlive = False
111: objHttpWebRequest.Timeout = 900000
112:
113: ' Get the response
114: Dim objWebResponse As WebResponse = objHttpWebRequest.GetResponse()
115: Dim objStreamReader As StreamReader = New StreamReader(objWebResponse.GetResponseStream(), System.Text.Encoding.UTF8)
116: Dim objXmlTextReader As XmlTextReader = New XmlTextReader(objStreamReader)
117: Dim objXmlDocument As XmlDocument = New XmlDocument()
118: objXmlDocument.Load(objXmlTextReader)
119:
120: Dim nsMgr = New XmlNamespaceManager(objXmlDocument.NameTable)
121: Dim sbHTML As New System.Text.StringBuilder("")
122:
123: sbHTML.Append("<ul>")
124: sbHTML.Append(Environment.NewLine)
125:
126: Dim strTitle As String = ""
127: Dim strUserName As String = ""
128: Dim strUrlPlayer As String = ""
129: Dim strUrlThumbnail As String = ""
130: Dim strCreatedDate As String = ""
131:
132: Dim objRecordNodeList = objXmlDocument.SelectNodes("records/record", nsMgr)
133: For Each objXmlNode As XmlNode In objRecordNodeList
134: sbHTML.Append("<li class=""VideoRecord"">")
135: sbHTML.Append(Environment.NewLine)
136:
137: For Each objXmlElement As XmlElement In objXmlNode.ChildNodes
138: If objXmlElement.Name = "title" Then
139: strTitle = objXmlElement.InnerText
140: End If
141: If objXmlElement.Name = "username" Then
142: strUserName = objXmlElement.InnerText
143: End If
144: If objXmlElement.Name = "url-player" Then
145: strUrlPlayer = objXmlElement.InnerText
146: End If
147: If objXmlElement.Name = "url-thumbnail" Then
148: strUrlThumbnail = objXmlElement.InnerText
149: End If
150: If objXmlElement.Name = "created-at" Then
151: strCreatedDate = objXmlElement.InnerText
152: End If
153: If objXmlElement.Name = "url-flv" Then
154: ' download the flv file using an asynchronous page task
155: Dim urlFlv As Uri = New Uri(objXmlElement.InnerText)
156: ' Pass additional parameter using the state object
157: Dim state As Object
158: state = urlFlv.ToString()
159: Dim bh As New BeginEventHandler(AddressOf BeginAsyncTask)
160: Dim eh As New EndEventHandler(AddressOf EndAsyncTask)
161: Dim th As New EndEventHandler(AddressOf TimeoutAsyncOperation)
162: Dim objTask As New PageAsyncTask(bh, eh, th, state)
163: RegisterAsyncTask(objTask)
164: End If
165: Next
166:
167: sbHTML.Append("<h3 class=""VideoTitle"">" & strTitle & "</h3>")
168: sbHTML.Append(Environment.NewLine)
169: sbHTML.Append("<p>" & strUserName & "</p>")
170: sbHTML.Append(Environment.NewLine)
171: sbHTML.Append("<a href=""" & strUrlPlayer & """>")
172: sbHTML.Append(Environment.NewLine)
173: sbHTML.Append("<img src=""" & strUrlThumbnail & """>")
174: sbHTML.Append(Environment.NewLine)
175: sbHTML.Append("</a>")
176: sbHTML.Append(Environment.NewLine)
177: sbHTML.Append("<br>")
178: sbHTML.Append(Environment.NewLine)
179: sbHTML.Append(Convert.ToDateTime(strCreatedDate).ToShortDateString & " " & Convert.ToDateTime(strCreatedDate).ToShortTimeString)
180: sbHTML.Append(Environment.NewLine)
181:
182: sbHTML.Append("</li>")
183: sbHTML.Append(Environment.NewLine)
184: Next
185:
186: sbHTML.Append("</ul>")
187: sbHTML.Append(Environment.NewLine)
188:
189: ' save the xml file for future reference
190: objXmlDocument.Save(Server.MapPath("App_Data/" & strUserName & "-videos.xml"))
191:
192: Dim objGenericControl As HtmlControls.HtmlGenericControl = New HtmlControls.HtmlGenericControl
193: objGenericControl.InnerHtml = sbHTML.ToString()
194: phVideos.Controls.Add(objGenericControl)
195: phVideos.Visible = True
196:
197: objWebResponse.Close()
198: objHttpWebRequest.Abort()
199: End Sub
200:
201: Function BeginAsyncTask(ByVal sender As Object, ByVal e As EventArgs, ByVal cb As AsyncCallback, ByVal state As Object) As IAsyncResult
202: Dim urlFlv As String = DirectCast(state, String)
203: Dim download As New DownloadFLVDelegate(AddressOf DownloadFLV)
204: Return download.BeginInvoke(urlFlv, cb, state)
205: End Function
206:
207: Sub EndAsyncTask(ByVal ar As IAsyncResult)
208: Debug.WriteLine("End of Asynchronous Task")
209: End Sub
210:
211: Sub TimeoutAsyncOperation(ByVal ar As IAsyncResult)
212: Debug.WriteLine("Asynchronous Task timed out downloading file: " & DirectCast(ar.AsyncState, String))
213: End Sub
214:
215: Sub DownloadFLV(ByVal urlFlv As String)
216: Dim objWebClient As WebClient = New WebClient()
217: Dim strFlvFileName = urlFlv.ToString.Replace("http://v.seesmic.com/flv/", "")
218: Try
219: Debug.WriteLine("Downloading..." & strFlvFileName)
220: objWebClient.DownloadFile(urlFlv, Server.MapPath("App_Data/" & strFlvFileName))
221: Catch ex As WebException
222: Debug.WriteLine("Error downloading file.." & strFlvFileName)
223: Debug.WriteLine(ex.ToString())
224: End Try
225: End Sub
226:
227: End Class