Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/src/screenshot_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ impl ScreenshotEditorInstances {
}));
}
Err(e) => {
eprintln!("Failed to render frame: {e}");
tracing::error!("Failed to render screenshot frame: {e}");
}
}

Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/routes/camera.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -370,13 +370,13 @@ function LegacyCameraPreviewPage(props: { disconnected: Accessor<boolean> }) {
socket.onmessage = (event) => {
const buffer = event.data as ArrayBuffer;
const clamped = new Uint8ClampedArray(buffer);
if (clamped.length < 12) {
if (clamped.length < 24) {
console.error("Received frame too small to contain metadata");
return;
}

const metadataOffset = clamped.length - 12;
const meta = new DataView(buffer, metadataOffset, 12);
const metadataOffset = clamped.length - 24;
const meta = new DataView(buffer, metadataOffset, 24);
const strideBytes = meta.getUint32(0, true);
const height = meta.getUint32(4, true);
const width = meta.getUint32(8, true);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/routes/screenshot-editor/AnnotationLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,8 @@ export function AnnotationLayer(props: {
opacity: 1,
rotation: 0,
text: tool === "text" ? "Text" : null,
maskType: tool === "mask" ? "blur" : null,
maskLevel: tool === "mask" ? 16 : null,
maskType: tool === "mask" ? "pixelate" : null,
maskLevel: tool === "mask" ? 7 : null,
};

if (tool === "text") {
Expand Down
6 changes: 4 additions & 2 deletions apps/desktop/src/routes/screenshot-editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { useScreenshotEditorContext } from "./context";
import { Header } from "./Header";
import { LayersPanel } from "./LayersPanel";
import { Preview } from "./Preview";
import { ScreenshotEditorSkeleton } from "./screenshot-editor-skeleton";
import { Dialog, EditorButton } from "./ui";

export function Editor() {
Expand All @@ -43,6 +44,7 @@ export function Editor() {
setLayersPanelOpen,
activePopover,
setActivePopover,
isRenderReady,
} = useScreenshotEditorContext();

createEffect(() => {
Expand Down Expand Up @@ -132,7 +134,7 @@ export function Editor() {
});

return (
<>
<Show when={isRenderReady()} fallback={<ScreenshotEditorSkeleton />}>
<div class="relative">
<Header />
<AnnotationConfigBar />
Expand All @@ -151,7 +153,7 @@ export function Editor() {
</div>
<Dialogs />
</div>
</>
</Show>
);
}

Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/routes/screenshot-editor/arrow.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export function getArrowHeadSize(strokeWidth: number) {
const width = Math.max(1, strokeWidth);
const length = Math.max(12, width * 3);
const headWidth = Math.max(8, width * 2);
const length = Math.max(20, width * 6);
const headWidth = Math.max(14, width * 5);
return { length, width: headWidth };
}

Expand Down
84 changes: 73 additions & 11 deletions apps/desktop/src/routes/screenshot-editor/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ import {
onCleanup,
} from "solid-js";
import { createStore, reconcile, unwrap } from "solid-js/store";
import {
createImageDataWS,
createLazySignal,
type FrameData,
} from "~/utils/socket";
import { createLazySignal, type FrameData } from "~/utils/socket";
import {
type Annotation,
type AnnotationType,
Expand Down Expand Up @@ -86,8 +82,9 @@ const DEFAULT_HOTKEYS: HotkeysConfiguration = {
const DEFAULT_PROJECT: ScreenshotProject = {
background: {
source: {
type: "wallpaper",
path: "macOS/sequoia-dark",
type: "color",
value: [255, 255, 255],
alpha: 255,
},
blur: 0,
padding: 20,
Expand Down Expand Up @@ -137,6 +134,8 @@ function createScreenshotEditorContext() {
});

const [latestFrame, setLatestFrame] = createLazySignal<FrameData>();
const [isRenderReady, setIsRenderReady] = createSignal(false);
let wsRef: WebSocket | null = null;

const [editorInstance] = createResource(async () => {
const instance = await commands.createScreenshotEditorInstance();
Expand All @@ -148,13 +147,22 @@ function createScreenshotEditorContext() {
}
}

let hasReceivedWebSocketFrame = false;

if (instance.path) {
const img = new Image();
img.crossOrigin = "anonymous";
img.src = convertFileSrc(instance.path);
img.onload = async () => {
if (hasReceivedWebSocketFrame) {
return;
}
try {
const bitmap = await createImageBitmap(img);
if (hasReceivedWebSocketFrame) {
bitmap.close();
return;
}
const existing = latestFrame();
if (existing?.bitmap) {
existing.bitmap.close();
Expand All @@ -164,6 +172,7 @@ function createScreenshotEditorContext() {
height: img.naturalHeight,
bitmap,
});
setIsRenderReady(true);
} catch (e: unknown) {
console.error("Failed to create ImageBitmap from fallback image:", e);
}
Expand All @@ -177,10 +186,58 @@ function createScreenshotEditorContext() {
};
}

const [_ws, _isConnected, _isWorkerReady] = createImageDataWS(
instance.framesSocketUrl,
setLatestFrame,
);
const ws = new WebSocket(instance.framesSocketUrl);
wsRef = ws;
ws.binaryType = "arraybuffer";
ws.onmessage = async (event) => {
const buffer = event.data as ArrayBuffer;
if (buffer.byteLength < 24) return;

const metadataOffset = buffer.byteLength - 24;
const meta = new DataView(buffer, metadataOffset, 24);
const strideBytes = meta.getUint32(0, true);
const height = meta.getUint32(4, true);
const width = meta.getUint32(8, true);

if (!width || !height) return;

hasReceivedWebSocketFrame = true;
setIsRenderReady(true);

const expectedRowBytes = width * 4;
const frameData = new Uint8ClampedArray(
buffer,
0,
buffer.byteLength - 24,
);

let processedData: Uint8ClampedArray;
if (strideBytes === expectedRowBytes) {
processedData = frameData.subarray(0, expectedRowBytes * height);
} else {
processedData = new Uint8ClampedArray(expectedRowBytes * height);
for (let row = 0; row < height; row++) {
const srcStart = row * strideBytes;
const destStart = row * expectedRowBytes;
processedData.set(
frameData.subarray(srcStart, srcStart + expectedRowBytes),
destStart,
);
}
}

try {
const imageData = new ImageData(processedData, width, height);
const bitmap = await createImageBitmap(imageData);
const existing = latestFrame();
if (existing?.bitmap && existing.bitmap !== bitmap) {
existing.bitmap.close();
}
setLatestFrame({ width, height, bitmap });
} catch (e) {
console.error("Failed to create ImageBitmap from frame:", e);
}
};

return instance;
});
Expand All @@ -198,6 +255,10 @@ function createScreenshotEditorContext() {
if (frame?.bitmap) {
frame.bitmap.close();
}
if (wsRef) {
wsRef.close();
wsRef = null;
}
});

const saveConfig = debounce((config: ProjectConfiguration) => {
Expand Down Expand Up @@ -362,6 +423,7 @@ function createScreenshotEditorContext() {
dialog,
setDialog,
latestFrame,
isRenderReady,
editorInstance,
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { type as ostype } from "@tauri-apps/plugin-os";
import { cx } from "cva";
import IconCapLogo from "~icons/cap/logo";

function SkeletonPulse(props: { class?: string }) {
return (
<div
class={cx("animate-pulse rounded bg-gray-3 dark:bg-gray-4", props.class)}
/>
);
}

function SkeletonButton(props: { class?: string; width?: string }) {
return (
<SkeletonPulse
class={cx("h-9 rounded-lg", props.width ?? "w-9", props.class)}
/>
);
}

function HeaderSkeleton() {
return (
<div
data-tauri-drag-region
class="flex relative flex-row items-center w-full h-14 px-4 border-b border-gray-3 bg-gray-1 dark:bg-gray-2 shrink-0 z-20 gap-4 justify-between"
>
<div class="flex items-center gap-4">
{ostype() === "macos" && <div class="w-14" />}
</div>

<div class="flex items-center gap-2 absolute left-1/2 -translate-x-1/2">
<SkeletonButton width="w-24" />
<SkeletonButton />
<div class="w-px h-6 bg-gray-4 mx-1" />
<SkeletonButton />
<SkeletonButton />
<SkeletonButton />
<SkeletonButton />
<SkeletonButton />
<div class="w-px h-6 bg-gray-4 mx-1" />
<SkeletonButton />
<SkeletonButton />
<SkeletonButton />
<SkeletonButton />
<SkeletonButton />
</div>

<div
class={cx(
"flex flex-row items-center gap-2",
ostype() !== "windows" && "pr-2",
)}
>
<div class="w-px h-6 bg-gray-4 mx-1" />
<SkeletonButton />
<SkeletonButton />
<SkeletonButton />
</div>
</div>
);
}

function PreviewSkeleton() {
return (
<div class="flex flex-col flex-1 overflow-hidden bg-gray-1 dark:bg-gray-2">
<div class="flex-1 relative flex items-center justify-center overflow-hidden bg-gray-2 dark:bg-gray-3">
<div class="absolute left-4 bottom-4 z-10 flex items-center gap-2 bg-gray-1 dark:bg-gray-3 rounded-lg shadow-sm p-1 border border-gray-4">
<SkeletonButton />
<SkeletonPulse class="w-20 h-2 rounded-full" />
<SkeletonButton />
</div>

<div class="flex items-center justify-center">
<div class="animate-spin">
<IconCapLogo class="size-12 text-gray-400 opacity-50" />
</div>
</div>
</div>
</div>
);
}

export function ScreenshotEditorSkeleton() {
return (
<>
<div class="relative">
<HeaderSkeleton />
</div>
<div
class="flex overflow-y-hidden flex-1 gap-0 pb-0 w-full min-h-0 leading-5"
data-tauri-drag-region
>
<div class="flex overflow-hidden flex-col flex-1 min-h-0">
<div class="flex overflow-y-hidden flex-row flex-1 min-h-0">
<PreviewSkeleton />
</div>
</div>
</div>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export function useScreenshotExport() {
ctx.drawImage(frame.bitmap, 0, 0);
} else {
const img = new Image();
img.crossOrigin = "anonymous";
img.src = convertFileSrc(editorCtx.path);
await new Promise((resolve, reject) => {
img.onload = resolve;
Expand Down
6 changes: 4 additions & 2 deletions crates/audio/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ pub fn render_audio(
}
}

out[out_offset + i * 2] = left;
out[out_offset + i * 2 + 1] = right;
let l = left.clamp(-1.0, 1.0);
let r = right.clamp(-1.0, 1.0);
out[out_offset + i * 2] = l;
out[out_offset + i * 2 + 1] = r;
}

samples
Expand Down
2 changes: 1 addition & 1 deletion crates/editor/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ pub struct AudioPlaybackBuffer<T: FromSampleBytes> {
}

impl<T: FromSampleBytes> AudioPlaybackBuffer<T> {
pub const PLAYBACK_SAMPLES_COUNT: u32 = 256;
pub const PLAYBACK_SAMPLES_COUNT: u32 = 512;
pub const WIRELESS_PLAYBACK_SAMPLES_COUNT: u32 = 1024;
const PROCESSING_SAMPLES_COUNT: u32 = 1024;

Expand Down
6 changes: 3 additions & 3 deletions crates/editor/src/playback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -803,14 +803,14 @@ impl AudioPlayback {

let candidate_order = if is_wireless {
vec![
BufferSizeStrategy::DeviceDefault,
BufferSizeStrategy::Fixed(wireless_samples_count),
BufferSizeStrategy::Fixed(default_samples_count),
BufferSizeStrategy::DeviceDefault,
]
} else {
vec![
BufferSizeStrategy::Fixed(default_samples_count),
BufferSizeStrategy::DeviceDefault,
BufferSizeStrategy::Fixed(default_samples_count),
]
};

Expand Down Expand Up @@ -935,7 +935,7 @@ impl AudioPlayback {
let headroom_for_stream = headroom_samples;
let mut playhead_rx_for_stream = playhead_rx.clone();
let mut last_video_playhead = playhead;
const SYNC_THRESHOLD_SECS: f64 = 0.05;
const SYNC_THRESHOLD_SECS: f64 = 0.12;

let stream_result = device.build_output_stream(
&config,
Expand Down
Loading