anc
Запись всего звука, папка хранения, точнее распознавание
- Звук собеседников — весь звук компьютера, кроме самого Skillok Meet:
Zoom, Google Meet, Телемост, что угодно. Windows — process loopback в
режиме исключения своего процесса, macOS — ScreenCaptureKit без фильтра
по приложению. Поиск Zoom и зависимость windows убраны.
- Если звук компьютера недоступен (на Mac — нет разрешения), запись идёт
с микрофона, а интерфейс говорит почему.
- Настройки: папка хранения записей (выбрать, открыть, вернуть по
умолчанию; записи переносятся целиком), точность распознавания —
по умолчанию полная whisper-large-v3.
- «Открыть папку с файлами» у записи; микрофон на Mac просит у CoreAudio
сразу 16 кГц.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96a28a9
30 сентября 2026 г.
recording.ts
// Запись: панель с таймером, уровнями и кнопками — одна на главной и на
// экране записи — и метка «идёт запись» в шапке, видная с любого экрана.
import { api, elapsed, events } from "./api";
import { esc } from "./dom";
import type { Scope } from "./scope";
import { changed, copy, refreshRecording, state, toast } from "./state";
export function recordingPanel(warning: string): string {
const r = state.recording;
if (!r) return "";
const meter = (label: string, key: string) => `
${label}
`;
return `
${
r.paused ? "Запись на паузе" : r.system ? "Идёт запись: собеседники и вы" : "Идёт запись: только ваш микрофон"
}
${r.system_error ? `Голоса собеседников не пишутся: ${esc(r.system_error)}` : ""}
${elapsed(r.started_at)}
${meter("Вы", "me")}${r.system ? meter("Собеседники", "others") : ""}
Остановить
${r.paused ? "Продолжить" : "Пауза"}
Скопировать предупреждение для чата
${esc(warning)}
`;
}
/** Кнопки панели и живые уровни звука. `onStop` — после остановки. */
export function bindRecording(root: HTMLElement, scope: Scope, warning: string, onStop: () => void): void {
root.querySelector("[data-rec-stop]")?.addEventListener("click", async (e) => {
const b = e.currentTarget as HTMLButtonElement;
b.disabled = true;
b.textContent = "Останавливаю…";
try {
await api.recorderStop();
await refreshRecording();
onStop();
} catch (err) {
b.disabled = false;
toast(err);
}
});
root.querySelector("[data-rec-pause]")?.addEventListener("click", async () => {
if (!state.recording) return;
try {
await api.recorderPause(!state.recording.paused);
state.recording.paused = !state.recording.paused;
changed();
} catch (err) {
toast(err);
}
});
root.querySelector("[data-rec-warning]")?.addEventListener("click", () => copy(warning, "Предупреждение скопировано — вставьте его в чат встречи"));
scope.on(
events.recorder(({ event }) => {
if (event.kind === "level") {
const key = event.data.speaker.kind === "me" ? "me" : "others";
const fill = root.querySelector(`[data-level="${key}"]`);
if (fill) fill.style.width = `${Math.max(0, Math.min(100, ((event.data.dbfs + 60) / 55) * 100))}%`;
} else {
toast(`Дорожка «${event.data.speaker.kind === "me" ? "вы" : "собеседники"}» перестала писаться: ${event.data.error}`);
}
}),
);
scope.every(1000, () => {
const t = root.querySelector("[data-timer]");
if (t && state.recording) t.textContent = elapsed(state.recording.started_at);
});
}
/** Метка в шапке: запись идёт, а открыт другой экран. */
export function drawHeaderBadge(): void {
const el = document.getElementById("rec-badge");
if (!el) return;
const r = state.recording;
const onItsPage = r && location.hash === `#/m/${encodeURIComponent(r.meeting_id)}`;
const onHome = r && (location.hash === "" || location.hash === "#/");
if (!r || onItsPage || onHome) {
el.hidden = true;
return;
}
el.hidden = false;
el.setAttribute("href", `#/m/${encodeURIComponent(r.meeting_id)}`);
el.innerHTML = `${r.paused ? "Пауза" : "Запись"} ${elapsed(r.started_at)}`;
}
export function startHeaderBadge(): void {
window.addEventListener("hashchange", drawHeaderBadge);
window.setInterval(() => {
const t = document.querySelector("[data-badge-timer]");
if (t && state.recording) t.textContent = elapsed(state.recording.started_at);
}, 1000);
}