Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 34 additions & 12 deletions apps/chrome-extension/src/background/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ const shouldAutoPipCaptureSource = (source: RecordingCaptureSource) =>
isWindowCaptureSource(source) && !isLikelyBrowserWindow(source);

const isWebcamPreviewEnabled = (settings: ExtensionSettings) =>
settings.webcam.enabled && Boolean(settings.webcam.deviceId);
Boolean(settings.webcam.enabled);

const shouldShowWebcamPreview = async (
settings: ExtensionSettings,
Expand Down Expand Up @@ -817,6 +817,29 @@ const broadcastOverlayHide = async () => {
pendingPreviewTabId = null;
};

const broadcastOverlayCountdown = async (
seconds: number,
durationMs: number,
) => {
const tabs = await getTabs();
await Promise.all(
tabs.map((tab) => {
if (!canInjectIntoTab(tab) || tab.id === undefined) {
return undefined;
}
return sendOverlay(
tab.id,
{
type: "overlay-countdown",
seconds,
durationMs,
},
false,
).catch(() => undefined);
}),
);
};

const broadcastRecordingStatusToTabs = async (status: RecordingStatus) => {
const message: RecordingStatusBroadcast = {
target: "recording-status",
Expand Down Expand Up @@ -1621,6 +1644,15 @@ const handleRequest = async (
: { ok: false, error: response.error };
}

if (message.type === "toggle-microphone-mute") {
const response = await sendOffscreen({
target: "offscreen",
type: "toggle-microphone-mute",
muted: message.muted,
});
return response;
}

if (message.type === "open-options") {
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") });
return { ok: true };
Expand Down Expand Up @@ -1737,17 +1769,7 @@ const handleRequest = async (
}

if (message.type === "show-countdown") {
// Relay the offscreen recorder's countdown to the recorded tab. Inject
// the overlay if it is not there yet; a tab that cannot host it (e.g. a
// chrome:// page) just shows nothing while the recorder waits out the
// same countdown, so the count is still kept out of the capture.
if (message.tabId !== undefined) {
void sendOverlay(message.tabId, {
type: "overlay-countdown",
seconds: message.seconds,
durationMs: message.durationMs,
}).catch(() => undefined);
}
await broadcastOverlayCountdown(message.seconds, message.durationMs);
return { ok: true };
}

Expand Down
137 changes: 137 additions & 0 deletions apps/chrome-extension/src/content/blur-overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { useEffect, useRef, useState } from "react";

type HighlightRect = {
top: number;
left: number;
width: number;
height: number;
};

type BlurOverlayProps = {
active: boolean;
onDone: () => void;
};

const OVERLAY_ROOT_ID = "cap-extension-recorder-overlay";

export function BlurOverlay({ active, onDone }: BlurOverlayProps) {
const [highlightRect, setHighlightRect] = useState<HighlightRect | null>(
null,
);
const rafRef = useRef<number | null>(null);

useEffect(() => {
if (!active) {
setHighlightRect(null);
return;
}

const handlePointerMove = (event: PointerEvent) => {
if (rafRef.current !== null) return;
rafRef.current = window.requestAnimationFrame(() => {
rafRef.current = null;
const target = document.elementFromPoint(
event.clientX,
event.clientY,
) as HTMLElement | null;

if (!target || target.closest(`#${OVERLAY_ROOT_ID}`)) {
setHighlightRect(null);
return;
}

const rect = target.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) {
setHighlightRect(null);
return;
}

setHighlightRect({
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
});
});
};

const handleClick = (event: MouseEvent) => {
const target = document.elementFromPoint(
event.clientX,
event.clientY,
) as HTMLElement | null;

if (!target || target.closest(`#${OVERLAY_ROOT_ID}`)) {
return;
}

event.preventDefault();
event.stopPropagation();

if (target.dataset.capBlurred === "true") {
const orig = target.dataset.capOrigFilter ?? "";
if (orig) {
target.style.filter = orig;
} else {
target.style.removeProperty("filter");
}
target.style.removeProperty("user-select");
delete target.dataset.capBlurred;
delete target.dataset.capOrigFilter;
} else {
target.dataset.capOrigFilter = target.style.filter || "";
target.style.setProperty("filter", "blur(12px)", "important");
target.style.setProperty("user-select", "none", "important");
target.dataset.capBlurred = "true";
}
};

const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
onDone();
}
};

window.addEventListener("pointermove", handlePointerMove, {
capture: true,
passive: true,
});
window.addEventListener("click", handleClick, {
capture: true,
});
window.addEventListener("keydown", handleKeyDown, { capture: true });

return () => {
if (rafRef.current !== null) {
window.cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
window.removeEventListener("pointermove", handlePointerMove, {
capture: true,
});
window.removeEventListener("click", handleClick, {
capture: true,
});
window.removeEventListener("keydown", handleKeyDown, { capture: true });
};
}, [active, onDone]);

if (!active || !highlightRect) return null;

return (
<div
className="cap-extension-blur-highlight"
style={{
top: `${highlightRect.top}px`,
left: `${highlightRect.left}px`,
width: `${highlightRect.width}px`,
height: `${highlightRect.height}px`,
}}
aria-hidden
>
<span className="cap-extension-blur-tooltip">Click to blur / unblur</span>
</div>
);
}
Loading
Loading