SharePoint Add-in: Accessing Webcam with Only Javascript

This blog post details how to access your Webcam via Javascript through the browser, and upload that content to a SharePoint library.  Then, with an added bonus, set it to be your User Profile Picture.

I have build this User Profile Webcam solution as an Add-in for SharePoint (was App for SharePoint).  This is a SharePoint Hosted app.  All the code runs in the browser and access SharePoint via the SharePoint Online API.

Step 1.  Access your webcam.

Modern Browsers:  Immersive Internet Explorer, Firefox, Chrome and Safari all has ways to access your webcam via

getUserMedia

 

But this doesn't work on IE (Desktop).  So for that we'll add a Polyfill (Flash). 

 

There's a project on github that does this nicely, and I use large chunks of the code from their demo.

https://github.com/addyosmani/getUserMedia.js

<script type="text/javascript" src="../Scripts/html5.js"></script>
<script type="text/javascript" src="../Scripts/getUserMedia.min.js"></script>
<!-- Add your JavaScript to the following file -->
<script type="text/javascript" src="../Scripts/App.js"></script>


<div id="webcam"></div>
<canvas id="canvas" height="240" width="320"></canvas>

<br />
<button class="btn" style="width:140px;" id="takeSnapshot">Take a picture</button>
this.snapshotBtn = document.getElementById('takeSnapshot');
$(this.snapshotBtn).click(this.getSnapshot);

this.getSnapshot = function () {
    // If the current context is WebRTC/getUserMedia (something
    // passed back from the shim to avoid doing further feature
    // detection), we handle getting video/images for our canvas 
    // from our HTML5 <video> element.
    if (App.options.context === 'webrtc') {
        var video = document.getElementsByTagName('video')[0];
        //App.canvas.width = video.videoWidth;
        //App.canvas.height = video.videoHeight;
        App.canvas.getContext('2d').drawImage(video, 0, 0, App.canvas.width, App.canvas.height);

        // Otherwise, if the context is Flash, we ask the shim to
        // directly call window.webcam, where our shim is located
        // and ask it to capture for us.
    } else if (App.options.context === 'flash') {
        window.webcam.capture();
        App.changeFilter();
    }
    else {
        alert('No context was supplied to getSnapshot()');
        return false;
    }
}

The code basically takes either the webrtc object and copy the image to the App canvas.  Or use the flash polyfill and Flash will write the image to the canvas element.

 

Step 2. Binary Format

This part is hairy.  But you are a developer, and dealing with Binary Encoding in JavaScript is what we do.

/*
Now, get canvas and do decoding.
1. canvas can return image data in png, in dataURL format.
2. strip the heading string "data:image/png;base64,"
*/

var ctx = App.canvas.getContext('2d');
var imageData = ctx.getImageData(0, 0, App.canvas.width, App.canvas.height);
var dataURL = App.canvas.toDataURL('image/png');
var base64 = dataURL.replace(/^data:image\/png;base64,/, "");

This is base64 dataUrl format.

 

/*
Reading.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding
https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#Solution_.232_.E2.80.93_rewriting_atob%28%29_and_btoa%28%29_using_TypedArrays_and_UTF-8

We need to convert DataURL to ByteArray
Mozilla has these two functions.
*/

function b64ToUint6(nChr) {
    return nChr > 64 && nChr < 91 ?
        nChr - 65
      : nChr > 96 && nChr < 123 ?
        nChr - 71
      : nChr > 47 && nChr < 58 ?
        nChr + 4
      : nChr === 43 ?
        62
      : nChr === 47 ?
        63
      :
        0;
}

function base64DecToArr(sBase64, nBlocksSize) {
    var
      sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
      nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2, taBytes = new Uint8Array(nOutLen);

    for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
        nMod4 = nInIdx & 3;
        nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 6 * (3 - nMod4);
        if (nMod4 === 3 || nInLen - nInIdx === 1) {
            for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
                taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
            }
            nUint24 = 0;
        }
    }
    return taBytes;
}

var byteArray = base64DecToArr(base64);

This gives us ByteArray.  We're getting close.

I'd like to tell you that I spend a good 3 evenings working to this point.  Try debugging that code and see if your eyes bleed!

/*

Finally, 

The ByteArray needs to be encoded as a string, and in the POST call, "binaryStringRequestBody" needs to be "true"

https://msdn.microsoft.com/en-us/library/office/dn769086.aspx
http://blogs.msdn.com/b/uksharepoint/archive/2013/04/20/uploading-files-using-the-rest-api-and-client-side-techniques.aspx
http://sharepoint.stackexchange.com/questions/54218/sharepoint-2013-rest-api-upload-image

*/

var binaryString = '';
var len = byteArray.byteLength;
for (var i = 0; i < len; i++) {
    binaryString += String.fromCharCode(byteArray[i])
}

SharePoint API wants data in BinaryString format.  Which is basically each byte of the ByteArray encoded as a concatenated string.

 

 

