I am trying to create an offscreen document that plays an audio for my chrome extension. My background.js
script receives a message that triggers the initiation of the offscreen document, which contains code to run an audio file uploaded by the user. Every time I run it, I get this in the service worker console:
Uncaught (in promise) Error: Could not establish connection. Receiving end does not exist.
And upon reloading the page, I get:
Error creating offscreen document: Error: Only a single offscreen document may be created.
Here is my code:
manifest.json:
{
"manifest_version": 3,
"name": "Custom",
"description": "",
"version": "1.0",
"action": {
"default_popup": "templates/popup.html",
"default_icon": "images/logo.png"
},
"background": {
"service_worker": "scripts/background.js"
},
"icons": {
"16": "images/logo.png",
"32": "images/logo.png",
"48": "images/logo.png",
"128": "images/logo.png"
},
"permissions": [
"storage",
"activeTab",
"scripting",
"offscreen"
],
"host_permissions": [
"https://www.netflix.com/watch/*"
],
"content_scripts": [
{
"matches": [
"https://www.netflix.com/watch/*"
],
"js": [
"scripts/content.js"
]
}
]
}
content.js:
// Try to find the video element immediately
if (!checkForVideoElement()) {
// If not found, use a mutation observer to watch for changes
const observer = new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
if (checkForVideoElement()) {
// Stop observing once the video element is found
observer.disconnect();
break;
}
}
}
});
// Start observing the document body for added nodes
observer.observe(document.body, { childList: true, subtree: true });
}
function checkForVideoElement() {
var video = document.querySelector("video");
if (video) {
console.log(video);
if (video.currentTime <= 60) {
console.log("Video found less than 60s");
// asking background.js to create an offscreen document and play the sound
chrome.runtime.sendMessage("runOffscreenTask");
}
return true;
}
return false;
}
background.js:
// Fix for crbug.com/1185241
if (typeof chrome.runtime.onMessage !== "undefined") {
const { onMessage } = chrome.runtime;
const { addListener } = onMessage;
onMessage.addListener = fn => addListener.call(onMessage, (msg, sender, respond) => {
const res = fn(msg, sender, respond);
if (res instanceof Promise) return !!res.then(respond, console.error);
if (res !== undefined) respond(res);
});
}
// Re-inject content scripts on extension install/update
chrome.runtime.onInstalled.addListener(async () => {
const contentScripts = chrome.runtime.getManifest().content_scripts;
for (const cs of contentScripts) {
const tabs = await chrome.tabs.query({ url: cs.matches });
for (const tab of tabs) {
chrome.scripting.executeScript({
target: { tabId: tab.id, allFrames: cs.all_frames },
files: cs.js,
injectImmediately: cs.run_at === 'document_start',
});
}
}
});
// Tab update listener
chrome.tabs.onUpdated.addListener(function (tabId, info, tab) {
if (info.status === "complete" && tab.url.indexOf("netflix.com/watch/") !== -1) {
chrome.tabs.update(tabId, { muted: true });
chrome.scripting.executeScript({
target: { tabId: tabId },
files: ['scripts/content.js']
}, () => {
console.log("Content script injected successfully");
});
chrome.storage.local.get("soundDuration", function (result) {
let duration = result.soundDuration || 0;
console.log("Duration of the sound: " + duration + "ms");
setTimeout(() => {
chrome.tabs.update(tabId, { muted: false });
}, (8000 - duration));
});
}
});
// Message listener for offscreen task
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message === "runOffscreenTask") {
offScreenTask();
return true;
}
});
async function offScreenTask() {
await setupOffscreenDocument('offscreen.html');
chrome.runtime.sendMessage("playSound");
return true;
}
let creating; // A global promise to avoid concurrency issues
async function setupOffscreenDocument() {
const offscreenUrl = chrome.runtime.getURL("offscreen.html");
const existingContexts = await chrome.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT'],
documentUrls: [offscreenUrl]
});
if (existingContexts.length > 0) {
return;
}
if (creating) {
await creating;
} else {
creating = chrome.offscreen.createDocument({
url: offscreenUrl,
reasons: ["AUDIO_PLAYBACK"],
justification: "Playing Custom Sound",
});
await creating;
creating = null;
}
}
offscreen.js:
console.log("Offscreen script loaded");
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log("Offscreen script received message:", message);
if ('crbug.com/1185241') { // replace with a check for Chrome version that fixes the bug
const { onMessage } = chrome.runtime, { addListener } = onMessage;
onMessage.addListener = fn => addListener.call(onMessage, (msg, sender, respond) => {
const res = fn(msg, sender, respond);
if (res instanceof Promise) return !!res.then(respond, console.error);
if (res !== undefined) respond(res);
});
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log("Offscreen script received message:", message);
if (message.action === "playSound") {
playSound();
}
});
function playSound() {
chrome.storage.local.get("Sound", function (result) {
let audio = result.Sound;
if (audio) {
console.log("Audio found");
// Convert the data URI to a Blob
fetch(audio)
.then(response => response.blob())
.then(blob => {
const audioUrl = URL.createObjectURL(blob);
const audioElement = new Audio(audioUrl);
audioElement.play().then(() => {
console.log("Audio played successfully");
}).catch(error => {
console.error("Error playing audio:", error);
});
})
.catch(error => {
console.error("Error converting audio data:", error);
});
} else {
console.log("Audio not found");
}
});
}
I am not sure how to fix the “receiving end” function, and I am confused why the “only one offscreen” function is ocurring even though I believe I have correctly implemetned a setupOffscreenDocument()
function in my background.js
too.
I have already looked at this answer on re-injecting the content script and this answer on separating async requests from message listeners and tried my best to implement all the suggestions, but my code still doesn’t work. I am very new at this, so I may have gone very obviously wrong in quite a few places!