{{announcement.body}}
{{announcement.title}}

Enable Background Sync, Media Capture, and Geolocation APIs in Your PWA

DZone 's Guide to

Enable Background Sync, Media Capture, and Geolocation APIs in Your PWA

In this tutorial, I will discuss advanced PWA features that provide access to your hardware APIs. We are going to build an app for making selfies with the Media Capture API and posting selfies with the BackgroundSync Api.

· Web Dev Zone ·
Free Resource

If you're looking to build a powerful PWA that takes advantage of the hardware on a device, things are only going to get better. In my previous post, I explained the fundamental concepts of PWA. In this article, I will discuss some PWA features that provide access to your hardware APIs:

Requirements

To start with this tutorial you must install the following:

  • Stable node version 8.9 or higher (https://nodejs.org/en/download/)

  • Git

Project Setup

As a starting point for the tutorial, clone this Github repository:                                                                  

Shell
 


xxxxxxxxxx
1
 
1
git clone https://github.com/petereijgermans11/progressive-web-app

Then in your terminal move to the following directory:  

Shell
 


xxxxxxxxxx
1
 
1
cd pwa-article/pwa-app-native-features-init

and install the dependencies through:

Shell
 




xxxxxxxxxx
1


 
1
npm i && npm start  


                                                           

Open your app on:  http:// localhost:8080   


Media Capture API

The Media Capture API allows authorized Web applications to access the streams from the device's audio and video capturing interfaces, i.e. to use the data available from the camera and the microphone. The streams exposed by the API can be bound directly to the HTML <audio> or <video> elements or read and manipulated in the code, including further more specific processing via Image Capture API, Media Recorder API or Real-Time Communication.

           figure 1


Media Capture API explanation

navigator.mediaDevices.getUserMedia(constraints)
 This prompts the user to access the media interface (video or audio).
                                             
stream.getAudioTracks()
 Returns a collection of audio tracks provided by your device's microphone.
                                                                        
stream.getVideoTracks()
 Returns a collection of video tracks provided by your device's camera.
                                                                        
mediaElement.srcObject = stream
 Sets a stream to be rendered in the provided <audio> or <video> HTML element.

Adjust the Progressive Selfies app to take selfies

Add this code to your index.html (Listing 1), directly below the tag: <div id = "create-post">.  In this code the "Capture button" is defined to take a snapshot (= selfie) of your video tracks.

JavaScript
 


xxxxxxxxxx
1
 
1
<video id="player" autoplay></video>
2
<canvas id="canvas" width="320px" height="240px"></canvas>
3
<button id="capture-btn"
4
         class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
5
   Capture
6
</button>

listing 1

The <video> and <canvas> tag is also defined for displaying your video and your snapshot (selfie) in your web page, respectively.

Add the following code into the existing feed.js to define your required variables (Listing 2). For example, the variable videoPlayer contains the HTML element <video> with the id = “player”. Your video tracks are rendered here. The canvasElement is for rendering your selfie, the captureButton is there to take a selfie.

JavaScript
 


xxxxxxxxxx
1
 
1
const videoPlayer = document.querySelector('#player');
2
const canvasElement = document.querySelector('#canvas'); 
3
const captureButton = document.querySelector('#capture-btn'); 
4
let picture;

listing 2

Add the initializeMedia() function in feed.js to initialize your camera.

JavaScript
 


x
 
1
const initializeMedia = () => {
2
   if (!('mediaDevices' in navigator)) {
3
       navigator.mediaDevices = {};
4
   }
5
   if (!('getUserMedia' in navigator.mediaDevices)) {
6
      navigator.mediaDevices.getUserMedia = (constraints) => {
7
          const getUserMedia = navigator.webkitGetUserMedia ||
8
                               navigator.mozGetUserMedia;
9
          if (!getUserMedia) {
10
              return Promise.reject(new Error('getUserMedia is not
11
                                               implemented!'));
12
          }
13
          return new Promise((resolve, reject) =>
14
                 getUserMedia.call(navigator, constraints, resolve, reject));
15
       }; 
16
   }
17
   navigator.mediaDevices.getUserMedia({video: {facingMode: 'user'},
18
                                                    audio: false})
17
   navigator.mediaDevices.getUserMedia({video: {facingMode: 'user'},
19
       .then(stream => {
20
           videoPlayer.srcObject = stream;
21
           videoPlayer.style.display = 'block';
22
           videoPlayer.setAttribute('autoplay', '');
23
           videoPlayer.setAttribute('muted', '');
24
           videoPlayer.setAttribute('playsinline', '');
25
       })
26
       .catch(error => {
27
           console.log(error);
28
       });
29
};

listing 3

This code initializes the camera. First, it is checked whether the API of the "mediaDevices" and "getUserMedia" is available in the navigator property of the window object. The window object represents the browser window (Javascript and the navigator property is also part of the window object). If the "mediaDevices" and "getUserMedia" are available in the navigator, the above code will prompt the user to access the camera by calling:

                                                             
navigator.mediaDevices.getUserMedia(constraints)
                                 

The call: videoPlayer.srcObject = stream, sets a stream (or video tracks), which is rendered in the provided <video> HTML element.

Add listing 4 in feed.js to define your "modal" to take a selfie. It also calls the above initializeMedia() function.

JavaScript
 




xxxxxxxxxx
1


 
1
const openCreatePostModal = () => {
2
   setTimeout(() => createPostArea.style.transform = 'translateY(0)', 1);
3
   initializeMedia();
4
};


listing 4

Add a click event handler to the "shareImageButton" in feed.js (see listing 5). This button (see figure 2) opens the "openCreatePostModal".Image 2

   figure 2
                                                                           
JavaScript
 




xxxxxxxxxx
1


 
1
shareImageButton.addEventListener('click', openCreatePostModal);


  listing 5                                  

Finally, add a click event handler for "Capture Button" in feed.js (listing 6). 

JavaScript
 




xxxxxxxxxx
1
11


1
captureButton.addEventListener('click', event => {
2
   canvasElement.style.display = 'block'; 
3
   videoPlayer.style.display = 'none'; 
4
   captureButton.style.display = 'none';
5
   const context = canvasElement.getContext('2d'); 
6
   context.drawImage(
7
       videoPlayer, 0, 0, canvasElement.width,
8
       videoPlayer.videoHeight / (videoPlayer.videoWidth / canvasElement.width)
9
   );
10
   videoPlayer.srcObject.getVideoTracks().forEach(track => track.stop());
11
   picture = dataURItoBlob(canvasElement.toDataURL());
12
});


listing 6

With this "Capture Button" you can take a snapshot / selfie of your video tracks (see figure 3). This snapshot is rendered in the canvasElement and converted to a Blob (see listing 7) via the function:     dataURItoBlob() for possible storage in a Database.

figure 3

Add this in utility.js:

JavaScript
 




xxxxxxxxxx
1
10


 
1
const dataURItoBlob= dataURI => {
2
   const byteString = atob(dataURI.split(',')[1]);
3
   const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
4
   const ab = new ArrayBuffer(byteString.length);
5
   const ia = new Uint8Array(ab);
6
   for (let i = 0; i < byteString.length; i++) {
7
       ia[i] = byteString.charCodeAt(i);
8
   }
9
   const blob = new Blob([ab], {type: mimeString});
10
   return blob; 
11
};


listing 7

If necessary, restart the server with npm and take a selfie using the Capture button (figure 4).

figure 4

Adjust the Progressive Selfies app to determine your location

                                                                        

Geolocation API

With the Geolocation API, web applications can access the location data provided by the device - obtained via GPS or through the network environment. Aside from the one-time location query, it also provides a way to notify the app of location changes.  

Geolocation API explanation
                                                                        
navigator.geolocation.getCurrentPosition(callback)

Performs a one-time search for the location with coordinates, accuracy, elevation and speed, if available.

                                                                        
navigator.geolocation.watchPosition(callback)

Location changes are observed.

Let's use the Geolocation API to determine the position of your selfie. Add the code below in your index.html, directly under the tag: div#manual-location. In this code, the "Get Location button" is defined to determine the location where you took the selfie (Listing 8).

JavaScript
 




xxxxxxxxxx
1
15


1
<div class="input-section">
2
   <button
3
       id="location-btn"
4
       type="button"
5
       class="mdl-button mdl-js-button mdl-button mdl-button--colored">
6
         Get Location
7
   </button>
8
   <div
9
       id="location-loader"
10
       class="mdl-spinner mdl-js-spinner is-active">
11
   </div>
12
</div>


 listing 8

Add listing 9 in feed.js, to define your required variables to determine the location.
JavaScript
 




xxxxxxxxxx
1


1
const locationButton = document.querySelector('#location-btn'); 
2
const locationLoader = document.querySelector('#location-loader'); 
3
let fetchedLocation = {lat: 0, lng: 0};


  listing 9                                                    

Add this initializeLocation() function in feed.js (listing 10):

JavaScript
 




xxxxxxxxxx
1


1
const initializeLocation = () => {
2
   if (!('geolocation' in navigator)) {
3
       locationButton.style.display = 'none';
4
   }
5
};


  listing 10    

And add the initializeLocation() function in openCreatePostModal, immediately after the initializeMedia() function call in feed.js (see Listing 11).                          

JavaScript
 




xxxxxxxxxx
1


 
1
const openCreatePostModal = () => {
2
   setTimeout(() => createPostArea.style.transform = 'translateY(0)',1);
3
   initializeMedia();
4
   initializeLocation();
5
};


    listing 11

Add a click event handler for the "locationButton" in feed.js. This "Location button" determines the location where you took the selfie (Listing 12).

JavaScript
 




xxxxxxxxxx
1
45


1
locationButton.addEventListener('click', event => {
2
   if (!('geolocation' in navigator)) {
3
       return; 
4
   }
5
   let sawAlert = false;
6
   locationButton.style.display = 'none';
7
   locationLoader.style.display = 'block';
8
   navigator.geolocation.getCurrentPosition(position => {
9
       locationButton.style.display = 'inline';
10
       locationLoader.style.display = 'none';
11
       fetchedLocation = {lat: position.coords.latitude, 
12
                          lng: position.coords.longitude};
13
       const reverseGeocodeService =
14
                       'https://nominatim.openstreetmap.org/reverse';
15
             fetch(`${reverseGeocodeService}?
16
                    format=jsonv2&lat=${fetchedLocation.
17
                    lat}&lon=${fetchedLocation.lng}`)
18
           .then(response => response.json())
19
           .then(data => {
20
               locationInput.value = `${data.address.country}, 
21
                                      ${data.address.state}`;
22
               document.querySelector('#manual-location').classList.
23
                                      add('is-focused');
24
           })
25
           .catch(error => {
26
               console.log(error);
27
               locationButton.style.display = 'inline';
28
               locationLoader.style.display = 'none';
29
               if (!sawAlert) {
30
                   alert('Couldn\'t fetch location, please enter
31
                   sawAlert = true;
32
}
33
               fetchedLocation = {lat: 0, lng: 0};
34
           });
35
   }, error => {
36
       console.log(error);
37
       locationButton.style.display = 'inline';
38
       locationLoader.style.display = 'none';
39
       if (!sawAlert) {
40
           alert('Couldn\'t fetch location, please enter manually!');
41
           sawAlert = true;
42
       }
43
       fetchedLocation = {lat: 0, lng: 0};
44
   }, {timeout: 7000});
45
});


listing 12

This code checks whether the API of the "geolocation" is available in the navigator property of the window object. If so, this code performs a one-time search to determine the location (with coordinates), via the function: navigator.geolocation.getCurrentPosition(). The address is then searched for using these coordinates via the 'openstreet map'.

figure 5

If necessary, restart the server with npm and retrieve the location using the "GET LOCATION" button (figure 5).

Send Selfies Online with BackgroundSync API

The BackgroundSync API allows users to queue data that needs to be sent to the server while a user is working offline, and then as soon as they’re online again, it sends the queued data to the server.

Example:

Let's say someone using our Progressive Selfies app wants to take and send a selfie, but the app is offline. Background-Sync API allows the user to queue a selfie while offline. As soon as it is back online, the Service Worker sends the data to the server. In our case we use an IndexedDB to store data in the meantime (figure 6). The library for this database can be found in the lib/idb.js folder. You can read about what and how a Service Worker works in my previous post about PWA.

                        figure 6                        


Clone the server

First clone the PWA-server via: 

Shell
 




xxxxxxxxxx
1


 
1
https://github.com/petereijgermans11/progressive-web-app-server


We will send the selfies to this server. 

Install the dependencies and start this server using: 

Shell
 




xxxxxxxxxx
1


 
1
npm i && npm start


The server runs on localhost: 3000

Sync selfies

To apply BackgroundSync to our app, we need to create a "store" in our Indexed-DB database to keep our "synchronized selfies" (Listing 13). We do that in utility.js. This utility.js contains the code needed for both the Service Worker and the app itself.


JavaScript
 




xxxxxxxxxx
1


1
const dbPromise = idb.openDb('selfies-store', 1, upgradeDB => {
2
   if (!upgradeDB.objectStoreNames.contains('selfies')) {
3
       upgradeDB.createObjectStore('selfies', {keyPath: 'id'});
4
   }
5
   if (!upgradeDB.objectStoreNames.contains('sync-selfies')) {
6
       upgradeDB.createObjectStore('sync-selfies', {keyPath: 'id'});
7
   }
8
});


listing 13

We need to use the BackgroundSync API when we send data to the server, via the submit event. Add a submit event handler in feed.js (Listing 14).
JavaScript
 




xxxxxxxxxx
1
38


 
1
form.addEventListener('submit', event => {
2
   event.preventDefault();
3
   if (titleInput.value.trim() === '' || locationInput.value.trim() === '' || !picture) {
4
       alert('Please enter valid data!');
5
       return;
6
   }
7
   closeCreatePostModal();
8
 
9
   const id = new Date().getTime();
10
 
11
   if ('serviceWorker' in navigator && 'SyncManager' in window) {
12
       navigator.serviceWorker.ready
13
           .then(sw => {
14
               const selfie = {
15
                   id: id,
16
                   title: titleInput.value,
17
                   location: locationInput.value,
18
                   selfie: picture,
19
               };
20
               writeData('sync-selfies', selfie)
21
                   .then(() => sw.sync.register('sync-new-selfies'))
22
                   .then(() => {
23
                       const snackbarContainer = 
24
                             document.querySelector('#confirmation-toast');
25
                       const data = {message: 'Your Selfie was saved for syncing!'};
26
                       snackbarContainer.MaterialSnackbar.showSnackbar(data);
27
                       readAllData('sync-selfies')
28
                           .then(syncSelfies => {
29
                             updateUI(syncSelfies);
30
                           })
31
                   })
32
                   .catch(function (err) {
33
                       console.log(err);
34
                   });
35
           });
36
   }
37
});
38
 
           


listing 14

In the first part of the code, the id is generated for the selfie to be sent. First we do a simple check to see if the browser supports serviceWorker and SyncManager. If this is the case and the Service Worker is ready, register a sync with the tag 'sync-new-selfies'. This is a simple string used to recognize this sync event. You can think of these sync tags as simple labels for different actions.

Then we save the selfie in the IndexedDB using the writeData function.

Finally, all stored selfies are read using readAllData("sync selfies") and are shown in your PWA via the function: updateUI(syncSelfies). A message is also sent out: "Your Selfie was saved for syncing!"

Service Worker

For the Service Worker to work correctly, a sync event listener must be defined in sw.js (Listing 15).

JavaScript
 




x


 
1
 self.addEventListener('sync', event => {
2
 console.log('[Service Worker] Background syncing', event);
3
 if (event.tag === 'sync-new-selfies') {
4
     console.log('[Service Worker] Syncing new Posts');
5
     event.waitUntil(
6
         readAllData('sync-selfies')
7
             .then(syncSelfies => {
8
                 for (const syncSelfie of syncSelfies) {
9
                     const postData = new FormData();
10
                     postData.append('id', syncSelfie.id);
11
                     postData.append('title', syncSelfie.title);
12
                     postData.append('location', syncSelfie.location);
13
                     postData.append('selfie', syncSelfie.selfie);
14
                     fetch(API_URL, {method: 'POST', body: postData})
15
                         .then(response => {
16
                             console.log('Sent data', response);
17
                             if (response.ok) {
18
                                 response.json()
19
                                     .then(resData => {
20
                                         deleteItemFromData('sync-selfies',
21
                                         parseInt(resData.id));
22
                                     });
23
                             }
24
                         })
25
                         .catch(error => 
26
                              console.log('Error while sending data', error));
27
                 }
28
             })
29
     );
30
 }
31
});


listing 15

The above sync event is only activated when the browser/device is reconnected. The Service Worker can handle these types of events (see my previous post about PWA). It also checks whether the current sync event has a tag that matches the string "sync-new-selfies". If we didn't have this tag, the sync event will fire every time the user is connected. 
As soon we are back online again, we retrieve the selfies stored in the IndexedDB. After that, the fetch API is used to send the selfies to the server, using the API_URL: http://localhost: 3000/selfies. Finally, the selfies already sent are removed from the IndexedDB.
                                                

Testing

 Believe it or not, testing all this is easier than you think: once you've visited the page and the Service Worker is up and running, all you have to do is disconnect from the network. Every Selfie that you try to send offline will now be stored in the IndexedDB (see figure 7). All saved selfies will also be shown in your main screen (see figure 8).

figure 7


figure 8

Once you are back online, the Service Worker will send the selfies to the server (see your Network tab in your Chrome Developer Tools in figure 9). There you will see that a selfie has been " posted" three times to the server. This server contains a folder called " images", which contains the posted selfies.   

figure 9

Finally

After this introduction you can continue with an extensive tutorial that you can find in: https://github.com/petereijgermans11/progressive-web-app/tree/master/pwa-workshop. In a subsequent article I will discuss other APIs such as Web Streams API, Push API (for receiving push notifications) and the web Bluetooth API.

Follow or like me on twitter https://twitter.com/EijgermansPeter


Topics:
backgroundworker, geolocation api, javascript, pwa, selfie, service worker, sync api, webapi, webapps

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}