mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-07 06:39:10 +08:00
[feat] receive remote audio with panel playback and bounded WAV capture
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user