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.