diff --git a/crates/compositor/src/pipeline_macos.rs b/crates/compositor/src/pipeline_macos.rs index 638b664f4..c162703f1 100644 --- a/crates/compositor/src/pipeline_macos.rs +++ b/crates/compositor/src/pipeline_macos.rs @@ -292,7 +292,11 @@ impl Decoder { _ if intent == DecodeIntent::Export && is_h264_8bit => false, _ => true, }; - let r = if want_hw { + // A VideoToolbox device can be created even when this codec has no + // VideoToolbox decoder (notably VP8). In that case get_format selects + // CPU frames, but cpu=None would send them straight to the Metal + // compositor without its required NV12/CVPixelBuffer conversion. + let r = if want_hw && codec_supports_videotoolbox(dec) { crate::ffi::av_hwdevice_ctx_create( &mut hwdev, crate::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX, @@ -685,6 +689,43 @@ unsafe extern "C" fn get_hw_format_macos( *pix_fmts } +unsafe fn codec_supports_videotoolbox(codec: *const crate::ffi::AVCodec) -> bool { + if codec.is_null() { + return false; + } + let mut index = 0; + loop { + let config = crate::ffi::avcodec_get_hw_config(codec, index); + if config.is_null() { + return false; + } + if (*config).device_type == crate::ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX + && (*config).pix_fmt == crate::ffi::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX + { + return true; + } + index += 1; + } +} + +#[cfg(test)] +mod webcam_decode_tests { + use super::codec_supports_videotoolbox; + use crate::ffi::{avcodec_find_decoder, AVCodecID}; + + #[test] + fn vp8_uses_cpu_presentation_while_h264_can_use_videotoolbox() { + unsafe { + let vp8 = avcodec_find_decoder(AVCodecID::AV_CODEC_ID_VP8); + let h264 = avcodec_find_decoder(AVCodecID::AV_CODEC_ID_H264); + assert!(!vp8.is_null()); + assert!(!h264.is_null()); + assert!(!codec_supports_videotoolbox(vp8)); + assert!(codec_supports_videotoolbox(h264)); + } + } +} + /// Source clip pour `run_composited_multi`. Mêmes champs que `pipeline_windows::ClipSource`. pub struct ClipSource { pub screen: String, diff --git a/electron/recording/webm-duration.test.ts b/electron/recording/webm-duration.test.ts index 9c742a9b2..16684b09f 100644 --- a/electron/recording/webm-duration.test.ts +++ b/electron/recording/webm-duration.test.ts @@ -85,7 +85,7 @@ describe("webm-duration patching", () => { return file.source; } - it("patches small WebM files under 2MB in memory successfully", async () => { + it("patches small WebM files under 2MB successfully", async () => { const webmBytes = createDummyWebm(true, 100); const filePath = pathFor("small.webm"); await writeFile(filePath, webmBytes); @@ -104,6 +104,20 @@ describe("webm-duration patching", () => { expect(duration.getValue()).toBe(5000); }); + it("preserves successive unknown-sized Clusters in a small MediaRecorder file", async () => { + const header = createDummyWebm(false); + const cluster = Buffer.from("1f43b67501ffffffffffffffe78100a3858100008000", "hex"); + const clusters = Buffer.concat([cluster, cluster]); + const filePath = pathFor("live-clusters.webm"); + await writeFile(filePath, Buffer.concat([header, clusters])); + + expect(await patchWebmDurationOnDisk(filePath, 2000)).toEqual({ patched: true }); + const patched = await readFile(filePath); + const firstCluster = patched.indexOf(Buffer.from("1f43b675", "hex")); + expect(firstCluster).toBeGreaterThan(0); + expect(patched.subarray(firstCluster)).toEqual(clusters); + }); + it("patches large WebM files over 2MB using the optimized streaming method successfully", async () => { // Create a file larger than 2MB (e.g. 2.5MB cluster data) to trigger the optimized method const webmBytes = createDummyWebm(true, 2.5 * 1024 * 1024); diff --git a/electron/recording/webm-duration.ts b/electron/recording/webm-duration.ts index dbca70a86..a238812e0 100644 --- a/electron/recording/webm-duration.ts +++ b/electron/recording/webm-duration.ts @@ -32,12 +32,10 @@ export async function patchWebmDurationOnDisk( let fileHandle: fs.FileHandle | null = null; const tmpPath = `${filePath}.duration-patch.tmp`; try { - const stat = await fs.stat(filePath); - if (stat.size < HEADER_CHUNK_SIZE) { - // Fallback: If file is smaller than 2MB, just use the in-memory method. - return await patchWebmDurationInMemory(filePath, durationMs); - } - + // Use the header-only splice even for small recordings. The parser treats an + // unknown-sized Cluster as opaque bytes, then serializes it with a finite + // size enclosing every later Cluster. That corrupts MediaRecorder's stream. + // Keeping all Cluster bytes verbatim preserves their original boundaries. fileHandle = await fs.open(filePath, "r"); const buffer = Buffer.alloc(HEADER_CHUNK_SIZE); const { bytesRead } = await fileHandle.read(buffer, 0, HEADER_CHUNK_SIZE, 0); diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index c5156749b..c2ef9296b 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -26,12 +26,12 @@ import { DEFAULT_WEBCAM_QUALITY, WEBCAM_TARGET_FRAME_RATE, type WebcamQualityId, - webcamBitrateForStream, webcamPresetFor, webcamQualityFrom, webcamVideoConstraints, } from "./webcamCaptureTarget"; import { webcamDeviceIdentityFrom } from "./webcamDeviceIdentity"; +import { webcamRecorderOptions as cameraRecorderOptions } from "./webcamRecorderOptions"; const TARGET_FRAME_RATE = 60; const MIN_FRAME_RATE = 30; @@ -387,10 +387,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn { * three and left the browser pipeline encoding a 2160p camera at the * monitor's rate, capped well below what the frame needs. */ - const webcamRecorderOptions = (stream: MediaStream | null): MediaRecorderOptions => ({ - mimeType: selectMimeType(), - videoBitsPerSecond: webcamBitrateForStream(stream), - }); + const webcamRecorderOptions = (stream: MediaStream | null): MediaRecorderOptions => + cameraRecorderOptions(stream, window.electronAPI.getPlatform()); const selectMimeType = () => { // H.264 first: hardware-accelerated, so sharp real-time output. AV1/VP9 are diff --git a/src/hooks/webcamRecorderOptions.test.ts b/src/hooks/webcamRecorderOptions.test.ts new file mode 100644 index 000000000..0c2ffb8c8 --- /dev/null +++ b/src/hooks/webcamRecorderOptions.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { webcamRecorderOptions } from "./webcamRecorderOptions"; + +describe("webcam recorder options", () => { + it("avoids the macOS H.264 camera encoder even when it advertises support", () => { + expect(webcamRecorderOptions(null, "darwin", () => true).mimeType).toBe( + "video/webm;codecs=vp8", + ); + }); + + it.each(["win32", "linux"])("keeps H.264 preferred on %s", (platform) => { + expect(webcamRecorderOptions(null, platform, () => true).mimeType).toBe( + "video/webm;codecs=h264", + ); + }); + + it("negotiates another supported codec when VP8 is unavailable", () => { + expect(webcamRecorderOptions(null, "darwin", (type) => type.endsWith("vp9")).mimeType).toBe( + "video/webm;codecs=vp9", + ); + }); + + it("lets MediaRecorder negotiate when no explicit codec is supported", () => { + expect(webcamRecorderOptions(null, "darwin", () => false)).not.toHaveProperty("mimeType"); + }); + + it("uses the delivered camera dimensions for bitrate", () => { + const stream = { + getVideoTracks: () => [{ getSettings: () => ({ width: 1920, height: 1080 }) }], + } as unknown as MediaStream; + expect(webcamRecorderOptions(stream, "darwin", () => true).videoBitsPerSecond).toBe(16_000_000); + }); +}); diff --git a/src/hooks/webcamRecorderOptions.ts b/src/hooks/webcamRecorderOptions.ts new file mode 100644 index 000000000..9d27db468 --- /dev/null +++ b/src/hooks/webcamRecorderOptions.ts @@ -0,0 +1,17 @@ +import { webcamBitrateForStream } from "./webcamCaptureTarget"; + +/** Keep webcam encoding independent of the screen recorder's codec preference. */ +export function webcamRecorderOptions( + stream: MediaStream | null, + platform: string, + isTypeSupported: (mimeType: string) => boolean = (type) => MediaRecorder.isTypeSupported(type), +): MediaRecorderOptions { + // Chromium's macOS H.264 camera encoder can stop delivering frames while the + // stream remains live. VP8 avoids that path; native screen capture still uses H.264. + const codecs = platform === "darwin" ? ["vp8", "vp9", "h264"] : ["h264", "vp8", "vp9"]; + const mimeType = codecs.map((codec) => `video/webm;codecs=${codec}`).find(isTypeSupported); + return { + ...(mimeType ? { mimeType } : {}), + videoBitsPerSecond: webcamBitrateForStream(stream), + }; +} diff --git a/technical-documentation/testing/manual-e2e-checklist.md b/technical-documentation/testing/manual-e2e-checklist.md index 13ff91307..48ac24e42 100644 --- a/technical-documentation/testing/manual-e2e-checklist.md +++ b/technical-documentation/testing/manual-e2e-checklist.md @@ -983,3 +983,4 @@ These checks are the human slice: an agent cannot click Apple's picker (see abov | 2026-10-16 | `v2.1.0-rc.3`, `release/v2.1.0` @ `523a3e4`, CI artifact `openscreen-linux` from build run 38878886935 (`Linux packages` succeeded; the run is red on `macOS x64 DMG`, so `Publish GitHub release` was skipped). `Openscreen-Linux-2.1.0-rc.3.AppImage` sha512 matched `latest-linux.yml`; run directly, not installed (the `.deb` needs a sudo password; FUSE3 mounts it, AppRun adds `--no-sandbox` because Ubuntu 24.04 restricts unprivileged user namespaces). Save Diagnostics reports `2.1.0-rc.3`, `channel: appimage`. The rc.1 `.deb` stayed installed for A/B runs | Ubuntu 24.04, GNOME Shell 46 **Wayland**, AMD Radeon 610M (Vulkan/VAAPI), 1920×1080 @ 60.001 Hz, USB2.0 HD UVC webcam | **Partial — targeted pass on the diff since rc.1: the 13 listed items pass on their scenario (item 1 only after a session reload); 2 pre-existing P2 defects found, neither a regression from rc.1** | **Driver:** the `computer-use-linux` 0.7.16 MCP that Claude Code starts over SSH inherits `XDG_SESSION_TYPE=tty` and no `WAYLAND_DISPLAY`, so it has no portal input and its click falls back to a missing `ydotool`. The same plugin binary, started from the shell with `XDG_SESSION_TYPE=wayland WAYLAND_DISPLAY=wayland-0` behind a small socket bridge, got one RemoteDesktop grant (pointer and keyboard) for the whole run. Its click takes a portal screenshot first, so **the whole screen flashes white ~100 ms on every click** (scroll, drag and keys do not); no click fell inside a measured playback window. **Defects:** (a) **Chat rewind fails on a message sent in the current view** (P2, pre-existing): ↶ → Rewind shows the toast "Message not found in this session.", and `chat.json` and the document stay unchanged. 6/6 attempts across 2 conversations, by the driver and by Etienne's own click. Cause, read in the code: `LeftPanel.tsx` seeds the user message as `local__u` and never swaps in the main-process id (`randomUUID()`, `chat-service.ts:397`). Messages are only reloaded from the main process when the active session changes, so rewind works again after switching conversations. rc.1 has the same code. Also, clicking ↶ scrolls the conversation to the bottom (the scroll effect has no deps), so the confirmation hangs detached at the top, clipped by the window edge. (b) **Preview after a seek on a held-frame (VFR) take** (P2, pre-existing): seek to 30 s and play, and the preview updates 1–2×/s for the rest of the clip (stills of 0.5–1.7 s, 2 A-B-A step-backs at the start) while the clock runs normally. Same with GOP 600 and GOP 60, and identical on rc.1. The CFR take after the same seek is fine, and the VFR take played from 0 is fine. **Passed:** - #1259 rewind, after a session reload: ↶ → Rewind on a two-operation turn shows "Rewound to the start of that message", puts the text back in the composer, and removes the annotation, the speed region and the later zoom. `chat.json` is rewritten without the message, which stays out after an app restart. The next turn ("Add a zoom from 10 to 12 seconds") leaves only that zoom in the document. - #1261 Smart cuts (wand → Smart cuts) on a 32.4 s espeak-ng-narrated clip with a local Whisper transcript (63 words): 3 dead-air cuts announced (4.99–6.94, 12.16–14.59, 25.56–27.52), exactly those 3 trims in the document. A typed two-operation turn (text "Hello" 1–3 s + 2× speed 15–20 s): both "applied" lines are in the document. - #1262 "Refit check" (64 px bold) stays on one line, unclipped, through Style None → Screen (Auto 1920×1508) → 9:16, in the preview and in `getFrames` (1080×1920). - #1266 Full Camera 23–30 s with a 1 s trim at 26–27 s, motion blur 0.2: `getFrames` at 25.95/27.00/27.02/27.05/28.00 s is clean, and so are paused frame steps across the cut and a 60 fps capture of the preview playing across it (488 frames, no ghost; the only large change is the region-end transition at 30 s). - #1245 Copy on the fragment after that cut, paste at 31 s: region 30.974–37.974 s = 7.0 s, the whole pill, where the fragment is 3 s. - #1253 `getFrames` rendered from the document (no export) over stdio `openscreen mcp`, 7 frames in 2 calls: no `window is not defined` in the CLI's or the app's log. - Save Diagnostics from the tray with a project open: 997 bytes before any take (`mainProcessLogs` 3, `helperOutput.linux` empty, `projectState` null, `recentLogs` []). After a take in the same instance: 5428 bytes, `helperOutput.linux` 1418 bytes (ready → capture-stopped, no cursor samples), 17 main-process lines including the helper's stderr, `projectState` null, no error (rc.1: 541 bytes). - Countdown: frame 0 of a full-screen take shows no digit (frames 0–5 identical at the centre; the first frame lands 2.7 s after Share). The overlay is still not centred ("3" at ≈(360,310)), the part known to be open. - HUD: "Hide recording bar" destroys the window (no dock icon, process alive) and tray → Open brings it back. Switched to vertical, the bar fits on screen (y 472–956). The rc.1 binary on the same machine still runs off the bottom. - Editor: opens maximized after a take; restore un-maximizes to 1320×964 (2/2, one after a maximized reopen). `editor-window.json` keeps 1320×964 over 3 launches (normal close `{96,54,1320×964,false}`, maximized close `{316,62,…,true}`), so no growth. - VAAPI first keyframe: full-screen take I-frame 204 KB, QP 13–21. Terminal window take at 3.1 Mbit/s (the rc.1 case), 1920×982: I-frame 185 KB, QP 13–30 with the cap reached in the lower half, text legible. **Preview smoothness** (#1189, #1282, #1285). Method: OpenScreen's PipeWire helper run standalone as a 60 fps monitor capture (VAAPI dmabuf, cursor hidden), counting runs of unchanged frames and A-B-A step-backs in the preview canvas (748×460, maximized editor). Reference: ffplay playing a 1080p60 pattern, captured the same way, changes at 60 Hz. Source: a 40 s 1080p60 take of a moving test pattern with two 8 s frozen segments, webcam PiP, pointer moving throughout. Project: 1.8× zoom 8–11 s, 2.2× orbit (3D) zoom 15–19 s, WGSL grayscale effect (`light`) 1–39 s, motion blur 0.2. - Normal (CFR) take, 2 full plays: **30 distinct frames/s** on the 60 Hz display throughout. Stills: 34 ms ×931, 51 ms ×79, 68 ms ×6. The only longer ones are a 350 ms stall at play start and a 2.9 s span where nothing in frame moves (pointer outside the 2.2× zoom per its telemetry, frozen pattern, still webcam scene). 0 step-backs; no still above 68 ms around the zoom transitions. GPU busy 30–55 %, so the 30 fps is not GPU saturation. **rc.1 on the same project is identical** (30 fps; 34 ms ×938, 51 ms ×77, 68 ms ×5; 383 ms start; 0 step-backs): the cadence is not new and rc.3 does not regress it. - Held-frame take (the same take made VFR with mpdecimate: holds of 6.7 s and 8 s, last frame at 34.9 s, clip 40 s, silent AAC to 40 s), played from 0 on rc.3: the preview keeps changing 30×/s through both holds (cursor and webcam move); max still 83 ms (once); 0 step-backs. In the trailing 5 s hold the pointer keeps moving until 0:40.0 (`02dbf13e`). Without the audio track (container ends at 34.93 s) playback stops at 0:34.9. **Also seen, not filed:** - stdio `listSources` raises the GNOME Share Screen picker and returns `displays: []` ("Failed to get sources" if cancelled), so a following `startRecording` raises a second picker. An MCP take has no countdown, so its first frame shows the picker fading out. - Closing the editor after a take quits the app ("the last window closed"), because the HUD is closed while the editor is up. - Tray → Open on a HUD that is open but buried shows "“openscreen” is ready" instead of raising it (once). - In 2 of 6 relaunches in vertical layout the HUD's last three buttons sat under the dock until it was dragged up. It still cannot go above y≈472 (rc.1 (4)). - Save Diagnostics leaves a Files window revealing the bundle. - Chat replies render markdown tables as raw text. - The app quit once on its own at 16:08:52 ("[app] quitting", clean, no crash) while text was being typed into the chat composer. Cause not established; Etienne was also at the screen. **Not covered:** Save Diagnostics from the menus (on Linux the editor is frameless with no native menu bar, and the OpenScreen menu has no such entry); the 64 KiB `helperOutput` cap (not reached at 1418 bytes); the in-app HTTP MCP server; the `.deb` install path; a full MP4 export; everything outside the 13 listed items (rc.1 row). Takes, projects, chat files, audio peaks, media-link entries and bundles from this pass were deleted, and `~/.config/openscreen` was restored from a backup taken before the pass. | | 2026-10-16 | `v2.1.0-rc.3`, `release/v2.1.0` @ `523a3e4`, DMG `Openscreen-macOS-Apple-Silicon-2.1.0-rc.3.dmg` from artifact `openscreen-mac-arm64` of build run 38878886935 (attempt 1). The pre-release was not published: the macOS x64 job failed on "Validate stapled DMG". DMG copied over by scp, SHA-512 identical to the source; `spctl` accepted, Notarized Developer ID (M4LK7C6S84); `stapler validate` OK. `CFBundleShortVersionString` 2.1.0-rc.3. The Save Diagnostics bundle reports `2.1.0-rc.3` · `dmg`, Electron 41.2.1, Chromium 146.0.7680.188, Node 24.14.1, darwin arm64 (About window not opened) | macOS 27.0.1 (26A434), Apple M1 (arm64), 8 GB, one 1920×1080 display at 120 Hz (1×); OBS Virtual Camera (no physical camera), BlackHole 2ch | **Partial — targeted pass on the changes since rc.1; 7 of 9 items pass, 2 P2 defects: #1266 in the export, rewind in the open conversation** | Real clicks and keystrokes through computer-use. Launched with `open -a`, `OPENSCREEN_MAC_SOURCE_PICKER=legacy`, `OPENSCREEN_DIAGNOSTIC=1` and `--remote-debugging-port`, the last only to read the `scripts/perf-ab/probe.js` probe (no injected action). Measured with the `ffmpeg` bundled in the app. avfoundation screen captures of the preview rectangle (120 fps asked, 75–79 fps obtained). Two HUD takes with the OBS webcam, microphone and system audio. **Defects:** - **#1266 in the export (P2)**: Full Camera 8.01–14.06 s with a cut at 10.00–11.39 s. The preview is clean at the cut (capture read frame by frame). In the exported MP4 (1080×1920, 60 fps, 1544 frames, 25.73 s = 27.12 − 1.39), frame 600 (t = 10.000 s, right at the cut at 10.001 s) shows the trail: the full-frame camera is doubled into several copies sliding toward its PiP corner. Frames 597–599 and 601–603 are clean. `getFrames` renders the trail at 10.000, 10.001, 10.017 and 10.033 s; 9.967 and 9.983 s are clean. It persists without the shader effect and in the Auto frame. On another project (Full Camera 9.95–17.95 s, cut at 13.93 s), the preview and `getFrames` at 13.90, 13.95 and 14.00 s are clean, but no sample lands exactly on the cut. - **Rewind (P2, predates rc.1)**: rewinding a message sent in the open conversation fails with "Message not found in this session." (2 tries) and undoes nothing. The renderer keeps the optimistic `local__u` id (`LeftPanel.tsx` l. 604–612); the real ids are only reloaded on a conversation switch. After Conversation 2 → Conversation 1, rewind works. This path dates from `be479097`, so not a regression (fixed since by #1292). **Passed:** - Rewind #1259, once the ids are reloaded: the message leaves the conversation and `chat.json`, its text comes back in the composer, and the "Rewind me" annotation disappears. The next turn ("zoom 20–22 s") adds only that zoom (`zoomRanges` 20000–22000), nothing from the rewound message. - Operations #1261 (DeepSeek `deepseek-v4-pro`): everything the agent reports "applied" is in the document. B&W effect 2000–9000 ms and 2× speed 20–23 s (`speedRegions`), B&W effect 5–12 s, zoom 20–22 s. *Smart cuts* (Auto-enhance menu): "added 4 trims", 4 trims present, none on a word (3.00–3.28, 11.83–12.46, 20.02–20.80, 24.30–24.54 s), about 2 min. - Annotation #1262: "Refit check rc3" at size 48 stays on one line, not clipped, in Auto then 9:16, in the preview (2 projects), the 9:16 MP4 export (frame 102, 1.70 s) and `getFrames` (0.5 s). - Pill copy #1245: a zoom cut by a clip boundary (second clip added from Media) gives two fragments, 23.866–36.330 and 36.330–45.080. Click on the 8.75 s fragment, Cmd+C, Cmd+V: the copy covers 50.089–71.303 = 21.214 s, the whole pill. A cut inside a single clip also gives 11.127 s = 11.127 s. - Save Diagnostics (OpenScreen app menu): bundle with `2.1.0-rc.3`, OS, versions, `helperOutput.mac` (811 chars) and 14 lines of `mainProcessLogs` since launch. `projectState` is null and the open project is absent. The brief's "editor document present" was removed on purpose by `dad3cca0`. - `getFrames` #1253 through a stdio client (`Openscreen mcp`) on the 2 projects, open and closed: frames rendered, no "window is not defined" or telemetry error in the main log or the MCP process's stderr. - macOS preview #1257: on Apple Silicon the Metal preview shows and moves (figures below). The Intel path is not testable here. **Preview smoothness (#1189, #1282, #1285), measured:** - Normal take (screen 1920×1080 at about 57 fps, webcam 30 fps, B&W 2–9 s, two 1.50× zooms, Full Camera, cut), playback 0–19 s. Probe: 587 paints in 19.8 s = 29.6/s; p50 33.3 ms, p95 34.8, p99 36.1; the only paint above 50 ms is the pause; 0 long task; all through readback (0 shared). Capture: 0 frame stepping back (A-B-A pattern); no gap above 45 ms around the zoom-in (5.15 s) and the zoom-out (16.28 s). Gaps of 67–123 ms appear only during Full Camera, where the OBS webcam source repeats its own frames (22.5 distinct frames/s, 26 gaps above 67 ms over 8–16 s). - Held-frame take (still screen: 8.7 distinct frames/s in the take, held up to 0.9 s; editable cursor; webcam; 1.80× zooms; B&W 5–12 s). Probe: 29.3/s, p95 34.5 ms. Capture: 28.6 visible changes/s, so the cursor and camera keep moving; 0 step back; largest gap 100 ms (twice), during a pointer pause where only the webcam moves. - Caveat: the preview paints 30 times a second on a 120 Hz display with a source at about 57 fps, and the capture tops out at 75–79 fps. **Also seen:** local transcription runs on `whispercpp-metal` (36.4 s of audio in 16.3 s). The export pipeline decodes H.264 in software. `getFrames` reports "clip #1 shortened by 0.113s": the clip added from Media lasts 36.33 s, the stream 36.217 s. **Not covered:** Apple's picker slice; the tray menu (no right-click); the About window; a physical camera; the Intel path; the rest of the checklist (earlier rows). The Mac restarted during setup; the whole pass ran after it. The test takes, projects and exports are in the Trash (`~/.Trash/openscreen-rc3-pass-1752/`); the HUD's camera and microphone are back to Off. | | 2026-10-16 | `v2.1.0-rc.4`, `release/v2.1.0` @ `89cdff2`, CI artifact `openscreen-windows-x64` from build run 38948496658. NSIS sha512 matched `update-info-win-x64.json`; About reads `2.1.0-rc.4`. | Windows 11 Home 26200 (x64), Ryzen 7 5800X, RTX 4070 Ti, 1920×1080 | **Pass — quick check of chat rewind (#1292) only** | Real OS input through computer-use, on a fresh 10 s take, with the chat provider deepseek-v4-flash. Each rewind ended with "Rewound to the start of that message": the message left the conversation and `chat.json`, its text returned to the composer and its edits were undone. No "Message not found" in the UI or the logs. - Rewind right after sending, with the agent still thinking: no edit arrived afterwards. - Rewind of a turn the agent had already answered (one zoom added): the zoom was removed. - Confirmation opened while the agent was answering and confirmed after its reply (three zooms added): all three were removed. **Not run:** - The hidden-rewind case (a turn with no checkpoint). With a document open, even a text-only turn ("pong") records a checkpoint, so its ↩ is shown and the rewind works. The UI offers no editor state without a document to test the no-checkpoint case. - The rest of the checklist (rc.3 row). | +| 2026-10-17 | Camera continuity fix, first tested on `v2.0.0` + local camera patch; ported to `main` @ `763cfedc` | macOS 27.0.1, Apple Silicon; Logitech C922 and built-in FaceTime camera | **Partial — camera capture, preview and MP4 export** | Computer-use capture-to-editor checks on the local build: 128 s VP8 webcam take, 3,828 frames, maximum PTS gap 68 ms; final installed-build 64 s take, 1,932 frames, maximum gap 66 ms. Before the codec change, H.264 webcam capture produced only 13 frames in 56 s. Native VP8 decoding presents the camera in preview and MP4 export. Autofocus untouched. Upstream port: focused recorder/WebM tests, both TypeScript checks and 4,909 unit tests pass (2 skipped). Not covered: desktop smoke test of the port on current main, Windows/Linux physical capture, and the rest of the checklist. A separate held-screen preview issue is addressed by upstream preview clock commits, outside this camera patch. |