Skip to content

Commit bc8a188

Browse files
committed
bugfix
1 parent 8de9323 commit bc8a188

10 files changed

Lines changed: 82 additions & 51 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "webbrain",
3-
"version": "20.1.2",
3+
"version": "20.1.3",
44
"description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.",
55
"private": true,
66
"type": "module",

src/chrome/ARCHITECTURE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# WebBrain Chrome/Edge Extension — Architecture
22

3-
> Version 20.1.2 · Manifest V3 · Service Worker background
3+
> Version 20.1.3 · Manifest V3 · Service Worker background
44
55
## High-Level Overview
66

@@ -192,8 +192,8 @@ offscreen/recorder.js
192192
│ captured audio ─→ mixDestination
193193
│ mic ─→ mixDestination
194194
│ tab audio only ─→ audioContext.destination (passthrough so user hears the call)
195-
├─ MediaStream(video tracks ∪ mixDestination audio tracks)
196-
└─ MediaRecorder(mimeType: 'video/webm;codecs=vp9,opus')
195+
├─ MediaStream(video tracks ∪ mixDestination audio tracks when audio exists)
196+
└─ MediaRecorder(mimeType selected from actual final video/audio tracks)
197197
└─ ondataavailable → chunks[]
198198
→ on stop, Blob → dataURL → background
199199

src/chrome/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "WebBrain",
4-
"version": "20.1.2",
4+
"version": "20.1.3",
55
"description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.",
66
"permissions": [
77
"sidePanel",

src/chrome/src/offscreen/recorder.js

Lines changed: 68 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,32 @@
5353
});
5454
}
5555