Step 3.  Upload to SharePoint

/*
Use RequestExecutor to post file back to sharepoint
*/

var appWebUrl = context.get_url();
var requestExecutor = new SP.RequestExecutor(appWebUrl);

var uploadPictureEndPoint = appWebUrl + "/_api/web/lists/getByTitle(@TargetLibrary)/RootFolder/Files/add(url=@TargetFileName,overwrite='true')?" +
    "&@TargetLibrary='" + "Pictures" + "'" +
    "&@TargetFileName='" + _spPageContextInfo.userLoginName + ".png" + "'";

/*
Using RequestExecutor, don't need REQUESTDIGEST
*/
//var digest = $("#__REQUESTDIGEST").val();

requestExecutor.executeAsync({
    url: uploadPictureEndPoint,
    method: "POST",
    headers: {
        "Accept": "application/json;odata=verbose"
    },
    contentType: "application/json;odata=verbose",
    binaryStringRequestBody: true,  // binaryStringRequestBody must be true
    body: binaryString,
    success: function (x, y, z) {
        alert("Success! Your file was uploaded to SharePoint.");
    },
    error: function (x, y, z) {
        alert("Oooooops... it looks like something went wrong uploading your file.");
    }
});

 

Step 4.  Setting User Profile Picture.

 

var appWebUrl = context.get_url();
var requestExecutor = new SP.RequestExecutor(appWebUrl);
var setPictureEndpoint = appWebUrl + "/_api/sp.userprofiles.peoplemanager/setmyprofilepicture";

requestExecutor.executeAsync({
    url: setPictureEndpoint,
    method: "POST",
    headers: {
        "Accept": "application/json;odata=verbose"
    },
    contentType: "application/json;odata=verbose",
    binaryStringRequestBody: true,
    body: App.binary,
    success: function (data) {
        alert('Set My Profile Picture succeeded, it will take a few seconds for the change to be propagated.');
    },
    error: function (error) {
        alert("Oooooops... it looks like something went wrong updating your profile picture (no permission?).");
    }
});

For this API call to /setmyprofilepicture to succeed, your App must have additional permissions.

This will ask the user (or Tenant Administrator) to grant the correct permission.

If you don't grant this permission, the webcam can still save picture to library, but it won't be able to set picture as user profile picture.

Summary

  • Get Webcam via Browser
  • Process canvas data to imageUrl to byteArray to BinaryString
  • Upload to SharePoint Library
  • Set as User Profile Picture
  • Achieve Zoolander face!
  • You can download the App for free from the Office Store to see it in action.  
    Feel free as a developer to hit F12 and step through the code.
  • Leave a comment below and let me know what you think.  For example, I think there's a need for an Outlook Add-in that uses the webcam.  Especially, if it can run on an iPad.

 

Quick - TSQL select XPath from XML Data Type with namespace

 

Examples on MSDN shows the full TSQL syntax for XPath query in a TSQL statement, this has been supported since SQL Server 2008.

SELECT CatalogDescription.query(' declare namespace PD="http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/ProductModelDescription"; <Product ProductModelID="{ /PD:ProductDescription[1]/@ProductModelID }" /> ') as Result FROM Production.ProductModel

This returns:

<Product ProductModelID="19"/>
<Product ProductModelID="23"/>

Wildcard in XPath Query

This is a much easier syntax with Wildcards that isn't well know.

SELECT CatalogDescription.query('/*:ProductDescription[1]/@ProductModelID') as Result
FROM Production.ProductModel

This returns:

19
23

Plenty of people will tell you using Wildcard matches in XPath is the worst thing in the world.  But I think if you just want to do a quick query, this is fine.

So, this is what can happen if you hack SP javascript files

 

SharePoint 2010's April 2014 CU introduced a bug with copy and paste. 

Bform.debug.js 381,869 11-Feb-2014 12:37
Bform.js 249,012 30-Jan-2014 16:39
Form.debug.js 211,134 11-Feb-2014 12:37
Form.js 126,783 30-Jan-2014 16:39

 

This was fixed and released in SharePoint 2010's August 2014 CU.
http://support.microsoft.com/kb/2760757

Bform.debug.js 381,888 15-Jul-2014 11:17
Bform.js 249,022 15-Jul-2014 11:17
Form.debug.js 211,153 15-Jul-2014 11:17
Form.js 126,793 15-Jul-2014 11:17

 

Story

On December 10, 2014, before we patched to the August 2014 CU level.  A certain very naughty person (me) couldn't wait and decided to offer a quick hack to the Javascript for the users over the holidays.  He even did a bak backup file of the original Form.js

 

image

 

We have now applied the August patch, but unfortunately, you can see:

bform.js, bform.debug.js, form.debug.js all updated.

Form.js is NOT updated.

Copy and paste still doesn't work in our environment.

 

Oops.

 

While a cheating way would be that we install the patch on another environment, and copy the correct Form.js file over. 

