Taking Picture Snapshots with ASP.NET and HTML5
This is another post on HTML5 and ASP.NET integration. HTML5 brought along with it a number of great JavaScript APIs; one of them is getUserMedia, which is W3C standard and you can get a good description of it in MDN. In a nutshell, it allows access to the PC’s camera and microphone. Unfortunately, but not unexpectedly, it is not supported by any version of Internet Explorer, but Chrome and Firefox have it.
I wanted to be able to capture a snapshot and to upload it in AJAX style. Of course, if you know me you can guess I wanted this with ASP.NET Web Forms and client callbacks!
OK, so I came up with this markup:
1: <web:PictureSnapshot runat="server" ID="picture" Width="400px" Height="400px" ClientIDMode="Static" OnPictureTaken="OnPictureTaken"/>
The properties and events worth notice are:
-
Width & Height: should be self explanatory;
-
PictureTaken: a handler for a server-side .NET event that is raised when the picture is sent to the server asynchronously.
The PictureSnapshot control exposes a number of JavaScript methods:
-
startCapture: when called, starts displaying the output of the camera in real time;
-
stopCapture: pauses the update of the camera;
-
takeSnapshot: takes a snapshot of the current camera output (as visible on screen) and sends it asynchronously to the server, raising the PictureTaken event.
When the startCapture method is called, the PictureSnapshot control (or, better, its underlying VIDEO tag) starts displaying whatever the camera is pointing to (if we so authorize it).
Here is an example of one of my home walls… yes, I deliberately got out of the way!
OK, enough talk, the code for PictureSnapshot looks like this:
1: public class PictureSnapshot : WebControl, ICallbackEventHandler
2: {
3: public PictureSnapshot() : base("video")
4: {
5: }
6:
7: public event EventHandler<PictureTakenEventArgs> PictureTaken;
8:
9: protected override void OnInit(EventArgs e)
10: {
11: var sm = ScriptManager.GetCurrent(this.Page);
12: var reference = this.Page.ClientScript.GetCallbackEventReference(this, "picture", "function(result, context){ debugger; }", String.Empty, "function (error, context) { debugger; }", true);
13: var takeSnapshotScript = String.Format("\ndocument.getElementById('{0}').takeSnapshot = function(){{ var video = document.getElementById('{0}'); var canvas = document.createElement('canvas'); canvas.width = video.width; canvas.height = video.height; var context = canvas.getContext('2d'); context.drawImage(video, 0, 0, video.width, video.height); var picture = canvas.toDataURL(); {1} }};\n", this.ClientID, reference);
14: var startCaptureScript = String.Format("\ndocument.getElementById('{0}').startCapture = function(){{ var video = document.getElementById('{0}'); if ((video.paused == true) && (video.src == '')) {{ var getMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia); debugger; getMedia = getMedia.bind(navigator); getMedia({{ video: true, audio: false }}, function (stream) {{ video.src = window.URL.createObjectURL(stream); }}, function (error) {{ debugger; }}) }}; video.play(); }};\n", this.ClientID);
15: var stopCaptureScript = String.Format("\ndocument.getElementById('{0}').stopCapture = function(){{ var video = document.getElementById('{0}'); video.pause(); }};\n", this.ClientID);
16: var script = String.Concat(takeSnapshotScript, startCaptureScript, stopCaptureScript);
17:
18: if (sm != null)
19: {
20: this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat("snapshot", this.ClientID), String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ {0} }});\n", script), true);
21: }
22: else
23: {
24: this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat("snapshot", this.ClientID), script, true);
25: }
26:
27: if (this.Width != Unit.Empty)
28: {
29: this.Attributes.Add(HtmlTextWriterAttribute.Width.ToString().ToLower(), this.Width.ToString());
30: }
31:
32: if (this.Height != Unit.Empty)
33: {
34: this.Attributes.Add(HtmlTextWriterAttribute.Height.ToString().ToLower(), this.Height.ToString());
35: }
36:
37: this.Attributes.Remove("autoplay");
38: this.Attributes.Remove("controls");
39: this.Attributes.Remove("crossorigin");
40: this.Attributes.Remove("loop");
41: this.Attributes.Remove("mediagroup");
42: this.Attributes.Remove("muted");
43: this.Attributes.Remove("poster");
44: this.Attributes.Remove("preload");
45: this.Attributes.Remove("src");
46:
47: base.OnInit(e);
48: }
49:
50: protected virtual void OnPictureTaken(PictureTakenEventArgs e)
51: {
52: var handler = this.PictureTaken;
53:
54: if (handler != null)
55: {
56: handler(this, e);
57: }
58: }
59:
60: #region ICallbackEventHandler Members
61:
62: String ICallbackEventHandler.GetCallbackResult()
63: {
64: return (String.Empty);
65: }
66:
67: void ICallbackEventHandler.RaiseCallbackEvent(String eventArgument)
68: {
69: this.OnPictureTaken(new PictureTakenEventArgs(eventArgument));
70: }
71:
72: #endregion
73: }
As you can see, it inherits from WebControl. This is the simplest class in ASP.NET that allows me to output the tag I want, and also includes some nice stuff (width, height, style, class, etc). Also, it implements ICallbackEventHandler for the client callback stuff.
You may find it strange that I am setting explicitly the WIDTH and HEIGHT attributes, that is because VIDEO requires it in the tag, not just on the STYLE. In the way, I am removing any CROSSORIGIN, SRC, MUTED, PRELOAD, LOOP, AUTOPLAY, MEDIAGROUP, POSTER and CONTROLS attributes that may be present in the markup, because we don’t really want them.
The current picture is translated to a Data URI by using a temporary CANVAS, and this is what is sent to the server.
The PictureTakenEventArgs class:
1: [Serializable]
2: public sealed class PictureTakenEventArgs : EventArgs
3: {
4: public PictureTakenEventArgs(String base64Picture)
5: {
6: var index = base64Picture.IndexOf(',');
7: var bytes = Convert.FromBase64String(base64Picture.Substring(index + 1));
8:
9: using (var stream = new MemoryStream(bytes))
10: {
11: this.Picture = Image.FromStream(stream);
12: }
13: }
14:
15: public Image Picture
16: {
17: get;
18: private set;
19: }
20: }
And this is how to use this control in JavaScript:
1: <script type="text/javascript">
1:
2:
3: function takeSnapshot()
4: {
5: document.getElementById('picture').takeSnapshot();
6: }
7:
8: function startCapture()
9: {
10: document.getElementById('picture').startCapture();
11: }
12:
13: function stopCapture()
14: {
15: document.getElementById('picture').stopCapture();
16: }
17:
</script>
2:
3: <input type="button" value="Start Capturing" onclick="startCapture()"/>
4: <input type="button" value="Take Snapshot" onclick="takeSnapshot()" />
5: <input type="button" value="Stop Capturing" onclick="stopCapture()" />
Finally, a simple event handler for PictureTaken:
1: protected void OnPictureTaken(Object sender, PictureTakenEventArgs e)
2: {
3: //do something with e.Picture
4: }
Pictures arrive as instances of the Image class, and you can do whatever you want with it.
As always, feedback is greatly appreciated!