-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add offscreen user media sample #1056
Open
aakash232
wants to merge
2
commits into
GoogleChrome:main
Choose a base branch
from
aakash232:offscreen-user-media-sample
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
34 changes: 34 additions & 0 deletions
34
functional-samples/cookbook.offscreen-user-media/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
This recipe shows how to use User Media in an Extension Service Worker using the [Offscreen document][1]. | ||
|
||
## Context | ||
|
||
- The extension aims to capture audio from the user in the context of extension (globally) via a media recorder in the offscreen document. | ||
- Service worker no longer have access to window objects and APIs. Hence, it was difficult for the extension to fetch permissions and capture audio. | ||
- Offscreen document handles permission checks, audio devices management and recording using navigator API and media recorder respectively. | ||
|
||
## Steps | ||
|
||
1. User presses START/STOP recording from extension popup | ||
2. Popup sends message to background service worker. | ||
3. If STOP, Service worker sends message to offscreen to stop mediarecorder. | ||
4. If START, Service worker sends message to the active tab's content script to intiate recording process. | ||
5. The content script sends message to offscreen to check audio permissions. | ||
- If GRANTED, send message to offscreen to start media recorder. | ||
- If DENIED, show alert on window | ||
- If PROMPT, | ||
- inject an IFrame to request permission from the user. | ||
- Listen to the user'e response on the iFrame | ||
- If allowed, move to GRANTED step. Else, DENIED. | ||
|
||
## Running this extension | ||
|
||
1. Clone this repository. | ||
2. Load this directory in Chrome as an [unpacked extension][2]. | ||
3. Open the Extension menu and click the extension named "Offscreen API - User media". | ||
|
||
Click on the extension popup for START and STOP recording buttons. | ||
|
||
Inspect the offscreen html page to view logs from media recorder and audio chunk management. | ||
|
||
[1]: https://developer.chrome.com/docs/extensions/reference/offscreen/ | ||
[2]: https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked |
133 changes: 133 additions & 0 deletions
133
functional-samples/cookbook.offscreen-user-media/background.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
/** | ||
* Path to the offscreen HTML document. | ||
* @type {string} | ||
*/ | ||
const OFFSCREEN_DOCUMENT_PATH = 'offscreen/offscreen.html'; | ||
|
||
/** | ||
* Reason for creating the offscreen document. | ||
* @type {string} | ||
*/ | ||
const OFFSCREEN_REASON = 'USER_MEDIA'; | ||
|
||
/** | ||
* Listener for extension installation. | ||
*/ | ||
chrome.runtime.onInstalled.addListener(handleInstall); | ||
|
||
/** | ||
* Listener for messages from the extension. | ||
* @param {Object} request - The message request. | ||
* @param {Object} sender - The sender of the message. | ||
* @param {function} sendResponse - Callback function to send a response. | ||
*/ | ||
chrome.runtime.onMessage.addListener((request) => { | ||
switch (request.message.type) { | ||
case 'TOGGLE_RECORDING': | ||
switch (request.message.data) { | ||
case 'START': | ||
initateRecordingStart(); | ||
break; | ||
case 'STOP': | ||
initateRecordingStop(); | ||
break; | ||
} | ||
break; | ||
} | ||
}); | ||
|
||
/** | ||
* Handles the installation of the extension. | ||
*/ | ||
async function handleInstall() { | ||
console.log('Extension installed...'); | ||
if (!(await hasDocument())) { | ||
// create offscreen document | ||
await createOffscreenDocument(); | ||
} | ||
} | ||
|
||
/** | ||
* Sends a message to the offscreen document. | ||
* @param {string} type - The type of the message. | ||
* @param {Object} data - The data to be sent with the message. | ||
*/ | ||
async function sendMessageToOffscreenDocument(type, data) { | ||
// Create an offscreen document if one doesn't exist yet | ||
try { | ||
if (!(await hasDocument())) { | ||
await createOffscreenDocument(); | ||
} | ||
} finally { | ||
// Now that we have an offscreen document, we can dispatch the message. | ||
chrome.runtime.sendMessage({ | ||
message: { | ||
type: type, | ||
target: 'offscreen', | ||
data: data | ||
} | ||
}); | ||
} | ||
} | ||
|
||
/** | ||
* Initiates the stop recording process. | ||
*/ | ||
function initateRecordingStop() { | ||
console.log('Recording stopped at offscreen'); | ||
sendMessageToOffscreenDocument('STOP_OFFSCREEN_RECORDING'); | ||
} | ||
|
||
/** | ||
* Initiates the start recording process. | ||
*/ | ||
function initateRecordingStart() { | ||
chrome.tabs.query({ active: true, lastFocusedWindow: true }, ([tab]) => { | ||
if (chrome.runtime.lastError || !tab) { | ||
console.error('No valid webpage or tab opened'); | ||
return; | ||
} | ||
|
||
chrome.tabs.sendMessage( | ||
tab.id, | ||
{ | ||
// Send message to content script of the specific tab to check and/or prompt mic permissions | ||
message: { type: 'PROMPT_MICROPHONE_PERMISSION' } | ||
}, | ||
(response) => { | ||
// If user allows the mic permissions, we continue the recording procedure. | ||
if (response.message.status === 'success') { | ||
console.log('Recording started at offscreen'); | ||
sendMessageToOffscreenDocument('START_OFFSCREEN_RECORDING'); | ||
} | ||
} | ||
); | ||
}); | ||
} | ||
|
||
/** | ||
* Checks if there is an offscreen document. | ||
* @returns {Promise<boolean>} - Promise that resolves to a boolean indicating if an offscreen document exists. | ||
*/ | ||
async function hasDocument() { | ||
// Check all windows controlled by the service worker if one of them is the offscreen document | ||
const matchedClients = await clients.matchAll(); | ||
for (const client of matchedClients) { | ||
if (client.url.endsWith(OFFSCREEN_DOCUMENT_PATH)) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Creates the offscreen document. | ||
* @returns {Promise<void>} - Promise that resolves when the offscreen document is created. | ||
*/ | ||
async function createOffscreenDocument() { | ||
await chrome.offscreen.createDocument({ | ||
url: OFFSCREEN_DOCUMENT_PATH, | ||
reasons: [OFFSCREEN_REASON], | ||
justification: 'To interact with user media' | ||
}); | ||
} |
79 changes: 79 additions & 0 deletions
79
functional-samples/cookbook.offscreen-user-media/contentScript.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/** | ||
* Listener for messages from the background script. | ||
* @param {Object} request - The message request. | ||
* @param {Object} sender - The sender of the message. | ||
* @param {function} sendResponse - Callback function to send a response. | ||
* @returns {boolean} - Whether the response should be sent asynchronously (true by default). | ||
*/ | ||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { | ||
switch (request.message.type) { | ||
case 'PROMPT_MICROPHONE_PERMISSION': | ||
// Check for mic permissions. If not found, prompt | ||
checkMicPermissions() | ||
.then(() => { | ||
sendResponse({ message: { status: 'success' } }); | ||
}) | ||
.catch(() => { | ||
promptMicPermissions(); | ||
const iframe = document.getElementById('PERMISSION_IFRAME_ID'); | ||
window.addEventListener('message', (event) => { | ||
if (event.source === iframe.contentWindow && event.data) { | ||
if (event.data.type === 'permissionsGranted') { | ||
sendResponse({ | ||
message: { status: 'success' } | ||
}); | ||
} else { | ||
sendResponse({ | ||
message: { | ||
status: 'failure' | ||
} | ||
}); | ||
} | ||
document.body.removeChild(iframe); | ||
} | ||
}); | ||
}); | ||
break; | ||
|
||
default: | ||
// Do nothing for other message types | ||
break; | ||
} | ||
return true; | ||
}); | ||
|
||
/** | ||
* Checks microphone permissions using a message to the background script. | ||
* @returns {Promise<void>} - Promise that resolves if permissions are granted, rejects otherwise. | ||
*/ | ||
function checkMicPermissions() { | ||
return new Promise((resolve, reject) => { | ||
chrome.runtime.sendMessage( | ||
{ | ||
message: { | ||
type: 'CHECK_PERMISSIONS', | ||
target: 'offscreen' | ||
} | ||
}, | ||
(response) => { | ||
if (response.message.status === 'success') { | ||
resolve(); | ||
} else { | ||
reject(response.message.data); | ||
} | ||
} | ||
); | ||
}); | ||
} | ||
|
||
/** | ||
* Prompts the user for microphone permissions using an iframe. | ||
*/ | ||
function promptMicPermissions() { | ||
const iframe = document.createElement('iframe'); | ||
iframe.setAttribute('hidden', 'hidden'); | ||
iframe.setAttribute('allow', 'microphone'); | ||
iframe.setAttribute('id', 'PERMISSION_IFRAME_ID'); | ||
iframe.src = chrome.runtime.getURL('requestPermissions.html'); | ||
document.body.appendChild(iframe); | ||
} |
25 changes: 25 additions & 0 deletions
25
functional-samples/cookbook.offscreen-user-media/manifest.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"name": "Offscreen API - User media", | ||
"version": "1.0", | ||
"manifest_version": 3, | ||
"description": "Shows how to record audio in a chrome extension via offscreen document.", | ||
"background": { | ||
"service_worker": "background.js" | ||
}, | ||
"content_scripts": [ | ||
{ | ||
"matches": ["<all_urls>"], | ||
"js": ["contentScript.js"] | ||
} | ||
], | ||
"action": { | ||
"default_popup": "popup/popup.html" | ||
}, | ||
"permissions": ["offscreen", "activeTab", "tabs"], | ||
"web_accessible_resources": [ | ||
{ | ||
"resources": ["requestPermissions.html", "requestPermissions.js"], | ||
"matches": ["<all_urls>"] | ||
} | ||
] | ||
} |
10 changes: 10 additions & 0 deletions
10
functional-samples/cookbook.offscreen-user-media/offscreen/offscreen.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>Offscreen Document</title> | ||
<script src="./offscreen.js"></script> | ||
</head> | ||
<body></body> | ||
</html> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can do just this
and run it without all the fuss with iframes, messaging to offscreen and back
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
however I still get permission in
prompt
state, so I am probably missing something from your codeThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sorokinvj
#821 : The reason for requesting mic and cam permissions inside the iframe is because we want to ask for the permissions on behalf of the extension app (
eg -> "[myExtension] wants to Use" instead of "www.google.com wants to use your camera and microphone"
) and hold onto the permissions across all tabs, only asking once for the permissions.Your suggestion of injecting CS directly via manifest will not help in accomplishing above goal. But will definitely ease around the fuss if you don't need it in the context of your extension.
Attaching a snapshot for reference how your suggested code will ask for permissions.
Let me know if I am missing out something in your proposed flow.
Thanks.