v0.2.0 working
This commit is contained in:
@@ -14,15 +14,22 @@
|
||||
<h1>POC 2 - Recording + Preview</h1>
|
||||
|
||||
<section class="card">
|
||||
<h2>Audio</h2>
|
||||
<h2>Video Preview</h2>
|
||||
|
||||
<div class="preview-wrap">
|
||||
<img id="video-preview" alt="Video preview" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Audio + Video</h2>
|
||||
|
||||
<div class="actions">
|
||||
<button id="start-audio-btn" type="button">Start audio</button>
|
||||
<button id="stop-audio-btn" type="button" disabled>Stop
|
||||
audio</button>
|
||||
<button id="start-av-btn" type="button">Start AV</button>
|
||||
<button id="stop-av-btn" type="button" disabled>Stop AV</button>
|
||||
</div>
|
||||
|
||||
<pre id="audio-status">Ready.</pre>
|
||||
<pre id="av-status">Ready.</pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
@@ -34,11 +41,19 @@
|
||||
video</button>
|
||||
</div>
|
||||
|
||||
<div class="preview-wrap">
|
||||
<img id="video-preview" alt="Video preview" />
|
||||
<pre id="video-status">Ready.</pre>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Audio</h2>
|
||||
|
||||
<div class="actions">
|
||||
<button id="start-audio-btn" type="button">Start audio</button>
|
||||
<button id="stop-audio-btn" type="button" disabled>Stop
|
||||
audio</button>
|
||||
</div>
|
||||
|
||||
<pre id="video-status">Ready.</pre>
|
||||
<pre id="audio-status">Ready.</pre>
|
||||
</section>
|
||||
</main>
|
||||
<script type="module" src="main.ts" defer></script>
|
||||
|
||||
221
frontend/main.ts
221
frontend/main.ts
@@ -1,4 +1,9 @@
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { AvStartResponse } from './ts/bindings/AvStartResponse';
|
||||
import { AvStopResponse } from './ts/bindings/AvStopResponse';
|
||||
|
||||
type Mode = "idle" | "audio" | "video" | "av";
|
||||
|
||||
const startAudioBtn = document.querySelector<HTMLButtonElement>("#start-audio-btn");
|
||||
const stopAudioBtn = document.querySelector<HTMLButtonElement>("#stop-audio-btn");
|
||||
@@ -7,7 +12,12 @@ const audioStatus = document.querySelector<HTMLElement>("#audio-status");
|
||||
const startVideoBtn = document.querySelector<HTMLButtonElement>("#start-video-btn");
|
||||
const stopVideoBtn = document.querySelector<HTMLButtonElement>("#stop-video-btn");
|
||||
const videoStatus = document.querySelector<HTMLElement>("#video-status");
|
||||
const videoPreview = document.querySelector<HTMLImageElement>("#video-preview");
|
||||
|
||||
const startAvBtn = document.querySelector<HTMLButtonElement>("#start-av-btn");
|
||||
const stopAvBtn = document.querySelector<HTMLButtonElement>("#stop-av-btn");
|
||||
const avStatus = document.querySelector<HTMLElement>("#av-status");
|
||||
|
||||
const previewElement = document.querySelector<HTMLImageElement>("#video-preview");
|
||||
|
||||
if (
|
||||
startAudioBtn === null ||
|
||||
@@ -16,19 +26,34 @@ if (
|
||||
startVideoBtn === null ||
|
||||
stopVideoBtn === null ||
|
||||
videoStatus === null ||
|
||||
videoPreview === null
|
||||
startAvBtn === null ||
|
||||
stopAvBtn === null ||
|
||||
avStatus === null ||
|
||||
previewElement === null
|
||||
) {
|
||||
throw new Error("missing UI elements");
|
||||
}
|
||||
|
||||
let currentMode: Mode = "idle";
|
||||
let previewTimer: number | null = null;
|
||||
let previewRequestInFlight = false;
|
||||
let previewObjectUrl: string | null = null;
|
||||
|
||||
function setAudioStatus(message: string): void {
|
||||
if (audioStatus)
|
||||
audioStatus.textContent = message;
|
||||
}
|
||||
|
||||
function setVideoStatus(message: string): void {
|
||||
if (videoStatus)
|
||||
videoStatus.textContent = message;
|
||||
}
|
||||
|
||||
function setAvStatus(message: string): void {
|
||||
if (avStatus)
|
||||
avStatus.textContent = message;
|
||||
}
|
||||
|
||||
function setAudioButtons(isRecording: boolean): void {
|
||||
if (startAudioBtn)
|
||||
startAudioBtn.disabled = isRecording;
|
||||
@@ -36,11 +61,6 @@ function setAudioButtons(isRecording: boolean): void {
|
||||
stopAudioBtn.disabled = !isRecording;
|
||||
}
|
||||
|
||||
function setVideoStatus(message: string): void {
|
||||
if (videoStatus)
|
||||
videoStatus.textContent = message;
|
||||
}
|
||||
|
||||
function setVideoButtons(isRecording: boolean): void {
|
||||
if (startVideoBtn)
|
||||
startVideoBtn.disabled = isRecording;
|
||||
@@ -48,11 +68,92 @@ function setVideoButtons(isRecording: boolean): void {
|
||||
stopVideoBtn.disabled = !isRecording;
|
||||
}
|
||||
|
||||
function setAvButtons(isRecording: boolean): void {
|
||||
if (startAvBtn)
|
||||
startAvBtn.disabled = isRecording;
|
||||
if (stopAvBtn)
|
||||
stopAvBtn.disabled = !isRecording;
|
||||
}
|
||||
|
||||
function setAllButtonsForMode(mode: Mode): void {
|
||||
if (mode === "idle") {
|
||||
setAudioButtons(false);
|
||||
setVideoButtons(false);
|
||||
setAvButtons(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setAudioButtons(mode === "audio");
|
||||
setVideoButtons(mode === "video");
|
||||
setAvButtons(mode === "av");
|
||||
|
||||
if (mode !== "audio") {
|
||||
if (stopAudioBtn)
|
||||
stopAudioBtn.disabled = true;
|
||||
if (startAudioBtn)
|
||||
startAudioBtn.disabled = true;
|
||||
}
|
||||
|
||||
if (mode !== "video") {
|
||||
if (stopVideoBtn)
|
||||
stopVideoBtn.disabled = true;
|
||||
if (startVideoBtn)
|
||||
startVideoBtn.disabled = true;
|
||||
}
|
||||
|
||||
if (mode !== "av") {
|
||||
if (stopAvBtn)
|
||||
stopAvBtn.disabled = true;
|
||||
if (startAvBtn)
|
||||
startAvBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function setCurrentMode(mode: Mode): void {
|
||||
currentMode = mode;
|
||||
setAllButtonsForMode(mode);
|
||||
}
|
||||
|
||||
function base64ToUint8Array(base64: string): Uint8Array {
|
||||
const binary = window.atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
|
||||
let index = 0;
|
||||
while (index < binary.length) {
|
||||
bytes[index] = binary.charCodeAt(index);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function updatePreviewImageFromBase64(encoded: string): void {
|
||||
const bytes = base64ToUint8Array(encoded);
|
||||
const copied = new Uint8Array(bytes.byteLength);
|
||||
|
||||
copied.set(bytes);
|
||||
|
||||
const blob = new Blob([copied.buffer], { type: "image/jpeg" });
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
|
||||
if (previewObjectUrl !== null) {
|
||||
URL.revokeObjectURL(previewObjectUrl);
|
||||
}
|
||||
|
||||
previewObjectUrl = objectUrl;
|
||||
if (previewElement)
|
||||
previewElement.src = objectUrl;
|
||||
}
|
||||
|
||||
async function refreshPreviewFrame(): Promise<void> {
|
||||
if (previewRequestInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentMode !== "video" && currentMode !== "av") {
|
||||
return;
|
||||
}
|
||||
|
||||
previewRequestInFlight = true;
|
||||
|
||||
try {
|
||||
@@ -73,8 +174,6 @@ function startPreviewPolling(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("starting preview polling");
|
||||
|
||||
window.setTimeout(() => {
|
||||
void refreshPreviewFrame();
|
||||
}, 120);
|
||||
@@ -91,97 +190,133 @@ function stopPreviewPolling(): void {
|
||||
}
|
||||
|
||||
previewRequestInFlight = false;
|
||||
if (videoPreview)
|
||||
videoPreview.removeAttribute("src");
|
||||
|
||||
if (previewObjectUrl !== null) {
|
||||
URL.revokeObjectURL(previewObjectUrl);
|
||||
previewObjectUrl = null;
|
||||
}
|
||||
|
||||
if (previewElement)
|
||||
previewElement.removeAttribute("src");
|
||||
}
|
||||
|
||||
startAudioBtn.addEventListener("click", async () => {
|
||||
startAudioBtn.disabled = true;
|
||||
if (currentMode !== "idle") {
|
||||
setAudioStatus("Another mode is already running.");
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentMode("audio");
|
||||
|
||||
try {
|
||||
const path = await invoke<string>("start_audio_recording");
|
||||
setAudioStatus(`Audio recording started.\nOutput: ${path}`);
|
||||
setAudioButtons(true);
|
||||
} catch (error) {
|
||||
setAudioStatus(`Start audio failed.\n${String(error)}`);
|
||||
setAudioButtons(false);
|
||||
setCurrentMode("idle");
|
||||
}
|
||||
});
|
||||
|
||||
stopAudioBtn.addEventListener("click", async () => {
|
||||
if (currentMode !== "audio") {
|
||||
setAudioStatus("Audio mode is not running.");
|
||||
return;
|
||||
}
|
||||
|
||||
stopAudioBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const path = await invoke<string>("stop_audio_recording");
|
||||
setAudioStatus(`Audio recording stopped.\nSaved file: ${path}`);
|
||||
setAudioButtons(false);
|
||||
setCurrentMode("idle");
|
||||
} catch (error) {
|
||||
setAudioStatus(`Stop audio failed.\n${String(error)}`);
|
||||
setAudioButtons(true);
|
||||
setCurrentMode("audio");
|
||||
}
|
||||
});
|
||||
|
||||
startVideoBtn.addEventListener("click", async () => {
|
||||
startVideoBtn.disabled = true;
|
||||
if (currentMode !== "idle") {
|
||||
setVideoStatus("Another mode is already running.");
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentMode("video");
|
||||
|
||||
try {
|
||||
const path = await invoke<string>("start_video_recording");
|
||||
setVideoStatus(`Video recording started.\nOutput: ${path}`);
|
||||
setVideoButtons(true);
|
||||
startPreviewPolling();
|
||||
} catch (error) {
|
||||
setVideoStatus(`Start video failed.\n${String(error)}`);
|
||||
setVideoButtons(false);
|
||||
stopPreviewPolling();
|
||||
setCurrentMode("idle");
|
||||
}
|
||||
});
|
||||
|
||||
stopVideoBtn.addEventListener("click", async () => {
|
||||
if (currentMode !== "video") {
|
||||
setVideoStatus("Video mode is not running.");
|
||||
return;
|
||||
}
|
||||
|
||||
stopVideoBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const path = await invoke<string>("stop_video_recording");
|
||||
setVideoStatus(`Video recording stopped.\nSaved file: ${path}`);
|
||||
setVideoButtons(false);
|
||||
stopPreviewPolling();
|
||||
setCurrentMode("idle");
|
||||
} catch (error) {
|
||||
setVideoStatus(`Stop video failed.\n${String(error)}`);
|
||||
setVideoButtons(true);
|
||||
setCurrentMode("video");
|
||||
}
|
||||
});
|
||||
|
||||
let previewObjectUrl: string | null = null;
|
||||
|
||||
function base64ToUint8Array(base64: string): Uint8Array {
|
||||
const binary = window.atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
|
||||
for (let index = 0;index < binary.length;index += 1) {
|
||||
bytes[index] = binary.charCodeAt(index);
|
||||
startAvBtn.addEventListener("click", async () => {
|
||||
if (currentMode !== "idle") {
|
||||
setAvStatus("Another mode is already running.");
|
||||
return;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
setCurrentMode("av");
|
||||
|
||||
function updatePreviewImageFromBase64(encoded: string): void {
|
||||
const bytes = base64ToUint8Array(encoded);
|
||||
const copied = new Uint8Array(bytes.byteLength);
|
||||
try {
|
||||
const result = await invoke<AvStartResponse>("start_av_recording");
|
||||
|
||||
copied.set(bytes);
|
||||
setAvStatus(
|
||||
`AV recording started.\nAudio: ${result.audio_path}\nVideo: ${result.video_path}`
|
||||
);
|
||||
|
||||
const blob = new Blob([copied.buffer], { type: "image/jpeg" });
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
startPreviewPolling();
|
||||
} catch (error) {
|
||||
setAvStatus(`Start AV failed.\n${String(error)}`);
|
||||
stopPreviewPolling();
|
||||
setCurrentMode("idle");
|
||||
}
|
||||
});
|
||||
|
||||
if (previewObjectUrl !== null) {
|
||||
URL.revokeObjectURL(previewObjectUrl);
|
||||
stopAvBtn.addEventListener("click", async () => {
|
||||
if (currentMode !== "av") {
|
||||
setAvStatus("AV mode is not running.");
|
||||
return;
|
||||
}
|
||||
|
||||
previewObjectUrl = objectUrl;
|
||||
if (videoPreview)
|
||||
videoPreview.src = objectUrl;
|
||||
}
|
||||
stopAvBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const result = await invoke<AvStopResponse>("stop_av_recording");
|
||||
|
||||
setAvStatus(
|
||||
`AV recording stopped.\nAudio: ${result.audio_path}\nVideo: ${result.video_path}`
|
||||
);
|
||||
|
||||
stopPreviewPolling();
|
||||
setCurrentMode("idle");
|
||||
} catch (error) {
|
||||
setAvStatus(`Stop AV failed.\n${String(error)}`);
|
||||
setCurrentMode("av");
|
||||
}
|
||||
});
|
||||
|
||||
setCurrentMode("idle");
|
||||
|
||||
@@ -27,6 +27,7 @@ body {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
3
frontend/ts/bindings/AvStartResponse.ts
Normal file
3
frontend/ts/bindings/AvStartResponse.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AvStartResponse = { audio_path: string, video_path: string, };
|
||||
3
frontend/ts/bindings/AvStopResponse.ts
Normal file
3
frontend/ts/bindings/AvStopResponse.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AvStopResponse = { audio_path: string, video_path: string, };
|
||||
Reference in New Issue
Block a user