I started developing with WebRTC, but that thing never gives me ICE candidates. I set up everything, I'm exchanging the descriptions and stuff, I also made a super-ugly function narrow down there to make sure everything runs correctly, one after another. Signaling state is stable for both, onError is never fired (as expected), but onIceCandidate also (not as expected), and when I want to send a random, empty MediaStream object pc1.addStream(new webkitMediaStream());, it always fires onNegotiationNeeded.
Does anyone have an idea what the heck is wrong with my code? I spent hours of browsing Stack Overflow, HTML5 Rocks, and W3C docs, but I don't understand that. Here is my entire code:
var config={
'iceServers':[{
'url':'stun:stun.l.google.com:19302'
},{
'url':'stun:stun1.l.google.com:19302'
},{
'url':'stun:stun2.l.google.com:19302'
},{
'url':'stun:stun3.l.google.com:19302'
},{
'url':'stun:stun4.l.google.com:19302'
}]
};
var pc1=new webkitRTCPeerConnection(config);
var pc2=new webkitRTCPeerConnection(config);
var onError=function(error)
{
console.error(error);
}
pc1.onicecandidate=function()
{
console.log('PC1 onIceCandidate (finally) fired!');
}
pc2.onicecandidate=function()
{
console.log('PC2 onIceCandidate (finally) fired!');
}
pc1.oniceconnectionstatechange=function()
{
console.log('PC1 oniceconnectionstatechange fired!');
}
pc2.oniceconnectionstatechange=function()
{
console.log('PC2 oniceconnectionstatechange fired!');
}
pc1.onnegotiationneeded=function()
{
console.log('PC1 onnegotiationneeded fired!');
}
pc2.onnegotiationneeded=function()
{
console.log('PC2 onnegotiationneeded fired!');
}
pc1.createOffer(function(offer){
pc1.setLocalDescription(offer,function(){
pc2.setRemoteDescription(new RTCSessionDescription(offer),function(){
pc2.createAnswer(function(answer){
pc2.setLocalDescription(answer,function(){
pc1.setRemoteDescription(new RTCSessionDescription(answer),new Function()/*I don't need you, bro*/,onError);
},onError);
},onError);
},onError);
},onError);
},onError);
BTW I'm developing with Google Chrome. I will make sure it also runs in Firefox, but right now the problem should be cross-browser. I want to make it to data channels before... (but I have nothing against a working solution with Firefox or cross-browser code)
In Chrome 38 and earlier, OfferToReceiveAudio defaulted to true. Starting from Chrome 39, OfferToReceiveAudio defaults to false, as announced by a WebRTC engineer at PSA: Behavior change to PeerConnection.createOffer constraint OfferToReceiveAudio (quoted below).
Because of this change, the SDP returned by createOffer does not contain any media, and therefore the ICE gathering process never starts. You can notice the consequences of this change by observing that the ICE events are never triggered, and the PeerConnection's iceGatheringState and iceConnectionState stay "new".
To make sure that the ICE gathering starts and complete, you have to add media to your offer, e.g. by setting OfferToReceiveAudio:true in the following constraints to your offer (either as a parameter of the PeerConnection constructor, or as a parameter to the peerConnection.createOffer method):
{
mandatory: {
OfferToReceiveAudio: true
}
}
(other ways to get media in the SDP include setting OfferToReceiveVideo:true, or calling peerConnection.addStream with a media stream that you got from getUserMedia)
webrtc-discuss: PSA: Behavior change to PeerConnection.createOffer constraint OfferToReceiveAudio:
I'm going to submit a change (https://webrtc-codereview.appspot.com/16309004/) to change the behavior of RTCPeerConnection.createOffer.
The change is expected to be included in Chrome M39.
What's changed:
Currently if the OfferToReceiveAudio constraint is not specified in PeerConnection.createOffer, the resulted offer SDP will have an "m=audio" line even if there is no audio track attached to the PeerConnection. In other words, OfferToReceiveAudio defaults to true.
After my change, OfferToReceiveAudio no longer defaults to true. Whether the offer SDP has an "m=audio" line depends on if any audio track has been attached to the PeerConnection.
What's not changed:
The behavior of setting an explicit value for OfferToReceiveAudio remains the same, i.e. OfferToReceiveAudio:true will result in an "m=audio" line regardless of the existence of audio tracks; OfferToReceiveAudio:false will result in no "m=audio" line regardless of the existence of audio tracks, unless setLocalDescription has been called with an SDP containing an "m=audio" line, in which case the new offer SDP will mark the audio content inactive instead of removing the audio content.
The above solution of Rob W from Jan 3 '15 worked for me at first but led to another problem.
I included {'offerToReceiveAudio':true,'offerToReceiveVideo':true} in the createOffer and createAnswer calls and onIceCandidate was called as described.
However, if I got it right, offerToReceive... means to only receive but not to send a stream. I figured that out because of the a=recvonly in the sdp offer and the a=sendonly in the sdp answer. As a result, only the caller could see the callees video but not vice versa.
As Rob states correctly:
other ways to get media in the SDP include [...] calling peerConnection.addStream with a
media stream that you got from getUserMedia
Adding the stream was what I had done already in the first place. However, my sending of the sdp happened before that adding because my logical flow was mixed up. Bringing that into the right order (addStream -> sdp = getLocalDescription -> send(sdp)) and removing the offerOptions did the trick for me.
Hope this helps anyone.
Solution in 2020
You have to do 2 things:
include offerToReceiveAudio and offerToReceiveVideo when create RTCPeerConnection
add addTrack
Sample code:
const peerConnection= new RTCPeerConnection({
configuration: {
offerToReceiveAudio: true,
offerToReceiveVideo: true
},
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
})
localStream.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream)
})
const offer = await peerConnection.createOffer()
await peerConnection.setLocalDescription(offer)
peerConnection.onicecandidate = event => {
if (event.candidate) {
console.log('Ice candidate: ', event.candidate)
}
}
... other codes
I found a solution. If you add this code after the config declaration:
var mediaConstraints = {
optional: [],
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
};
and modify the last line like this
},onError,mediaConstraints);
it just works. Don't ask me why.
The doc page should be used as the reference: https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer
const offerOptions = {
offerToReceiveAudio: true,
offerToReceiveVideo: true
}
myPeerConnection.createOffer ( offerOptions )
Related
I need to define my Service Worker as persistent in my Chrome extension because I'm using the webRequest API to intercept some data passed in a form for a specific request, but I don't know how I can do that. I've tried everything, but my Service Worker keeps unloading.
How can I keep it loaded and waiting until the request is intercepted?
Service worker (SW) can't be persistent by definition and the browser must forcibly terminate all of SW connections such as network requests or runtime ports after a certain time, which in Chrome is 5 minutes. The inactivity timer when no such requests or ports are open is even shorter: 30 seconds.
Chromium team currently considers this behavior intentional and good, however this only applies to extensions that observe infrequent events, so they'll run just a few times a day thus reducing browser memory footprint between the runs e.g. webRequest/webNavigation events with urls filter for a rarely visited site. These extensions can be reworked to maintain the state, example. Unfortunately, such an idyll is unsustainable in many cases.
Known problems
Problem 1: Chrome 106 and older doesn't wake up SW for webRequest events.
Although you can try to subscribe to an API like chrome.webNavigation as shown in the other answers, but it helps only with events that occur after the worker starts.
Problem 2: the worker randomly stops waking up for events.
The workaround may be to call chrome.runtime.reload().
Problem 3: Chrome 109 and older doesn't prolong SW lifetime for a new chrome API event in an already running background script. It means that when the event occurred in the last milliseconds of the 30-second inactivity timeout your code won't be able to run anything asynchronous reliably. It means that your extension will be perceived as unreliable by the user.
Problem 4: worse performance than MV2 in case the extension maintains a socket connection or the state (variables) takes a long time to rebuild or you observe frequent events like these:
chrome.tabs.onUpdated/onActivated,
chrome.webNavigation if not scoped to a rare url,
chrome.webRequest if not scoped to a rare url or type,
chrome.runtime.onMessage/onConnect for messages from content script in all tabs.
Starting SW for a new event is essentially like opening a new tab. Creating the environment takes ~50ms, running the entire SW script may take 100ms (or even 1000ms depending on the amount of code), reading the state from storage and rebuilding/hydrating it may take 1ms (or 1000ms depending on the complexity of data). Even with an almost empty script it'd be at least 50ms, which is quite a huge overhead to call the event listener, which takes only 1ms.
SW may restart hundreds of times a day, because such events are generated in response to user actions that have natural gaps in them e.g. clicked a tab then wrote something, during which the SW is terminated and restarted again for a new event thus wearing down CPU, disk, battery, often introducing a frequent perceivable lag of the extension's reaction.
"Persistent" service worker with offscreen API
Courtesy of Keven Augusto.
In Chrome 109 and newer you can use offscreen API to create an offscreen document and send some message from it every 30 second or less, to keep service worker running. Currently this document's lifetime is not limited (only audio playback is limited, which we don't use), but it's likely to change in the future.
manifest.json
"permissions": ["offscreen"]
offscreen.html
<script src="offscreen.js"></script>
offscreen.js
// send a message every 20 sec to service worker
setInterval(() => {
chrome.runtime.sendMessage({ keepAlive: true });
}, 20000);
background.js
// create the offscreen document if it doesn't already exist
async function createOffscreen() {
if (await chrome.offscreen.hasDocument?.()) return;
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['BLOBS'],
justification: 'keep service worker running',
});
}
chrome.runtime.onStartup.addListener(() => {
createOffscreen();
});
// a message from an offscreen document every 20 second resets the inactivity timer
chrome.runtime.onMessage.addListener(msg => {
if (msg.keepAlive) console.log('keepAlive');
});
"Persistent" service worker while nativeMessaging host is connected
In Chrome 105 and newer the service worker will run as long as it's connected to a nativeMessaging host via chrome.runtime.connectNative. If the host process is terminated due to a crash or user action, the port will be closed, and the SW will terminate as usual. You can guard against it by listening to port's onDisconnect event and call chrome.runtime.connectNative again.
"Persistent" service worker while a connectable tab is present
Downsides:
The need for an open web page tab
Broad host permissions (like <all_urls> or *://*/*) for content scripts which puts most extensions into the slow review queue in the web store.
Warning! If you already connect ports, don't use this workaround, use another one for ports below.
Warning! Also implement the workaround for sendMessage (below) if you use sendMessage.
manifest.json, the relevant part:
"permissions": ["scripting"],
"host_permissions": ["<all_urls>"],
"background": {"service_worker": "bg.js"}
background service worker bg.js:
const onUpdate = (tabId, info, tab) => /^https?:/.test(info.url) && findTab([tab]);
findTab();
chrome.runtime.onConnect.addListener(port => {
if (port.name === 'keepAlive') {
setTimeout(() => port.disconnect(), 250e3);
port.onDisconnect.addListener(() => findTab());
}
});
async function findTab(tabs) {
if (chrome.runtime.lastError) { /* tab was closed before setTimeout ran */ }
for (const {id: tabId} of tabs || await chrome.tabs.query({url: '*://*/*'})) {
try {
await chrome.scripting.executeScript({target: {tabId}, func: connect});
chrome.tabs.onUpdated.removeListener(onUpdate);
return;
} catch (e) {}
}
chrome.tabs.onUpdated.addListener(onUpdate);
}
function connect() {
chrome.runtime.connect({name: 'keepAlive'})
.onDisconnect.addListener(connect);
}
all your other extension pages like the popup or options:
;(function connect() {
chrome.runtime.connect({name: 'keepAlive'})
.onDisconnect.addListener(connect);
})();
If you also use sendMessage
In Chrome 99-101 you need to always call sendResponse() in your chrome.runtime.onMessage listener even if you don't need the response. This is a bug in MV3. Also, make sure you do it in less than 5 minutes time, otherwise call sendResponse immediately and send a new message back via chrome.tabs.sendMessage (to the tab) or chrome.runtime.sendMessage (to the popup) after the work is done.
If you already use ports e.g. chrome.runtime.connect
Warning! If you also connect more ports to the service worker you need to reconnect each one before its 5 minutes elapse e.g. in 295 seconds. This is crucial in Chrome versions before 104, which killed SW regardless of additional connected ports. In Chrome 104 and newer this bug is fixed but you'll still need to reconnect them, because their 5-minute lifetime hasn't changed, so the easiest solution is to reconnect the same way in all versions of Chrome: e.g. every 295 seconds.
background script example:
chrome.runtime.onConnect.addListener(port => {
if (port.name !== 'foo') return;
port.onMessage.addListener(onMessage);
port.onDisconnect.addListener(deleteTimer);
port._timer = setTimeout(forceReconnect, 250e3, port);
});
function onMessage(msg, port) {
console.log('received', msg, 'from', port.sender);
}
function forceReconnect(port) {
deleteTimer(port);
port.disconnect();
}
function deleteTimer(port) {
if (port._timer) {
clearTimeout(port._timer);
delete port._timer;
}
}
client script example e.g. a content script:
let port;
function connect() {
port = chrome.runtime.connect({name: 'foo'});
port.onDisconnect.addListener(connect);
port.onMessage.addListener(msg => {
console.log('received', msg, 'from bg');
});
}
connect();
"Forever", via a dedicated tab, while the tab is open
Instead of using the SW, open a new tab with an extension page inside, so this page will act as a "visible background page" i.e. the only thing the SW would do is open this tab. You can also open it from the action popup.
chrome.tabs.create({url: 'bg.html'})
It'll have the same abilities as the persistent background page of ManifestV2 but a) it's visible and b) not accessible via chrome.extension.getBackgroundPage (which can be replaced with chrome.extension.getViews).
Downsides:
consumes more memory,
wastes space in the tab strip,
distracts the user,
when multiple extensions open such a tab, the downsides snowball and become a real PITA.
You can make it a little more bearable for your users by adding info/logs/charts/dashboard to the page and also add a beforeunload listener to prevent the tab from being accidentally closed.
Caution regarding persistence
You still need to save/restore the state (variables) because there's no such thing as a persistent service worker and those workarounds have limits as described above, so the worker can terminate. You can maintain the state in a storage, example.
Note that you shouldn't make your worker persistent just to simplify state/variable management. Do it only to restore the performance worsened by restarting the worker in case your state is very expensive to rebuild or if you hook into frequent events listed in the beginning of this answer.
Future of ManifestV3
Let's hope Chromium will provide an API to control this behavior without the need to resort to such dirty hacks and pathetic workarounds. Meanwhile describe your use case in crbug.com/1152255 if it isn't already described there to help Chromium team become aware of the established fact that many extensions may need a persistent background script for an arbitrary duration of time and that at least one such extension may be installed by the majority of extension users.
unlike the chrome.webRequest API the chrome.webNavigation API works perfectly because the chrome.webNavigation API can wake up the service worker, for now you can try putting the chrome.webRequest API api inside the chrome.webNavigation.
chrome.webNavigation.onBeforeNavigate.addListener(function(){
chrome.webRequest.onResponseStarted.addListener(function(details){
//.............
//.............
},{urls: ["*://domain/*"],types: ["main_frame"]});
},{
url: [{hostContains:"domain"}]
});
If i understand correct you can wake up service worker (background.js) by alerts. Look at below example:
manifest v3
"permissions": [
"alarms"
],
service worker background.js:
chrome.alarms.create({ periodInMinutes: 4.9 })
chrome.alarms.onAlarm.addListener(() => {
console.log('log for debug')
});
Unfortunately this is not my problem and may be you have different problem too. When i refresh dev extension or stop and run prod extension some time service worker die at all. When i close and open browser worker doesn't run and any listeners inside worker doesn't run it too. It tried register worker manually. Fore example:
// override.html
<!DOCTYPE html>
<html lang="en">
<head>...<head>
<body>
...
<script defer src="override.js"></script>
<body>
<html>
// override.js - this code is running in new tab page
navigator.serviceWorker.getRegistrations().then((res) => {
for (let worker of res) {
console.log(worker)
if (worker.active.scriptURL.includes('background.js')) {
return
}
}
navigator.serviceWorker
.register(chrome.runtime.getURL('background.js'))
.then((registration) => {
console.log('Service worker success:', registration)
}).catch((error) => {
console.log('Error service:', error)
})
})
This solution partially helped me but it does not matter because i have to register worker on different tabs. May be somebody know decision. I will pleasure.
I found a different solution to keeping the extension alive. It improves on wOxxOm's answer by using a secondary extension to open the connection port to our main extension. Then both extensions try to communicate with each other in the event that any disconnects, hence keeping both alive.
The reason this was needed was that according to another team in my company, wOxxOm's answer turned out to be unreliable. Reportedly, their SW would eventually fail in an nondeterministic manner.
Then again, my solution works for my company as we are deploying enterprise security software, and we will be force installing the extensions. Having the user install 2 extensions may still be undesirable in other use-cases.
As Clairzil Bawon samdi's answer that chrome.webNavigation could wake up the service worker in MV3, here are workaround in my case:
// manifest.json
...
"background": {
"service_worker": "background.js"
},
"host_permissions": ["https://example.com/api/*"],
"permissions": ["webRequest", "webNavigation"]
...
In my case it listens onHistoryStateUpdated event to wake up the service worker:
// background.js
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
console.log('wake me up');
});
chrome.webRequest.onSendHeaders.addListener(
(details) => {
// code here
},
{
urls: ['https://example.com/api/*'],
types: ['xmlhttprequest'],
},
['requestHeaders']
);
IMHO (and direct experience) a well structured SW will work forever.
Obviously there are some particular cases, like uninterruptible connections, which may suffer a lot once SW falls asleep, but still if the code is not prepared to handle the specific behaviour.
It seems like a battle against windmills, punctually after 30 seconds SW stops doing anything, falls asleep, several events are not honored anymore and the problems start... if our SW has nothing else pressing to think about.
From "The art of War" (Sun Tzu): if you can't fight it, make friends with it.
so... ok, lets try to give something consistent to think about from time to time to our SW and put a "patch" (because this IS A PATCH!) to this issue.
Obviously I don't assure this solution will work for all of you, but it worked for me in the past, before I decided to review the whole logic and code of my SW.
So I decided to share it for your own tests.
This doesn't require any special permission in manifest V3.
Remember to call the StayAlive() function below at SW start.
To perform reliable tests remember to not open any DevTools page. Use chrome://serviceworker-internals instead and find the log (Scope) of your extension ID.
EDIT:
Since the logic of the code may not be clear to some, I will try to explain it to dispel doubts:
Any extension's SW can attempt to make a connection and send messages through a named port and, if something fails, generate an error.
The code below connects to a named port and tries to send a message through it to a nonexistent listener (so it will generate errors).
While doing this, SW is active and running (it has something to do, that is, it has to send a message through a port).
Because noone is listening, it generates a (catched and logged) error (in onDisconnect) and terminates (normal behaviour happening in whatever code).
But after 25 secs it does the same iter from start, thus keeping SW active forever.
It works fine. It is a simple trick to keep the service worker active.
// Forcing service worker to stay alive by sending a "ping" to a port where noone is listening
// Essentially it prevents SW to fall asleep after the first 30 secs of work.
const INTERNAL_STAYALIVE_PORT = "Whatever_Port_Name_You_Want"
var alivePort = null;
...
StayAlive();
...
async function StayAlive() {
var lastCall = Date.now();
var wakeup = setInterval( () => {
const now = Date.now();
const age = now - lastCall;
console.log(`(DEBUG StayAlive) ----------------------- time elapsed: ${age}`)
if (alivePort == null) {
alivePort = chrome.runtime.connect({name:INTERNAL_STAYALIVE_PORT})
alivePort.onDisconnect.addListener( (p) => {
if (chrome.runtime.lastError){
console.log(`(DEBUG StayAlive) Disconnected due to an error: ${chrome.runtime.lastError.message}`);
} else {
console.log(`(DEBUG StayAlive): port disconnected`);
}
alivePort = null;
});
}
if (alivePort) {
alivePort.postMessage({content: "ping"});
if (chrome.runtime.lastError) {
console.log(`(DEBUG StayAlive): postMessage error: ${chrome.runtime.lastError.message}`)
} else {
console.log(`(DEBUG StayAlive): "ping" sent through ${alivePort.name} port`)
}
}
//lastCall = Date.now();
}, 25000);
}
Hoping this will help someone.
Anyway, I still recommend, where possible, to review the logic and the code of your SW, because, as I mentioned at the beginning of this post, any well structured SW will work perfectly in MV3 even without tricks like this one.
EDIT (jan 17, 2023)
when you think you've hit bottom, watch out for the trapdoor that might suddenly open under your feet.
Sun Tzu
This revision of the StayAlive() function above still keeps the service worker active, but avoids calling the function every 25 seconds, so as not to burden it with unnecessary work.
In practice, it appears that by running the Highlander() function below at predefined intervals, the service worker will still live forever.
How it works
The first call of Highlander() is executed before the expiration of the fateful 30 seconds (here it is executed after 4 seconds from the start of the service worker).
Subsequent calls are performed before the expiration of the fateful 5 minutes (here they are executed every 270 seconds).
The service worker, in this way, will never go to sleep and will always respond to all events.
It thus appears that, per Chromium design, after the first Highlander() call within the first 30 seconds, the internal logic that manages the life of the (MV3) service worker extends the period of full activity until the next 5 minutes.
This is really really hilarious...
anyway... this is the ServiceWorker.js I used for my tests.
// -----------------
// SERVICEWORKER.JS
// -----------------
const INTERNAL_STAYALIVE_PORT = "CT_Internal_port_alive"
var alivePort = null;
const SECONDS = 1000;
var lastCall = Date.now();
var isFirstStart = true;
var timer = 4*SECONDS;
// -------------------------------------------------------
var wakeup = setInterval(Highlander, timer);
// -------------------------------------------------------
async function Highlander() {
const now = Date.now();
const age = now - lastCall;
console.log(`(DEBUG Highlander) ------------- time elapsed from first start: ${convertNoDate(age)}`)
if (alivePort == null) {
alivePort = chrome.runtime.connect({name:INTERNAL_STAYALIVE_PORT})
alivePort.onDisconnect.addListener( (p) => {
if (chrome.runtime.lastError){
console.log(`(DEBUG Highlander) Expected disconnect (on error). SW should be still running.`);
} else {
console.log(`(DEBUG Highlander): port disconnected`);
}
alivePort = null;
});
}
if (alivePort) {
alivePort.postMessage({content: "ping"});
if (chrome.runtime.lastError) {
console.log(`(DEBUG Highlander): postMessage error: ${chrome.runtime.lastError.message}`)
} else {
console.log(`(DEBUG Highlander): "ping" sent through ${alivePort.name} port`)
}
}
//lastCall = Date.now();
if (isFirstStart) {
isFirstStart = false;
clearInterval(wakeup);
timer = 270*SECONDS;
wakeup = setInterval(Highlander, timer);
}
}
function convertNoDate(long) {
var dt = new Date(long).toISOString()
return dt.slice(-13, -5) // HH:MM:SS only
}
EDIT (jan 20, 2023):
On Github, I created a repository for a practical example of how to properly use the Highlander function in a real world extension. For the implementation of this repo, I also took into account wOxxOm's comments to my post (many thanks to him).
Still on Github, I created another repository to demonstrate in another real world extension how a service worker can immediately start by itself (put itself in RUNNING status), without the aid of external content scripts, and how it can live on forever using the usual Highlander function.
This repository includes a local WebSocket Echo Test server used by the extension in its client communication sample and useful to externally debug the extension when the extension's host browser has been closed. That's right, because, depending on the type of configuration applied, when the host browser is closed Highlander-DNA can either shut down with the browser or continue to live forever, with all functionality connected and managed (e.g. the included WebSocket client/server communications test sample).
EDIT (jan 22, 2023)
I tested memory and CPU consumption while a Service Worker is always in RUNNING state due to the use of Highlander. The consumption to keep it running all the time is practically ZERO. I really don't understand why the Chromium team is persisting in wanting to unnecessarily complicate everyone's life.
WebSocket callbacks registered from within the chrome.runtime listener registrations of my extensions's service worker would not get invoked, which sounds like almost the same problem.
I approached this problem by making sure that my service worker never ends, by adding the following code to it:
function keepServiceRunning() {
setTimeout(keepServiceRunning, 2000);
}
keepServiceRunning()
After this, my callbacks now get invoked as expected.
I need to define my Service Worker as persistent in my Chrome extension because I'm using the webRequest API to intercept some data passed in a form for a specific request, but I don't know how I can do that. I've tried everything, but my Service Worker keeps unloading.
How can I keep it loaded and waiting until the request is intercepted?
Service worker (SW) can't be persistent by definition and the browser must forcibly terminate all of SW connections such as network requests or runtime ports after a certain time, which in Chrome is 5 minutes. The inactivity timer when no such requests or ports are open is even shorter: 30 seconds.
Chromium team currently considers this behavior intentional and good, however this only applies to extensions that observe infrequent events, so they'll run just a few times a day thus reducing browser memory footprint between the runs e.g. webRequest/webNavigation events with urls filter for a rarely visited site. These extensions can be reworked to maintain the state, example. Unfortunately, such an idyll is unsustainable in many cases.
Known problems
Problem 1: Chrome 106 and older doesn't wake up SW for webRequest events.
Although you can try to subscribe to an API like chrome.webNavigation as shown in the other answers, but it helps only with events that occur after the worker starts.
Problem 2: the worker randomly stops waking up for events.
The workaround may be to call chrome.runtime.reload().
Problem 3: Chrome 109 and older doesn't prolong SW lifetime for a new chrome API event in an already running background script. It means that when the event occurred in the last milliseconds of the 30-second inactivity timeout your code won't be able to run anything asynchronous reliably. It means that your extension will be perceived as unreliable by the user.
Problem 4: worse performance than MV2 in case the extension maintains a socket connection or the state (variables) takes a long time to rebuild or you observe frequent events like these:
chrome.tabs.onUpdated/onActivated,
chrome.webNavigation if not scoped to a rare url,
chrome.webRequest if not scoped to a rare url or type,
chrome.runtime.onMessage/onConnect for messages from content script in all tabs.
Starting SW for a new event is essentially like opening a new tab. Creating the environment takes ~50ms, running the entire SW script may take 100ms (or even 1000ms depending on the amount of code), reading the state from storage and rebuilding/hydrating it may take 1ms (or 1000ms depending on the complexity of data). Even with an almost empty script it'd be at least 50ms, which is quite a huge overhead to call the event listener, which takes only 1ms.
SW may restart hundreds of times a day, because such events are generated in response to user actions that have natural gaps in them e.g. clicked a tab then wrote something, during which the SW is terminated and restarted again for a new event thus wearing down CPU, disk, battery, often introducing a frequent perceivable lag of the extension's reaction.
"Persistent" service worker with offscreen API
Courtesy of Keven Augusto.
In Chrome 109 and newer you can use offscreen API to create an offscreen document and send some message from it every 30 second or less, to keep service worker running. Currently this document's lifetime is not limited (only audio playback is limited, which we don't use), but it's likely to change in the future.
manifest.json
"permissions": ["offscreen"]
offscreen.html
<script src="offscreen.js"></script>
offscreen.js
// send a message every 20 sec to service worker
setInterval(() => {
chrome.runtime.sendMessage({ keepAlive: true });
}, 20000);
background.js
// create the offscreen document if it doesn't already exist
async function createOffscreen() {
if (await chrome.offscreen.hasDocument?.()) return;
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['BLOBS'],
justification: 'keep service worker running',
});
}
chrome.runtime.onStartup.addListener(() => {
createOffscreen();
});
// a message from an offscreen document every 20 second resets the inactivity timer
chrome.runtime.onMessage.addListener(msg => {
if (msg.keepAlive) console.log('keepAlive');
});
"Persistent" service worker while nativeMessaging host is connected
In Chrome 105 and newer the service worker will run as long as it's connected to a nativeMessaging host via chrome.runtime.connectNative. If the host process is terminated due to a crash or user action, the port will be closed, and the SW will terminate as usual. You can guard against it by listening to port's onDisconnect event and call chrome.runtime.connectNative again.
"Persistent" service worker while a connectable tab is present
Downsides:
The need for an open web page tab
Broad host permissions (like <all_urls> or *://*/*) for content scripts which puts most extensions into the slow review queue in the web store.
Warning! If you already connect ports, don't use this workaround, use another one for ports below.
Warning! Also implement the workaround for sendMessage (below) if you use sendMessage.
manifest.json, the relevant part:
"permissions": ["scripting"],
"host_permissions": ["<all_urls>"],
"background": {"service_worker": "bg.js"}
background service worker bg.js:
const onUpdate = (tabId, info, tab) => /^https?:/.test(info.url) && findTab([tab]);
findTab();
chrome.runtime.onConnect.addListener(port => {
if (port.name === 'keepAlive') {
setTimeout(() => port.disconnect(), 250e3);
port.onDisconnect.addListener(() => findTab());
}
});
async function findTab(tabs) {
if (chrome.runtime.lastError) { /* tab was closed before setTimeout ran */ }
for (const {id: tabId} of tabs || await chrome.tabs.query({url: '*://*/*'})) {
try {
await chrome.scripting.executeScript({target: {tabId}, func: connect});
chrome.tabs.onUpdated.removeListener(onUpdate);
return;
} catch (e) {}
}
chrome.tabs.onUpdated.addListener(onUpdate);
}
function connect() {
chrome.runtime.connect({name: 'keepAlive'})
.onDisconnect.addListener(connect);
}
all your other extension pages like the popup or options:
;(function connect() {
chrome.runtime.connect({name: 'keepAlive'})
.onDisconnect.addListener(connect);
})();
If you also use sendMessage
In Chrome 99-101 you need to always call sendResponse() in your chrome.runtime.onMessage listener even if you don't need the response. This is a bug in MV3. Also, make sure you do it in less than 5 minutes time, otherwise call sendResponse immediately and send a new message back via chrome.tabs.sendMessage (to the tab) or chrome.runtime.sendMessage (to the popup) after the work is done.
If you already use ports e.g. chrome.runtime.connect
Warning! If you also connect more ports to the service worker you need to reconnect each one before its 5 minutes elapse e.g. in 295 seconds. This is crucial in Chrome versions before 104, which killed SW regardless of additional connected ports. In Chrome 104 and newer this bug is fixed but you'll still need to reconnect them, because their 5-minute lifetime hasn't changed, so the easiest solution is to reconnect the same way in all versions of Chrome: e.g. every 295 seconds.
background script example:
chrome.runtime.onConnect.addListener(port => {
if (port.name !== 'foo') return;
port.onMessage.addListener(onMessage);
port.onDisconnect.addListener(deleteTimer);
port._timer = setTimeout(forceReconnect, 250e3, port);
});
function onMessage(msg, port) {
console.log('received', msg, 'from', port.sender);
}
function forceReconnect(port) {
deleteTimer(port);
port.disconnect();
}
function deleteTimer(port) {
if (port._timer) {
clearTimeout(port._timer);
delete port._timer;
}
}
client script example e.g. a content script:
let port;
function connect() {
port = chrome.runtime.connect({name: 'foo'});
port.onDisconnect.addListener(connect);
port.onMessage.addListener(msg => {
console.log('received', msg, 'from bg');
});
}
connect();
"Forever", via a dedicated tab, while the tab is open
Instead of using the SW, open a new tab with an extension page inside, so this page will act as a "visible background page" i.e. the only thing the SW would do is open this tab. You can also open it from the action popup.
chrome.tabs.create({url: 'bg.html'})
It'll have the same abilities as the persistent background page of ManifestV2 but a) it's visible and b) not accessible via chrome.extension.getBackgroundPage (which can be replaced with chrome.extension.getViews).
Downsides:
consumes more memory,
wastes space in the tab strip,
distracts the user,
when multiple extensions open such a tab, the downsides snowball and become a real PITA.
You can make it a little more bearable for your users by adding info/logs/charts/dashboard to the page and also add a beforeunload listener to prevent the tab from being accidentally closed.
Caution regarding persistence
You still need to save/restore the state (variables) because there's no such thing as a persistent service worker and those workarounds have limits as described above, so the worker can terminate. You can maintain the state in a storage, example.
Note that you shouldn't make your worker persistent just to simplify state/variable management. Do it only to restore the performance worsened by restarting the worker in case your state is very expensive to rebuild or if you hook into frequent events listed in the beginning of this answer.
Future of ManifestV3
Let's hope Chromium will provide an API to control this behavior without the need to resort to such dirty hacks and pathetic workarounds. Meanwhile describe your use case in crbug.com/1152255 if it isn't already described there to help Chromium team become aware of the established fact that many extensions may need a persistent background script for an arbitrary duration of time and that at least one such extension may be installed by the majority of extension users.
unlike the chrome.webRequest API the chrome.webNavigation API works perfectly because the chrome.webNavigation API can wake up the service worker, for now you can try putting the chrome.webRequest API api inside the chrome.webNavigation.
chrome.webNavigation.onBeforeNavigate.addListener(function(){
chrome.webRequest.onResponseStarted.addListener(function(details){
//.............
//.............
},{urls: ["*://domain/*"],types: ["main_frame"]});
},{
url: [{hostContains:"domain"}]
});
If i understand correct you can wake up service worker (background.js) by alerts. Look at below example:
manifest v3
"permissions": [
"alarms"
],
service worker background.js:
chrome.alarms.create({ periodInMinutes: 4.9 })
chrome.alarms.onAlarm.addListener(() => {
console.log('log for debug')
});
Unfortunately this is not my problem and may be you have different problem too. When i refresh dev extension or stop and run prod extension some time service worker die at all. When i close and open browser worker doesn't run and any listeners inside worker doesn't run it too. It tried register worker manually. Fore example:
// override.html
<!DOCTYPE html>
<html lang="en">
<head>...<head>
<body>
...
<script defer src="override.js"></script>
<body>
<html>
// override.js - this code is running in new tab page
navigator.serviceWorker.getRegistrations().then((res) => {
for (let worker of res) {
console.log(worker)
if (worker.active.scriptURL.includes('background.js')) {
return
}
}
navigator.serviceWorker
.register(chrome.runtime.getURL('background.js'))
.then((registration) => {
console.log('Service worker success:', registration)
}).catch((error) => {
console.log('Error service:', error)
})
})
This solution partially helped me but it does not matter because i have to register worker on different tabs. May be somebody know decision. I will pleasure.
I found a different solution to keeping the extension alive. It improves on wOxxOm's answer by using a secondary extension to open the connection port to our main extension. Then both extensions try to communicate with each other in the event that any disconnects, hence keeping both alive.
The reason this was needed was that according to another team in my company, wOxxOm's answer turned out to be unreliable. Reportedly, their SW would eventually fail in an nondeterministic manner.
Then again, my solution works for my company as we are deploying enterprise security software, and we will be force installing the extensions. Having the user install 2 extensions may still be undesirable in other use-cases.
As Clairzil Bawon samdi's answer that chrome.webNavigation could wake up the service worker in MV3, here are workaround in my case:
// manifest.json
...
"background": {
"service_worker": "background.js"
},
"host_permissions": ["https://example.com/api/*"],
"permissions": ["webRequest", "webNavigation"]
...
In my case it listens onHistoryStateUpdated event to wake up the service worker:
// background.js
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
console.log('wake me up');
});
chrome.webRequest.onSendHeaders.addListener(
(details) => {
// code here
},
{
urls: ['https://example.com/api/*'],
types: ['xmlhttprequest'],
},
['requestHeaders']
);
IMHO (and direct experience) a well structured SW will work forever.
Obviously there are some particular cases, like uninterruptible connections, which may suffer a lot once SW falls asleep, but still if the code is not prepared to handle the specific behaviour.
It seems like a battle against windmills, punctually after 30 seconds SW stops doing anything, falls asleep, several events are not honored anymore and the problems start... if our SW has nothing else pressing to think about.
From "The art of War" (Sun Tzu): if you can't fight it, make friends with it.
so... ok, lets try to give something consistent to think about from time to time to our SW and put a "patch" (because this IS A PATCH!) to this issue.
Obviously I don't assure this solution will work for all of you, but it worked for me in the past, before I decided to review the whole logic and code of my SW.
So I decided to share it for your own tests.
This doesn't require any special permission in manifest V3.
Remember to call the StayAlive() function below at SW start.
To perform reliable tests remember to not open any DevTools page. Use chrome://serviceworker-internals instead and find the log (Scope) of your extension ID.
EDIT:
Since the logic of the code may not be clear to some, I will try to explain it to dispel doubts:
Any extension's SW can attempt to make a connection and send messages through a named port and, if something fails, generate an error.
The code below connects to a named port and tries to send a message through it to a nonexistent listener (so it will generate errors).
While doing this, SW is active and running (it has something to do, that is, it has to send a message through a port).
Because noone is listening, it generates a (catched and logged) error (in onDisconnect) and terminates (normal behaviour happening in whatever code).
But after 25 secs it does the same iter from start, thus keeping SW active forever.
It works fine. It is a simple trick to keep the service worker active.
// Forcing service worker to stay alive by sending a "ping" to a port where noone is listening
// Essentially it prevents SW to fall asleep after the first 30 secs of work.
const INTERNAL_STAYALIVE_PORT = "Whatever_Port_Name_You_Want"
var alivePort = null;
...
StayAlive();
...
async function StayAlive() {
var lastCall = Date.now();
var wakeup = setInterval( () => {
const now = Date.now();
const age = now - lastCall;
console.log(`(DEBUG StayAlive) ----------------------- time elapsed: ${age}`)
if (alivePort == null) {
alivePort = chrome.runtime.connect({name:INTERNAL_STAYALIVE_PORT})
alivePort.onDisconnect.addListener( (p) => {
if (chrome.runtime.lastError){
console.log(`(DEBUG StayAlive) Disconnected due to an error: ${chrome.runtime.lastError.message}`);
} else {
console.log(`(DEBUG StayAlive): port disconnected`);
}
alivePort = null;
});
}
if (alivePort) {
alivePort.postMessage({content: "ping"});
if (chrome.runtime.lastError) {
console.log(`(DEBUG StayAlive): postMessage error: ${chrome.runtime.lastError.message}`)
} else {
console.log(`(DEBUG StayAlive): "ping" sent through ${alivePort.name} port`)
}
}
//lastCall = Date.now();
}, 25000);
}
Hoping this will help someone.
Anyway, I still recommend, where possible, to review the logic and the code of your SW, because, as I mentioned at the beginning of this post, any well structured SW will work perfectly in MV3 even without tricks like this one.
EDIT (jan 17, 2023)
when you think you've hit bottom, watch out for the trapdoor that might suddenly open under your feet.
Sun Tzu
This revision of the StayAlive() function above still keeps the service worker active, but avoids calling the function every 25 seconds, so as not to burden it with unnecessary work.
In practice, it appears that by running the Highlander() function below at predefined intervals, the service worker will still live forever.
How it works
The first call of Highlander() is executed before the expiration of the fateful 30 seconds (here it is executed after 4 seconds from the start of the service worker).
Subsequent calls are performed before the expiration of the fateful 5 minutes (here they are executed every 270 seconds).
The service worker, in this way, will never go to sleep and will always respond to all events.
It thus appears that, per Chromium design, after the first Highlander() call within the first 30 seconds, the internal logic that manages the life of the (MV3) service worker extends the period of full activity until the next 5 minutes.
This is really really hilarious...
anyway... this is the ServiceWorker.js I used for my tests.
// -----------------
// SERVICEWORKER.JS
// -----------------
const INTERNAL_STAYALIVE_PORT = "CT_Internal_port_alive"
var alivePort = null;
const SECONDS = 1000;
var lastCall = Date.now();
var isFirstStart = true;
var timer = 4*SECONDS;
// -------------------------------------------------------
var wakeup = setInterval(Highlander, timer);
// -------------------------------------------------------
async function Highlander() {
const now = Date.now();
const age = now - lastCall;
console.log(`(DEBUG Highlander) ------------- time elapsed from first start: ${convertNoDate(age)}`)
if (alivePort == null) {
alivePort = chrome.runtime.connect({name:INTERNAL_STAYALIVE_PORT})
alivePort.onDisconnect.addListener( (p) => {
if (chrome.runtime.lastError){
console.log(`(DEBUG Highlander) Expected disconnect (on error). SW should be still running.`);
} else {
console.log(`(DEBUG Highlander): port disconnected`);
}
alivePort = null;
});
}
if (alivePort) {
alivePort.postMessage({content: "ping"});
if (chrome.runtime.lastError) {
console.log(`(DEBUG Highlander): postMessage error: ${chrome.runtime.lastError.message}`)
} else {
console.log(`(DEBUG Highlander): "ping" sent through ${alivePort.name} port`)
}
}
//lastCall = Date.now();
if (isFirstStart) {
isFirstStart = false;
clearInterval(wakeup);
timer = 270*SECONDS;
wakeup = setInterval(Highlander, timer);
}
}
function convertNoDate(long) {
var dt = new Date(long).toISOString()
return dt.slice(-13, -5) // HH:MM:SS only
}
EDIT (jan 20, 2023):
On Github, I created a repository for a practical example of how to properly use the Highlander function in a real world extension. For the implementation of this repo, I also took into account wOxxOm's comments to my post (many thanks to him).
Still on Github, I created another repository to demonstrate in another real world extension how a service worker can immediately start by itself (put itself in RUNNING status), without the aid of external content scripts, and how it can live on forever using the usual Highlander function.
This repository includes a local WebSocket Echo Test server used by the extension in its client communication sample and useful to externally debug the extension when the extension's host browser has been closed. That's right, because, depending on the type of configuration applied, when the host browser is closed Highlander-DNA can either shut down with the browser or continue to live forever, with all functionality connected and managed (e.g. the included WebSocket client/server communications test sample).
EDIT (jan 22, 2023)
I tested memory and CPU consumption while a Service Worker is always in RUNNING state due to the use of Highlander. The consumption to keep it running all the time is practically ZERO. I really don't understand why the Chromium team is persisting in wanting to unnecessarily complicate everyone's life.
WebSocket callbacks registered from within the chrome.runtime listener registrations of my extensions's service worker would not get invoked, which sounds like almost the same problem.
I approached this problem by making sure that my service worker never ends, by adding the following code to it:
function keepServiceRunning() {
setTimeout(keepServiceRunning, 2000);
}
keepServiceRunning()
After this, my callbacks now get invoked as expected.
I want to capture video with the webcamera.
And there is the right decision:
window.onload = function () {
var video = document.getElementById('video');
var videoStreamUrl = false;
navigator.getUserMedia({video: true}, function (stream) {
videoStreamUrl = window.URL.createObjectURL(stream);
video.src = videoStreamUrl;
}, function () {
console.log('error');
});
};
but produces an error in the browser:
[Deprecation] URL.createObjectURL with media streams is deprecated and will be removed in M68, around July 2018. Please use HTMLMediaElement.srcObject instead. See https://www.chromestatus.com/features/5618491470118912 for more details.
how to use HTMLMediaElement.srcObject for my purposes ? Thanks for your time!
MediaElement.srcObject should allow Blobs, MediaSources and MediaStreams to be played in the MediaElement without the need to bind these sources in the memory for the lifetime of the document like blobURIs do.
(Currently no browser support anything else than MediaStream though...)
Indeed, when you do URL.createObjectURL(MediaStream), you are telling the browser that it should keep alive this Source until your revoke the blobURI, or until the document dies.
In the case of a LocalMediaStream served from a capturing device (camera or microphone), this also means that the browser has to keep the connection to this device open.
Firefox initiated the deprecation of this feature, one year or so ago, since srcObject can provide the same result in better ways, easier to handle for everyone, and hence Chrome seems to finally follow (not sure what's the specs status about this).
So to use it, simply do
MediaElement.srcObject = MediaStream;
Also note that the API you are using is itself deprecated (and not only in FF), and you shouldn't use it anymore. Indeed, the correct API to capture MediaStreams from user Media is the MediaDevices.getUserMedia one.
This API now returns a Promise which gets resolved to the MediaStream.
So a complete correction of your code would be
var video = document.getElementById('video');
navigator.mediaDevices.getUserMedia({
video: true
})
.then(function(stream) {
video.srcObject = stream;
})
.catch(function(error) {
console.log('error', error);
});
<video id="video"></video>
Or as a fiddle since StackSnippetsĀ® overprotected iframe may not deal well with gUM.
I'm trying to share my screen with Kurento WebRtc server. But getting this error:
NavigatorUserMediaError {name: "ScreenCaptureError", message: "", constraintName: ""}
There is no errors in Firefox with same code.
Constraints using for webrtc:
var constraints = {
audio: true,
video: {
mandatory : {
chromeMediaSource: 'screen',
maxWidth: 1920,
maxHeight: 1080,
maxFrameRate: 30,
minFrameRate: 15,
minAspectRatio: 1.6
},
optional: []
}
}
var options = {
localVideo : video,
onicecandidate : onIceCandidate,
mediaConstraints : constraints
}
webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options,function(error) {
if (error) {
return console.error(error);
}
webRtcPeer.generateOffer(onOfferPresenter);
});
How do I share my screen using chrome and kurento?
Sharing a screen with Kurento through WebRTC, is exactly the same as sharing the webcam: get the stream from the client and negotiate the endpoint. The tricky part when doing screenshare is to get the stream. The kurento-utils-js library will give you a little help on that, as you can create the WebRtcPeer object, in the client, indicating that you want to share your screen or a window. You just need to make sure that you
have an extension installed to do screen-sharing in Chrome. In FF, it's enough to add the domain to the whitelist. Check this extension.
pass a valid sendSource value (screen or window) in the options bag when creating the kurentoUtils.WebRtcPeer object
have a getScreenConstraints method in your window object, as it will be used here. getScreenConstraints should return a valid set of constraints, depending on the browser. YOu can check an implementation of that function here
I think that should be enough. We are doing screen sharing with the library, using our own getScreenConstrains and extension, and it works fine. Once you have that, doing screen sharing with the kurento-utils-js library is quite easy. Just need to pass the sendSource value when creating the peer like so
var constraints = {
audio: false,
video: true
}
var options = {
localVideo: videoInput, //if you want to see what you are sharing
onicecandidate: onIceCandidate,
mediaConstraints: constraints,
sendSource: 'screen'
}
webRtcPeerScreencast = kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options, function(error) {
if (error) return onError(error) //You'll need to use whatever you use for handling errors
this.generateOffer(onOffer)
});
The value of sendSource is a string, and it depends on what you want to share
'screen': will let you share the whole screen. If you have more than one, you can choose which one to share
'window': lets you choose between all open windows
[ 'screen', 'window' ]: WARNING! Only accepted by Chrome, this will let the user choose between full screens or windows.
'webcam': this is the default value of you don't specify anything here. Guess what'll happen ;-)
Mozzila has sample code for using nsIWebBrowserPersist saveURI to download files from a Firefox extension.
However, it doesn't say how to monitor if the download stops and needs to be restarted again.
Is there some way to check if the download stops and restart the download again? If I can't do it with nsIWebBrowserPersist, can I use something else?
You need to implement and nsIWebProgressListener and .progressListener.
Your listener must implement all nsIWebProgressListener methods, but you can just stub out most of them. The important one (for this question) is onStateChange, where you can check for an error result which will indicate network errors, and, as the documentation states, also check for the server returning http error:
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
var mylistener = {
QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIWebProgressListener]),
...
onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
if (!(aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP)) {
// not yet done.
}
aWebProgress.progressListener = null; // reset to avoid circular references -> leaks
if (!Components.isSuccessCode(aStatus)) {
// Some network or file related error happened.
}
if (aRequest instanceof Components.interfaces.nsIHttpChannel && aRequest.responseStatus >= 400) {
// Some http related error happened.
}
}
};
Downloads.jsm supports resuming downloads. Set tryToKeepPartialData on your Downloads object.