[feat] receive remote audio with panel playback and bounded WAV capture

This commit is contained in:
dijunkun
2026-11-06 21:55:58 +08:00
parent 02b10d0d8e
commit 82b8cc11bc
15 changed files with 287 additions and 7 deletions
+1 -1
View File
@@ -255,7 +255,7 @@ export class BrowserPanel {
if (typeof body.view_id !== "string" || !client.views.has(body.view_id))
throw new Failure("VIEW_UNAUTHORIZED");
const name = req.url?.match(
/^\/api\/(ready|devices|status|connect|video_open|control|unlock|recovery|input)$/,
/^\/api\/(ready|devices|status|connect|video_open|control|unlock|recovery|input|audio)$/,
)?.[1];
if (!name) throw new Failure("UNKNOWN_ACTION");
const { view_id, ...args } = body;
+11
View File
@@ -41,10 +41,15 @@ const uiNames: Record<UIAction, string> = {
unlock: "crossdesk_ui_unlock",
recovery: "crossdesk_ui_recovery",
input: "crossdesk_ui_input",
audio: "crossdesk_ui_audio",
};
function output(value: Data, meta?: Data): CallToolResult {
const data = { ...value };
const content: any[] = [];
if (data.mime_type === "audio/wav" && typeof data.data === "string") {
content.push({ type: "audio", mimeType: data.mime_type, data: data.data });
delete data.data;
}
if (data.mime_type === "image/png" && typeof data.data === "string") {
content.push({ type: "image", mimeType: data.mime_type, data: data.data });
delete data.data;
@@ -261,6 +266,12 @@ export function createServer(
},
{ readOnly: true },
);
add("crossdesk_audio", z.strictObject({ session_id: id, enabled: z.boolean() }),
"Enable or disable remote desktop audio reception. Audio starts disabled. Panel playback also requires the user's Play audio button. Status reports PCM reception counters.",
async (p, ctx) => output(await sessions.audio(p, false, ctx.mcpReq.signal)));
add("crossdesk_audio_capture", z.strictObject({ session_id: id, duration_ms: z.int().min(100).max(10000).default(3000) }),
"Capture the next 100–10000 ms of remote desktop audio as a bounded mono 48 kHz WAV audio content block. Temporarily enables reception, then restores the previous setting. AUDIO_UNAVAILABLE means insufficient PCM arrived. Cancellation discards the capture.",
async (p, ctx) => output(await sessions.audio(p, true, ctx.mcpReq.signal)), { readOnly: true });
add("crossdesk_list_sessions", z.strictObject({}),
"List the active remote sessions with their device aliases, displays and control state. Each session_id addresses one connection. Up to eight connections can be active concurrently.",
async () => output(await sessions.listSessions()), { readOnly: true });
+13
View File
@@ -61,6 +61,7 @@ interface Session {
displayBatch?: boolean;
mode: string;
desiredControl: boolean;
audioEnabled?: boolean;
recoveryEnabled: boolean;
recoveryAttempts: number;
recoveryAttempt: number;
@@ -317,6 +318,10 @@ export class Sessions {
if (next.status.preview_bitrate_supported) settings.max_bitrate_bps = s.status.preview_bitrate_bps;
await next.native.request("video_settings", settings).catch(() => {});
}
if (s.audioEnabled) {
await next.native.request("audio", { enabled: true });
next.audioEnabled = true;
}
next.recoveryEnabled = true;
next.recoveryAttempts = s.recoveryAttempts;
next.recoveryAttempt = attempt;
@@ -756,6 +761,14 @@ export class Sessions {
}
return record.result;
}
async audio(p: Data, capture: boolean, signal?: AbortSignal): Promise<Data> {
const { session_id, ...args } = p;
const s = this.session(session_id);
const result = await s.native.request(capture ? "audio_capture" : "audio", args,
capture ? (args.duration_ms ?? 3000) + 5000 : 5000, signal);
if (!capture) { s.status = result; s.audioEnabled = args.enabled; }
return result;
}
async videoSettings(p: Data, signal?: AbortSignal): Promise<Data> {
const { session_id, ...args } = p;
const s = this.session(session_id);
+4
View File
@@ -9,6 +9,7 @@ export const uiSchemas = {
connect: uiConnect,
video_open: z.strictObject({}),
input: viewerInput,
audio: z.strictObject({ enabled: z.boolean() }),
recovery: z.strictObject({ enabled: z.boolean(), max_attempts: z.int().min(1).max(5).default(3) }),
unlock: z.strictObject({
request_id: id,
@@ -46,6 +47,9 @@ export async function uiAction(
return sessions.status(undefined, view.id, signal);
case "connect":
return sessions.connectUI(view, p, signal);
case "audio":
if (!view.session) throw new Failure("NOT_READY");
return sessions.audio({ session_id: view.session, ...p }, false, signal);
case "input":
return sessions.viewerInput(view, p, signal);
case "control":