(feat): migrate experimental tauri cef to electron
(feat): add flake to expose tensamin desktop package (qol): update todo
This commit is contained in:
parent
be80e2f387
commit
a209ade10b
32 changed files with 1649 additions and 459 deletions
|
|
@ -20,7 +20,7 @@ import {
|
|||
type DesktopScreenShareCapabilities,
|
||||
type DesktopScreenShareSource,
|
||||
useDesktopMedia,
|
||||
} from "@tensamin/tauri/context";
|
||||
} from "@tensamin/shared/desktopMedia";
|
||||
import { toast } from "@tensamin/shared/log";
|
||||
import { AppWindow, Loader2, MonitorUp } from "lucide-react";
|
||||
import type { ScreenShareCaptureOptions } from "livekit-client";
|
||||
|
|
@ -150,7 +150,9 @@ export default function ScreenShareDialog({
|
|||
setLoading(true);
|
||||
|
||||
try {
|
||||
if (capabilities.platform === "linux") {
|
||||
if (capabilities.runtime === "electron") {
|
||||
await startLinuxDesktopScreenShare(selectedSource.id);
|
||||
} else if (capabilities.platform === "linux") {
|
||||
await startLinuxDesktopScreenShare(selectedSource.id);
|
||||
} else {
|
||||
await setScreenShareEnabled(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { log } from "@tensamin/shared/log";
|
||||
import {
|
||||
type LocalTrack,
|
||||
|
|
@ -22,6 +21,39 @@ type ScreenShareStoreSetState = (
|
|||
| ((state: ScreenShareStoreState) => Partial<ScreenShareStoreState>),
|
||||
) => void;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
tensaminDesktop?: {
|
||||
media?: {
|
||||
getScreenShareCapabilities?: () => Promise<{
|
||||
runtime?: "electron" | "tauri";
|
||||
platform: "linux" | "macos" | "windows" | "other";
|
||||
showAudioOutputSelector: boolean;
|
||||
showAudioSwitch: boolean;
|
||||
hasReliableSystemAudio: boolean;
|
||||
}>;
|
||||
listScreenShareSources?: () => Promise<
|
||||
Array<{
|
||||
id: string;
|
||||
kind: "screen" | "window";
|
||||
name: string;
|
||||
subtitle?: string | null;
|
||||
thumbnail?: string | null;
|
||||
}>
|
||||
>;
|
||||
listScreenShareAudioOutputs?: () => Promise<
|
||||
Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
}>
|
||||
>;
|
||||
selectScreenShareSource?: (sourceId: string) => Promise<boolean>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type ScreenShareControllerOptions = {
|
||||
room: Room;
|
||||
getState: () => ScreenShareStoreState;
|
||||
|
|
@ -128,84 +160,26 @@ export function createScreenShareController({
|
|||
async function startLinuxDesktopScreenShare(sourceId: string) {
|
||||
await clearPublishedScreenShare();
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 1280;
|
||||
canvas.height = 720;
|
||||
canvas.style.display = "none";
|
||||
document.body.appendChild(canvas);
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
if (!context) {
|
||||
canvas.remove();
|
||||
throw new Error("Failed to initialize the screen share canvas.");
|
||||
}
|
||||
|
||||
const stream = canvas.captureStream(8);
|
||||
const videoTrack = stream.getVideoTracks()[0];
|
||||
|
||||
if (!videoTrack) {
|
||||
canvas.remove();
|
||||
throw new Error("Failed to create a video track for screen sharing.");
|
||||
}
|
||||
|
||||
const image = new Image();
|
||||
let stopped = false;
|
||||
let frameRequestInFlight = false;
|
||||
|
||||
const renderFrame = async () => {
|
||||
if (stopped || frameRequestInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
frameRequestInFlight = true;
|
||||
|
||||
try {
|
||||
const dataUrl = await invoke<string>("capture_screen_share_frame", {
|
||||
sourceId,
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
image.onload = () => resolve();
|
||||
image.onerror = () =>
|
||||
reject(new Error("Failed to decode screen share frame."));
|
||||
image.src = dataUrl;
|
||||
});
|
||||
|
||||
if (
|
||||
canvas.width !== image.naturalWidth ||
|
||||
canvas.height !== image.naturalHeight
|
||||
) {
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
}
|
||||
|
||||
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
} finally {
|
||||
frameRequestInFlight = false;
|
||||
}
|
||||
};
|
||||
|
||||
await renderFrame();
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
void renderFrame().catch((error) => {
|
||||
log(
|
||||
1,
|
||||
"call",
|
||||
"red",
|
||||
"Failed to capture Linux screen share frame",
|
||||
error,
|
||||
);
|
||||
if (window.tensaminDesktop?.media?.selectScreenShareSource) {
|
||||
await window.tensaminDesktop.media.selectScreenShareSource(sourceId);
|
||||
const tracks = await room.localParticipant.createScreenTracks({
|
||||
audio: false,
|
||||
video: true,
|
||||
systemAudio: "exclude",
|
||||
surfaceSwitching: "exclude",
|
||||
selfBrowserSurface: "exclude",
|
||||
contentHint: "detail",
|
||||
});
|
||||
}, 125);
|
||||
|
||||
await publishScreenShareTracks([videoTrack], () => {
|
||||
stopped = true;
|
||||
window.clearInterval(interval);
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
canvas.remove();
|
||||
});
|
||||
await publishScreenShareTracks(tracks, () => {
|
||||
tracks.forEach((track) => track.stop());
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Electron desktop media bridge is unavailable. Cannot capture ${sourceId}.`,
|
||||
);
|
||||
}
|
||||
|
||||
async function stopScreenShare() {
|
||||
|
|
|
|||
|
|
@ -741,6 +741,14 @@ let screenShareController: ReturnType<
|
|||
typeof createScreenShareController
|
||||
> | null = null;
|
||||
|
||||
function getNoiseFilterAssetBaseUrl() {
|
||||
if (window.location.protocol === "file:") {
|
||||
return new URL("./assets", document.baseURI).href.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
return "/assets";
|
||||
}
|
||||
|
||||
function getScreenShareController() {
|
||||
if (!screenShareController) {
|
||||
screenShareController = createScreenShareController({
|
||||
|
|
@ -1086,7 +1094,7 @@ export function useInitializeCall() {
|
|||
noiseReductionLevel: 60,
|
||||
sampleRate: 48000,
|
||||
assetConfig: {
|
||||
cdnUrl: "/assets",
|
||||
cdnUrl: getNoiseFilterAssetBaseUrl(),
|
||||
},
|
||||
}),
|
||||
[],
|
||||
|
|
|
|||
Loading…
Reference in a new issue