Is there a way to play an audio file from a service worker?
I'm trying to use io.sound library but it is a JavaScript plugin that requires window, so it doesn't work.
EDIT
As suggested by Jeff I'm trying to open a new window and post a message to that window. this is my code:
function notifyClientToPlaySound() {
idbKeyval.get('pageToOpenOnNotification')
.then(url => {
console.log("notifyClientToPlaySound", url);
clients.matchAll({
type: "window"
//includeUncontrolled: true
})
.then((windowClients) => {
console.log("notifyClientToPlaySound - windowClients", windowClients);
for (var i = 0; i < windowClients.length; i++) {
var client = windowClients[i];
if (client.url === url && "focus" in client) {
notify({ event: "push" });
return client.focus();
}
}
//https://developer.mozilla.org/en-US/docs/Web/API/Clients/openWindow
if (clients.openWindow) {
return clients.openWindow("/")
.then(() => {
notify({ event: "push" });
});
}
})
});
}
This function is now called from event.waitUntil(..) inside self.addEventListener("push", (event) => { ... }
self.addEventListener("push", (event) => {
console.log("[serviceWorker] Push message received", event);
event.waitUntil(
idbKeyval.get('fetchNotificationDataUrl')
.then(url => {
console.log("[serviceWorker] Fetching notification data from -> " + url);
return fetch(url, {
credentials: "include"
});
})
.then(response => {
if (response.status !== 200) {
// Either show a message to the user explaining the error
// or enter a generic message and handle the
// onnotificationclick event to direct the user to a web page
console.log("[serviceWorker] Looks like there was a problem. Status Code: " + response.status);
throw new Error();
}
// Examine the text in the response
return response.json();
})
.then(data => {
if (!data) {
console.error("[serviceWorker] The API returned no data. Showing default notification", data);
//throw new Error();
showDefaultNotification({ url: "/" });
}
notifyClientToPlaySound(); <------ HERE
var title = data.Title;
var message = data.Message;
var icon = data.Icon;
var tag = data.Tag;
var url = data.Url;
return self.registration.showNotification(title, {
body: message,
icon: icon,
tag: tag,
data: {
url: url
},
requireInteraction: true
});
})
.catch(error => {
console.error("[serviceWorker] Unable to retrieve data", error);
var title = "An error occurred";
var message = "We were unable to get the information for this push message";
var icon = "/favicon.ico";
var tag = "notification-error";
return self.registration.showNotification(title, {
body: message,
icon: icon,
tag: tag,
data: {
url: "/"
},
requireInteraction: true
});
})
);
});
But when clients.openWindow is called, it returns the following exception:
Uncaught (in promise) DOMException: Not allowed to open a window.
How can I solve this?
The living specification for the Web Notifications API does reference a sound property that could be specified when showing a notification, and would theoretically allow you to play the sound of your choosing when showing a notification from a service worker.
However, while the specification references this property, as of the time of this writing, it's not supported in any browsers.
Update (Aug. '19): It looks like reference to sound has been removed from https://notifications.spec.whatwg.org/#alerting-the-user
Your best bet would be post a message along to an open window that's controlled by the current service worker, and have the window play the sound in response to the message event.
If there is no controlled client available (e.g. because your service worker has been awoken by a push event, and your site isn't currently open in a browser) then you'd have the option of opening a new window inside your notificationclick handler, which is triggered in response to a user clicking on the notification you display in your push event handler. You can then post a message to that new window.
Related
I have built a React progressive web application that makes use of service workers.
The service worker gets registered and is activated:
I have been trying to detect the "activate" event using this:
service-worker.js
navigator.serviceWorker.addEventListener("activate", function (event) {
console.log("service worker activated");
});
I added that at the end of the service-worker file. But, this event never gets triggered and I have no idea why.
I also tried to implement push notifications and trigger the from the backend. For this, I needed a "push" event listener that would listen to these events from the server:
navigator.serviceWorker.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(showPushNotification(title, description, image));
});
This is how showPushNotification is defined:
export function showPushNotification(title, description, image) {
if (!("serviceWorker" in navigator)) {
console.log("Service Worker is not supported in this browser");
return;
}
navigator.serviceWorker.ready.then(function (registration) {
registration.showNotification(title, {
body: description,
icon: image,
actions: [
{
title: "Say hi",
action: "Say hi",
},
],
});
});
}
I tested calling that function manually and it successfully triggerss a push notification.
This is the server code that triggers the push notification:
const sendPushNotification = async (user_id, title, description, image) => {
const search_option = { user: user_id };
const users_subscriptions = await PushNotificationSubscription.find(
search_option
);
const number_of_users_subscriptions = users_subscriptions.length;
const options = {
vapidDetails: {
subject: "mailto:xxxx#xxxx.com",
publicKey: VAPID_PUBLIC_KEY,
privateKey: VAPID_PRIVATE_KEY,
},
};
let push_notif_sending_results = {};
for (let i = 0; i < number_of_users_subscriptions; i++) {
const user_subscription = users_subscriptions[i];
await webPush
.sendNotification(
user_subscription,
JSON.stringify({
title,
description,
image,
}),
options
)
.then((notif_send_result) => {
push_notif_sending_results[i] = { success: notif_send_result };
})
.catch((error) => {
push_notif_sending_results[i] = { error: error };
});
}
return push_notif_sending_results;
};
This is the part responsible for sending the push notification:
webPush
.sendNotification(
user_subscription,
JSON.stringify({
title,
description,
image,
}),
options
)
And it's successfully executed as it returns a 201 HTTP response.
So the "push" event listener is supposed to detect it and trigger a push notification.
I think everything regarding the push notification has been successfully implementing and the problem is how the "push" event listener is added since the "activate" event listener also doesn't work.
So I tried moving the two event listeners here right after the registration of the service worker is successful:
function registerValidSW(swUrl, config) {
navigator.serviceWorker.register(swUrl).then((registration) => {
registration.addEventListener("activate", (event) => {
console.log(
"🚀 ~ file: serviceWorker.js:159 ~ navigator.serviceWorker.register ~ event",
event
);
});
registration.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(
showPushNotification(title, description, image)
);
});
});
}
But, it's still the same result.
Neither the "push" nor the "activate" event listeners get triggered.
Any idea what's going on?
Here's the whole service-worker file:
service-worker.js
import axios from "axios";
const isLocalhost = Boolean(
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets;
return;
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
} else {
// Is not localhost. Just register service worker
console.log(
"Is not localhost. Just register a service worker, by calling registerValidSW"
);
registerValidSW(swUrl, config);
}
});
}
}
async function subscribeToPushNotifications(serviceWorkerReg) {
let subscription = await serviceWorkerReg.pushManager.getSubscription();
if (subscription === null) {
const dev_public_vapid_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const prod_public_vapid_key =
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const public_vapid_key = isLocalhost
? dev_public_vapid_key
: prod_public_vapid_key;
subscription = await serviceWorkerReg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: public_vapid_key,
});
axios
.post("/api/push_notif_subscription/subscribe", subscription)
.then((response) => {})
.catch((error) => {});
}
}
export function showPushNotification(title, description, image) {
if (!("serviceWorker" in navigator)) {
console.log("Service Worker is not supported in this browser");
return;
}
navigator.serviceWorker.ready.then(function (registration) {
registration.showNotification(title, {
body: description,
icon: image,
actions: [
{
title: "Say hi",
action: "Say hi",
},
],
});
});
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker.register(swUrl).then((registration) => {
subscribeToPushNotifications(registration);
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (!installingWorker) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been preached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { "Service-Worker": "script" },
}).then((response) => {
// Ensure the service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get("content-type");
if (
response.status === 404 ||
(!!contentType && contentType.indexOf("javascript") === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
console.log("Service worker found, calling registerValidSW");
registerValidSW(swUrl, config);
}
});
}
export function unregister() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister();
});
}
}
navigator.serviceWorker.addEventListener("activate", function (event) {
console.log("service worker activated");
});
navigator.serviceWorker.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(showPushNotification(title, description, image));
});
The events "push" and "activate" are part of the ServiceWorkerGlobalScope as within the Service Worker API.
Push notifications must be handled within the service worker itself.
Therefore only the service worker can register an "activate" event listener.
The same applies for a "push" listener.
Specially in terms of the "push" listener this makes sense.
The idea of push events is to receive them, even if the main app (in this case the website) has been closed.
The service worker is an exception, as it even runs without the page being loaded.
Therefore move the "push" event into your service worker.
Your code (within the service worker) may look like this:
this.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(showPushNotification(title, description, image));
});
function showPushNotification(title, description, image) {
registration.showNotification(title, {
body: description,
icon: image,
actions: [
{
title: "Say hi",
action: "Say hi",
},
],
});
}
The rest seems fine to me.
Update (Some more explanation)
I took a more careful look at your service-worker.js and it seems it contains general methods for registering the service worker.
As mentioned above the main app and the service worker are two completely separate chunks of code, running in different spaces. So this means everything which is not supposed to run in the service worker itself must be put outside of the service-worker.js. The service worker (in your case) should only contain the code for handling push notifications. It's important that you do not include the "service-worker.js" within your application.
In your case, you may seperate these functions into service-worker-register.js which contain all functions which are for managing the service worker registration but should not be executed within the service worker itself (isLocalhost, register, subscribeToPushNotifications, registerValidSW, checkValidServiceWorker, and unregister). Please note the code snippet from above and make changes accordingly to the code left within the service worker.
MDN has a pretty in depth tutorial on service workers (and there are a lot more) I recommend having a look at:
developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers
I'm currently trying to make a postMessage() triggered from a push notification work correctly. Currently, I've got a PWA which has Firebase push notifications. When a notification is sent and clicked, the PWA is launched (if it's installed) and a postMessage() is sent.
However, the postMessage() doesn't get received by the client. If the app is in the foreground, it works as expected.
If the all is in the background though, it doesn't catch the postMessage(). I was wondering if I needed to listen to some kind of load event in my main sw.js file before sending the postMessage() - but I'm not sure.
My firebase-message-sw.js file is as follows:
messaging.onBackgroundMessage(function(payload) {
console.log('[firebase-messaging-sw.js] Received background message ', payload);
// Customize notification here
// self.addEventListener('load', (e) => {
console.log(`page has loaded | load event | firebase-messaging-sw.js`);
const channel = new BroadcastChannel('sw-messages');
channel.postMessage({
title: payload.notification.title,
body: payload.notification.body,
image: payload.notification.image,
// icon: event.notification.icon,
}, "*")
// })
})
My sw.js file is like this:
addEventListener('notificationclick', event => {
event.notification.close();
const urlToOpen = new URL("/settings", self.location.origin).href;
const promiseChain = clients.matchAll({
type: 'window',
includeUncontrolled: true
}).then((windowClients) => {
let matchingClient = null;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.url === urlToOpen) {
matchingClient = windowClient;
break;
}
}
if (matchingClient) {
return matchingClient.focus();
} else {
console.log(`opening new window`);
return clients.openWindow(urlToOpen);
}
});
event.waitUntil(promiseChain);
console.log("promiseChain | sw.js");
const channel = new BroadcastChannel('sw-messages');
channel.postMessage({
title: event.notification.title,
body: event.notification.body,
image: event.notification.image,
icon: event.notification.icon,
})
});
NOTE: This all works as required when the website is viewed in a browser, and not a PWA.
Does anyone has any suggestions/recommendations on what I should do?
Thanks in advance.
I've implemented the Push WebAPI in my web application using Service Worker as many articles explain on the web.
Now I need to store some data inside IndexedDB to make them available while the web app is closed (chrome tab closed, service worker in background execution).
In particular I would like to store a simple url from where retrieve the notification data (from server).
Here is my code:
self.addEventListener("push", (event) => {
console.log("[serviceWorker] Push message received", event);
notify({ event: "push" }); // This notifies the push service for handling the notification
var open = indexedDB.open("pushServiceWorkerDb", 1);
open.onsuccess = () => {
var db = open.result;
var tx = db.transaction("urls");
var store = tx.objectStore("urls");
var request = store.get("fetchNotificationDataUrl");
request.onsuccess = (ev) => {
var fetchNotificationDataUrl = request.result;
console.log("[serviceWorker] Fetching notification data from ->", fetchNotificationDataUrl);
if (!(!fetchNotificationDataUrl || fetchNotificationDataUrl.length === 0 || !fetchNotificationDataUrl.trim().length === 0)) {
event.waitUntil(
fetch(fetchNotificationDataUrl, {
credentials: "include"
}).then((response) => {
if (response.status !== 200) {
console.log("[serviceWorker] Looks like there was a problem. Status Code: " + response.status);
throw new Error();
}
return response.json().then((data) => {
if (!data) {
console.error("[serviceWorker] The API returned no data. Showing default notification", data);
//throw new Error();
showDefaultNotification({ url: "/" });
}
var title = data.Title;
var message = data.Message;
var icon = data.Icon;
var tag = data.Tag;
var url = data.Url;
return self.registration.showNotification(title, {
body: message,
icon: icon,
tag: tag,
data: {
url: url
},
requireInteraction: true
});
});
}).catch((err) => {
console.error("[serviceWorker] Unable to retrieve data", err);
var title = "An error occurred";
var message = "We were unable to get the information for this push message";
var icon = "/favicon.ico";
var tag = "notification-error";
return self.registration.showNotification(title, {
body: message,
icon: icon,
tag: tag,
data: {
url: "/"
},
requireInteraction: true
});
})
);
} else {
showDefaultNotification({ url: "/" });
}
}
};
});
Unfortunately when I receive a new push event it doesn't work, showing this exception:
Uncaught DOMException: Failed to execute 'waitUntil' on 'ExtendableEvent': The event handler is already finished.
at IDBRequest.request.onsuccess (https://192.168.0.102/pushServiceWorker.js:99:23)
How can I resolve this?
Thanks in advance
The initial call to event.waitUntil() needs to be done synchronously when the event handler is first invoked. You can then pass in a promise chain to event.waitUntil(), and inside that promise chain, carry out any number of asynchronous actions.
Your current code invokes an asynchronous IndexedDB callback before it calls event.waitUntil(), which is why you're seeing that error.
The easiest way to include IndexedDB operations inside a promise chain is to use a wrapper library, like idb-keyval, which takes the callback-based IndexedDB API and converts it into a promise-based API.
Your code could then look like:
self.addEventListener('push', event => {
// Call event.waitUntil() immediately:
event.waitUntil(
// You can chain together promises:
idbKeyval.get('fetchNotificationDataUrl')
.then(url => fetch(url))
.then(response => response.json())
.then(json => self.registration.showNotification(...)
);
});
I am implementing chrome push notification for my website users. Which I am able to do successfully.
I have two question ?
1) how to get the previous subscription id whenever i block the notification from browser setting. I have to remove the subscription id from my backend server
2) whenever i reload the website pushManager.subscribe method is running every time in which i am sending subscription id to server due to which the API is hitting every time with same subscription id
push.js
'use strict';
if ('serviceWorker' in navigator) {
console.log('Service Worker is supported');
navigator.serviceWorker.register('service_worker.js').then(function() {
return navigator.serviceWorker.ready;
}).then(function(reg) {
console.log('Service Worker is ready :^)', reg);
reg.pushManager.subscribe({userVisibleOnly: true}).then(function(sub) {
console.log('endpoint:',JSON.stringify(sub.endpoint));
console.log(sub.endpoint.substring('https://android.googleapis.com/gcm/send/'.length));
});
}).catch(function(error) {
console.log('Service Worker error :^(', error);
});
}
service-worker.js
'use strict';
var myurl;
console.log('Started', self);
self.addEventListener('install', function(event) {
self.skipWaiting();
console.log('Installed', event);
});
self.addEventListener('activate', function(event) {
console.log('Activated', event);
});
self.addEventListener('push', function(event) {
console.log('Push message', event);
event.waitUntil(
fetch('/notify.json').then(function(response) {
return response.json().then(function(data) {
console.log(JSON.stringify(data));
var title = data.title;
var body = data.body;
myurl=data.myurl;
return self.registration.showNotification(title, {
body: body,
icon: 'profile.png',
tag: 'notificationTag'
});
});
}).catch(function(err) {
console.error('Unable to retrieve data', err);
var title = 'An error occurred';
var body = 'We were unable to get the information for this push message';
return self.registration.showNotification(title, {
body: body,
icon: 'profile.png',
tag: 'notificationTag'
});
})
);
});
// var title = 'Vcona';
// event.waitUntil(
// self.registration.showNotification(title, {
// 'body': 'School Management',
// 'icon': 'profile.png'
// }));
self.addEventListener('notificationclick', function(event) {
console.log('Notification click: tag', event.notification.tag);
// Android doesn't close the notification when you click it
// See http://crbug.com/463146
event.notification.close();
var url = 'https://demo.innotical.com';
// Check if there's already a tab open with this URL.
// If yes: focus on the tab.
// If no: open a tab with the URL.
event.waitUntil(
clients.matchAll({
type: 'window'
})
.then(function(windowClients) {
console.log('WindowClients', windowClients);
for (var i = 0; i < windowClients.length; i++) {
var client = windowClients[i];
console.log('WindowClient', client);
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow(myurl);
}
})
);
});
Best pieces of advice I can give:
Keep track of your subscription (especially what you send to your server) in indexDB. Why IndexDB?
You can update indexDB in the window and in the serviceworker. This is important as you'll first get a PushSubscription in the window, but serviceworker will dispatch pushsubscriptionchange events which you should listen for and attempt to get a new PushSubscription, if you can.
When the page loads, check indexDB for an old subscription, if it exists, compare it to getSubscription() (i.e. your current subscription). This check should include any values you need server side, for example, when browsers go from not supporting payloads, to supporting them, they go from having no keys, to suddenly having keys - so you should check if you server has these keys or not.
DO NOT USE any of the API's for GCM, this will NOT work on other browsers (Firefox, Opera, Samsung Browser + others in the future) and aren't needed.
1) You can't get previous reg id. There are to ways:
Every time you subscribe for notifications you can save it to a local chrome db(for example indexdb) and when you subscribe another time you just restore you previous reg id from this db.
When you send a notification to GCM it responds you with canonical ids and another information about correctness of reg ids, so you can remove invalid one
2) You have to check first if subscription id already exists, then subscribe if not:
if ('serviceWorker' in navigator) {
console.log('Service Worker is supported');
navigator.serviceWorker.register('service_worker.js').then(function() {
return navigator.serviceWorker.ready;
}).then(function(reg) {
console.log('Service Worker is ready :^)', reg);
reg.pushManager.getSubscription().then(function(subscription) {
if(!subscription) {
reg.pushManager.subscribe({userVisibleOnly: true}).then(function(sub) {
console.log('endpoint:',JSON.stringify(sub.endpoint));
console.log(sub.endpoint.substring('https://android.googleapis.com/gcm/send /'.length));
//send new subscription id to server
return;
});
}
});
}).catch(function(error) {
console.log('Service Worker error :^(', error);
});
}
I need the chrome registration id to send it as a parameter to the API call, so I can fetch message corresponding to the registration id. My code is as follows:
self.addEventListener('push', function(event) {
var apiPath = 'http://localhost/api/v1/notification/getNotification?regId=';
event.waitUntil(registration.pushManager.getSubscription().then(function (subscription){
apiPath = apiPath + subscription.endpoint.split("/").slice(-1);
event.waitUntil(fetch(apiPath).then(function(response){
if(response.status !== 200){
console.log("Problem Occurred:"+response.status);
throw new Error();
}
return response.json().then(function(data){
var title = data.title;
var message = data.body;
var icon = data.icon;
var tag = data.tag;
var url = data.url;
return self.registration.showNotification(title,{
body: message,
icon: icon,
tag: tag,
data: url
});
})
}).catch(function(err){
var title = 'Notification';
var message = 'You have new notifications';
return self.registration.showNotification(title,{
body: message,
icon: '/images/Logo.png',
tag: 'Demo',
data: 'http://www.google.com/'
});
})
)
}));
return;
});
The error I am getting with the above code is:
Uncaught (in promise) DOMException:
Failed to execute 'waitUntil' on 'ExtendableEvent': The event handler is already finished.(…)
along with the extra notification 'The site has been updated in the background'.
Now even if I remove event.waitUntil before the fetch(apiPath) part, I am still getting that extra notification.
Please help me find a solution to this.
P.S: The question at Chrome Push Notification: This site has been updated in the background does'nt seem to be of any use in my case.
You don't need to call event.waitUntil multiple times. You just need to call it once and pass it a promise, the event's lifetime will be extended until the promise is resolved.
self.addEventListener('push', function(event) {
var apiPath = 'http://localhost/api/v1/notification/getNotification?regId=';
event.waitUntil(
registration.pushManager.getSubscription()
.then(function(subscription) {
apiPath = apiPath + subscription.endpoint.split("/").slice(-1);
return fetch(apiPath)
.then(function(response) {
if (response.status !== 200){
console.log("Problem Occurred:"+response.status);
throw new Error();
}
return response.json();
})
.then(function(data) {
return self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon,
tag: data.tag,
data: data.url,
});
})
.catch(function(err) {
return self.registration.showNotification('Notification', {
body: 'You have new notifications',
icon: '/images/Logo.png',
tag: 'Demo',
data: 'http://www.google.com/'
});
});
})
);
});