Skip to content

Commit e8d2613

Browse files
oliverdunkAmySteam
andauthored
Add sample for tabCapture using offscreen document (GoogleChrome#974)
* Add sample for using tabCapture in offscreen document. * Add recording item to tabCapture demo. Adds an icon which indicates if recording is currently in progress. * Addressed a number of pieces of feedback. * Apply suggestions from code review Co-authored-by: amysteamdev <[email protected]> * Add step to pin extension. * Add comments. --------- Co-authored-by: amysteamdev <[email protected]>
1 parent 327718c commit e8d2613

File tree

7 files changed

+201
-0
lines changed

7 files changed

+201
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# chrome.tabCapture recorder
2+
3+
This sample demonstrates how to use the [`chrome.tabCapture`](https://developer.chrome.com/docs/extensions/reference/tabCapture/) API to record in the background, using a service worker and [offscreen document](https://developer.chrome.com/docs/extensions/reference/offscreen/).
4+
5+
## Overview
6+
7+
In this sample, clicking the action button starts recording the current tab in an offscreen document. After 30 seconds, or once the action button is clicked again, the recording ends and is saved as a download.
8+
9+
## Implementation Notes
10+
11+
See the [Audio recording and screen capture guide](https://developer.chrome.com/docs/extensions/mv3/screen_capture/#audio-and-video-offscreen-doc) for more implementation details.
12+
13+
## Running this extension
14+
15+
1. Clone this repository.
16+
2. Load this directory in Chrome as an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked).
17+
3. Pin the extension from the extension menu.
18+
4. Click the extension's action icon to start recording.
19+
5. Click the extension's action again to stop recording.
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "Tab Capture - Recorder",
3+
"description": "Records the current tab in an offscreen document.",
4+
"version": "1",
5+
"manifest_version": 3,
6+
"minimum_chrome_version": "116",
7+
"action": {
8+
"default_icon": "icons/not-recording.png"
9+
},
10+
"background": {
11+
"service_worker": "service-worker.js"
12+
},
13+
"permissions": ["tabCapture", "offscreen"]
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!--
2+
Copyright 2023 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
-->
16+
<!DOCTYPE html>
17+
<script src="offscreen.js"></script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
chrome.runtime.onMessage.addListener(async (message) => {
16+
if (message.target === 'offscreen') {
17+
switch (message.type) {
18+
case 'start-recording':
19+
startRecording(message.data);
20+
break;
21+
case 'stop-recording':
22+
stopRecording();
23+
break;
24+
default:
25+
throw new Error('Unrecognized message:', message.type);
26+
}
27+
}
28+
});
29+
30+
let recorder;
31+
let data = [];
32+
33+
async function startRecording(streamId) {
34+
if (recorder?.state === 'recording') {
35+
throw new Error('Called startRecording while recording is in progress.');
36+
}
37+
38+
const media = await navigator.mediaDevices.getUserMedia({
39+
audio: {
40+
mandatory: {
41+
chromeMediaSource: 'tab',
42+
chromeMediaSourceId: streamId
43+
}
44+
},
45+
video: {
46+
mandatory: {
47+
chromeMediaSource: 'tab',
48+
chromeMediaSourceId: streamId
49+
}
50+
}
51+
});
52+
53+
// Continue to play the captured audio to the user.
54+
const output = new AudioContext();
55+
const source = output.createMediaStreamSource(media);
56+
source.connect(output.destination);
57+
58+
// Start recording.
59+
recorder = new MediaRecorder(media, { mimeType: 'video/webm' });
60+
recorder.ondataavailable = (event) => data.push(event.data);
61+
recorder.onstop = () => {
62+
const blob = new Blob(data, { type: 'video/webm' });
63+
window.open(URL.createObjectURL(blob), '_blank');
64+
65+
// Clear state ready for next recording
66+
recorder = undefined;
67+
data = [];
68+
};
69+
recorder.start();
70+
71+
// Record the current state in the URL. This provides a very low-bandwidth
72+
// way of communicating with the service worker (the service worker can check
73+
// the URL of the document and see the current recording state). We can't
74+
// store that directly in the service worker as it may be terminated while
75+
// recording is in progress. We could write it to storage but that slightly
76+
// increases the risk of things getting out of sync.
77+
window.location.hash = 'recording';
78+
}
79+
80+
async function stopRecording() {
81+
recorder.stop();
82+
83+
// Stopping the tracks makes sure the recording icon in the tab is removed.
84+
recorder.stream.getTracks().forEach((t) => t.stop());
85+
86+
// Update current state in URL
87+
window.location.hash = '';
88+
89+
// Note: In a real extension, you would want to write the recording to a more
90+
// permanent location (e.g IndexedDB) and then close the offscreen document,
91+
// to avoid keeping a document around unnecessarily. Here we avoid that to
92+
// make sure the browser keeps the Object URL we create (see above) and to
93+
// keep the sample fairly simple to follow.
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
chrome.action.onClicked.addListener(async (tab) => {
16+
const existingContexts = await chrome.runtime.getContexts({});
17+
let recording = false;
18+
19+
const offscreenDocument = existingContexts.find(
20+
(c) => c.contextType === 'OFFSCREEN_DOCUMENT'
21+
);
22+
23+
// If an offscreen document is not already open, create one.
24+
if (!offscreenDocument) {
25+
// Create an offscreen document.
26+
await chrome.offscreen.createDocument({
27+
url: 'offscreen.html',
28+
reasons: ['USER_MEDIA'],
29+
justification: 'Recording from chrome.tabCapture API'
30+
});
31+
} else {
32+
recording = offscreenDocument.documentUrl.endsWith('#recording');
33+
}
34+
35+
if (recording) {
36+
chrome.runtime.sendMessage({
37+
type: 'stop-recording',
38+
target: 'offscreen'
39+
});
40+
chrome.action.setIcon({ path: 'icons/not-recording.png' });
41+
return;
42+
}
43+
44+
// Get a MediaStream for the active tab.
45+
const streamId = await chrome.tabCapture.getMediaStreamId({
46+
targetTabId: tab.id
47+
});
48+
49+
// Send the stream ID to the offscreen document to start recording.
50+
chrome.runtime.sendMessage({
51+
type: 'start-recording',
52+
target: 'offscreen',
53+
data: streamId
54+
});
55+
56+
chrome.action.setIcon({ path: '/icons/recording.png' });
57+
});

0 commit comments

Comments
 (0)