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! Smile

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

image

Here is an example of one of my home walls… yes, I deliberately got out of the way! Smile

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!

                             

2 Comments

  • Hi Ricardo!
    I have been looking this days on your different posts, I believe it´s the approach I´ve looking a while to learn and to implement video into aspx in a easy way,
    The point is that I was wondering if it is posible to set up or configure an external device (IP + PORT) as the source of the video instead of using a web cam plugged by usb, I still investigating this with no success so I decided to write, any clue or advice would be really appreciated to move forward.

    Thank you for sharing,
    Kind rewards.

  • @Alberto: I apologize for the delay, but I wasn't getting notifications from pending comments!
    To be honest, I don't know... I guess it should be possible, provided you have a .NET API, to take a screenshot and to return it. Consider using SignalR for that. Unfortunately, I do not know of such APIs, sorry! But keep us informed! ;-)

Add a Comment

As it will appear on the website

Not displayed

Your website