56+
function pickRecorderMimeType({ hasVideo, hasAudio, requestedMime }) {
57+
const candidates = hasVideo
58+
? (hasAudio
59+
? [
60+
'video/webm;codecs=vp9,opus',
61+
'video/webm;codecs=vp8,opus',
62+
'video/webm',
63+
]
64+
: [
65+
'video/webm;codecs=vp9',
66+
'video/webm;codecs=vp8',
67+
'video/webm',
68+
])
69+
: [
70+
'audio/webm;codecs=opus',
71+
'audio/webm',
72+
];
73+
const chosenMime = (requestedMime && MediaRecorder.isTypeSupported(requestedMime))
74+
? requestedMime
75+
: candidates.find(m => MediaRecorder.isTypeSupported(m));
76+
if (!chosenMime) {
77+
throw new Error('No supported MediaRecorder mimeType found.');
78+
}
79+
return chosenMime;
80+
}
81+
5682
async function start(message) {
5783
let { streamId, tabId, options } = message || {};
5884
if (session) {
@@ -74,26 +100,6 @@
74100
mimeType: requestedMime,
75101
} = options || {};
76102

77-
// Pick a MediaRecorder mimeType the browser actually supports. VP9 is
78-
// best quality-per-byte; VP8 is the wide-compat fallback. Audio-only
79-
// gets webm/opus.
80-
const candidates = video
81-
? [
82-
'video/webm;codecs=vp9,opus',
83-
'video/webm;codecs=vp8,opus',
84-
'video/webm',
85-
]
86-
: [
87-
'audio/webm;codecs=opus',
88-
'audio/webm',
89-
];
90-
const chosenMime = (requestedMime && MediaRecorder.isTypeSupported(requestedMime))
91-
? requestedMime
92-
: candidates.find(m => MediaRecorder.isTypeSupported(m));
93-
if (!chosenMime) {
94-
throw new Error('No supported MediaRecorder mimeType found.');
95-
}
96-
97103
// 1. Capture stream — chrome.tabCapture exposes the active tab as a
98104
// MediaStream when we pass the streamId we got from
99105
// chrome.tabCapture.getMediaStreamId() on the service-worker side. For
@@ -172,41 +178,63 @@
172178
// them. Release everything we acquired before rethrowing.
173179
let audioContext = null;
174180
let recorder;
181+
let chosenMime;
175182
try {
176-
audioContext = new AudioContext();
177-
const mixDest = audioContext.createMediaStreamDestination();
178-
179183
const capturedAudioTracks = captureStream.getAudioTracks();
180-
if (capturedAudioTracks.length) {
181-
const capturedAudioSource = audioContext.createMediaStreamSource(
182-
new MediaStream(capturedAudioTracks)
183-
);
184-
capturedAudioSource.connect(mixDest); // into the recording
185-
if (source === 'tab') {
186-
capturedAudioSource.connect(audioContext.destination);
184+
const hasAudioSources = capturedAudioTracks.length > 0 || !!micStream;
185+
let mixDest = null;
186+
if (hasAudioSources) {
187+
audioContext = new AudioContext();
188+
mixDest = audioContext.createMediaStreamDestination();
189+
190+
if (capturedAudioTracks.length) {
191+
const capturedAudioSource = audioContext.createMediaStreamSource(
192+
new MediaStream(capturedAudioTracks)
193+
);
194+
capturedAudioSource.connect(mixDest); // into the recording
195+
if (source === 'tab') {
196+
capturedAudioSource.connect(audioContext.destination);
197+
}
187198
}
188-
}
189199

190-
if (micStream) {
191-
const micSource = audioContext.createMediaStreamSource(micStream);
192-
micSource.connect(mixDest); // into the recording (do NOT loop to speaker — feedback)
200+
if (micStream) {
201+
const micSource = audioContext.createMediaStreamSource(micStream);
202+
micSource.connect(mixDest); // into the recording (do NOT loop to speaker — feedback)
203+
}
193204
}
194205

195206
// 4. Build the final stream the recorder consumes.
196207
const finalStream = new MediaStream();
197208
if (video) {
198209
for (const t of captureStream.getVideoTracks()) finalStream.addTrack(t);
199210
}
200-
for (const t of mixDest.stream.getAudioTracks()) finalStream.addTrack(t);
211+
if (mixDest) {
212+
for (const t of mixDest.stream.getAudioTracks()) finalStream.addTrack(t);
213+
}
214+
const finalHasVideo = finalStream.getVideoTracks().length > 0;
215+
const finalHasAudio = finalStream.getAudioTracks().length > 0;
216+
if (!finalHasVideo && !finalHasAudio) {
217+
throw new Error('No media tracks available to record.');
218+
}
219+
chosenMime = pickRecorderMimeType({
220+
hasVideo: finalHasVideo,
221+
hasAudio: finalHasAudio,
222+
requestedMime,
223+
});
201224

202225
// 5. MediaRecorder. Collect dataavailable chunks. Pass a timeslice so
203226
// partial data survives a crash and gives us progress estimates.
204-
recorder = new MediaRecorder(finalStream, {
227+
const recorderOptions = {
205228
mimeType: chosenMime,
206-
// ~256 kbps audio is plenty for speech; the rest is video budget.
207-
audioBitsPerSecond: 192_000,
208-
videoBitsPerSecond: 2_500_000,
209-
});
229+
};
230+
if (finalHasAudio) {
231+
// ~192 kbps audio is plenty for speech; the rest is video budget.
232+
recorderOptions.audioBitsPerSecond = 192_000;
233+
}
234+
if (finalHasVideo) {
235+
recorderOptions.videoBitsPerSecond = 2_500_000;
236+
}
237+
recorder = new MediaRecorder(finalStream, recorderOptions);
210238
} catch (e) {
211239
try { for (const t of captureStream.getTracks()) t.stop(); } catch {}
212240
if (micStream) { try { for (const t of micStream.getTracks()) t.stop(); } catch {} }

src/chrome/src/ui/settings.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919

2020
// Version shown in the subtitle. Kept here so it only needs one update per
2121
// release; the subtitle string itself is translated.
22-
const EXT_VERSION = '20.1.2';
22+
const EXT_VERSION = '20.1.3';
2323

2424
const providersContainer = document.getElementById('providers');
2525
const displaySettings = document.getElementById('display-settings');

src/firefox/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# WebBrain Firefox Extension — Architecture
22

3-
> Version 20.1.2 · Manifest V2 · Background Page
3+
> Version 20.1.3 · Manifest V2 · Background Page
44
55
## How Firefox Differs from Chrome
66

src/firefox/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 2,
33
"name": "WebBrain",
4-
"version": "20.1.2",
4+
"version": "20.1.3",
55
"description": "Open-source AI browser agent — chat with pages, automate tasks, multi-provider LLM support.",
66
"permissions": [
77
"activeTab",

src/firefox/src/ui/settings.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919

2020
// Version shown in the subtitle. Kept here so it only needs one update per
2121
// release; the subtitle string itself is translated.
22-
const EXT_VERSION = '20.1.2';
22+
const EXT_VERSION = '20.1.3';
2323

2424
const providersContainer = document.getElementById('providers');
2525
const displaySettings = document.getElementById('display-settings');

test/run.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4857,6 +4857,9 @@ test('chrome /record-full-screen is slash-only, Chrome-only, and hidden from the
48574857
assert.match(host, /export async function startDisplayRecording\(options = \{\}/, 'chrome: recorder host should expose display recording for slash command routing');
48584858
assert.match(host, /function scheduleRecordingSafetyWatchdog[\s\S]*?chrome\.alarms\?\.create/, 'chrome: hidden recordings should have a background-owned safety cap');
48594859
assert.match(offscreen, /navigator\.mediaDevices\.getDisplayMedia\(\{[\s\S]*?audio: audio !== false,[\s\S]*?video: true,[\s\S]*?\}\)/, 'chrome: offscreen recorder should open the screen/window picker through getDisplayMedia');
4860+
assert.match(offscreen, /function pickRecorderMimeType\(\{ hasVideo, hasAudio, requestedMime \}\)[\s\S]*?'video\/webm;codecs=vp9,opus'[\s\S]*?'video\/webm;codecs=vp9'[\s\S]*?'audio\/webm;codecs=opus'/, 'chrome: recorder should choose MIME after it knows whether the final stream has audio');
4861+
assert.match(offscreen, /const hasAudioSources = capturedAudioTracks\.length > 0 \|\| !!micStream;[\s\S]*?if \(hasAudioSources\) \{[\s\S]*?mixDest = audioContext\.createMediaStreamDestination\(\);[\s\S]*?if \(mixDest\) \{[\s\S]*?mixDest\.stream\.getAudioTracks\(\)/, 'chrome: display video-only recordings should not add an empty WebAudio track');
4862+
assert.match(offscreen, /if \(finalHasAudio\) \{[\s\S]*?recorderOptions\.audioBitsPerSecond = 192_000;[\s\S]*?\}[\s\S]*?if \(finalHasVideo\) \{[\s\S]*?recorderOptions\.videoBitsPerSecond = 2_500_000;/, 'chrome: recorder bitrate hints should match the actual final stream tracks');
48604863
assert.doesNotMatch(offscreen, /chrome\.desktopCapture/, 'chrome: offscreen recorder cannot use extension APIs beyond chrome.runtime');
48614864
assert.match(offscreen, /addEventListener\('ended', \(\) => \{[\s\S]*?s\.captureEndedCleanupStarted = true;[\s\S]*?finalizeCaptureEnded\(s\)\.catch/, 'chrome: capture-ended cleanup should use the async finalize path');
48624865
assert.match(offscreen, /async function finalizeCaptureEnded\(s\) \{[\s\S]*?s\.stopping = true;[\s\S]*?await waitForRecorderStop\(s\);[\s\S]*?notifyCaptureEnded\(s\);[\s\S]*?await releaseSession\(s\);[\s\S]*?\}/, 'chrome: capture-ended finalize should wait for final recorder data before notifying background');

0 commit comments

Comments
 (0)