We'll be experimenting in this dev environment to see what is the best way to revert and go back to the supported path.  Stay tuned for a follow up article.

TypeScript Definition file for SPServices v0.1

 

Here's something I have wanted to write for a long time, both as Thank-You to Marc Anderson's work on SPServices, as well as for personal learning of the TypeScript language.

 

Adding TypeScript Definition reference:

image

 

$().sp (intellisense)

image

 

$().SPServices.defaults.cacheXML

image

 

$().SPServices.Version()

image

 

$().SPServices.SPCascadeDropdowns({ ... })

image

 

And one more.

$().SPServices({ ... })

image

 

Syntax Error Detection that TypeScript does so well. 

Except for the error message.

Repeat after me in a ROBOT way.  "String is missing apply from type Function".

What it means is I was expecting a function why did I find a string. 

And because Javascript can be surprisingly retarded, TypeScript tries to accommodate and wonders: perhaps this string is a function in disguise - so let me look for the apply method on this object.  NOPE.  It is not a function.   /s

 

image

 

 

Bonus.  TypeScript understands SPServices returns a JQueryXHR promise object.

image

 

Wait.  Where's all the other methods?

 

This is v0.1...

 

Download

https://static1.squarespace.com/static/5527bff2e4b0b430660b0d10/5527c30de4b030eeeef09715/5527c30ee4b030eeeef09d1c/1418221412217/jquery.SPServices.d.ts

Also, how do I get this into Marc's repo?

And I say to ASHX for SharePoint: make me a folder.

Sometimes, I do get to do some on-premises farm stuff.  Because deep in my soul, I'm a hardcore dev.  Muahaha.

Scenario

In InfoPath, we want to be able to:

  • Send people to a folder within a document library, for them to upload attachments. 
  • Each form has its own unique ID, say "1234".  The folder will be <site>/Attachments/1234/
  • The folder doesn't need to be created when it's not used.  That is, it would be great to create the folder ON DEMAND
  • Finally, InfoPath is quite dumb.  It only has a hyperlink.

 

Solution

  • Create a HTTP Handler that takes this URL:  <site/sitecollection>/_layouts/InfoPathHelper/InfoPathHandler.ashx?folder=<site>/attachments/<ID>
  • Create a folder on demand, and then respond via a HTTP Redirect.

 

Steps

 

image

  1. Add a ashx handler to your SharePoint solution.  CKSDev has great template for this.
  2. Add code to ProcessRequest
    public void ProcessRequest(HttpContext context)
    {
        if (!string.IsNullOrEmpty(context.Request.QueryString["folder"]))
        {
            CreateFolderAndRedirectResponse(context);
            return;
        }
    }

  3. Add a function to check for the folder, create it if we need it, and end with a redirect.

    private void CreateFolderAndRedirectResponse(HttpContext context)
    {
        // <site-collection>/_layouts/InfoPathHelper/InfoPathHandler.ashx?folder=<site>/

        var path = context.Request.QueryString["folder"];
        var server = new Uri(SPContext.Current.Web.Url);
        var url = string.Format("{0}://{1}{2}", server.Scheme, server.Authority, path);

        // elevate permission to create the folder.
        SPSecurity.RunWithElevatedPrivileges(() =>
        {
            try
            {
                using (var site = new SPSite(url))
                {
                    using (var web = site.OpenWeb())
                    {
                        SPFolder folder = web.GetFolder(path);
                        SPFolder f = folder;
                        List<SPFolder> folders = new List<SPFolder>();
                        SPDocumentLibrary library = folder.DocumentLibrary;
                        if (library == null)
                        {
                            return;
                        }
                        while(f.Url.ToLower() != library.RootFolder.Url.ToLower()){
                            if (f.Exists || string.IsNullOrEmpty(f.Url))
                            {
                                break;
                            }

                            folders.Add(f);

                            f = f.ParentFolder;
                            if (f == null)
                            {
                                // if this happens we're in trouble
                                return;
                            }
                        }


                        if (folders.Count > 0)
                        {
                            // we are in a GET request - need to allow unsafe updates
                            web.AllowUnsafeUpdates = true;
                            folders.Reverse();
                            foreach (SPFolder f1 in folders)
                            {
                                if (!f1.Exists)
                                {
                                    f1.ParentFolder.SubFolders.Add(f1.Name);
                                }
                            }
                            web.AllowUnsafeUpdates = false;
                        }
                    }
                }
            }
            catch (Exception ex)
            {

            }
        });

        context.Response.Redirect(path, true);
    }

  4. Wait what's all the strange looking nested folder stuff?  That's right, as a bonus, if you specify nested folder within the document library, the HTTP Handler will create those too!

    folder=/attachments/1234/1235/
  5. InfoPath is super simple, just add a Hyperlink to the URL

    image

    Link to Data Source:

    concat("/_layouts/InfoPathHelper/InfoPathHandler.ashx?attachments?folder=", my:ID)

 

See it running

 

image