Skip to content

Commit 2f1be03

Browse files
committed
docs: replace demo gif with recorded mp4
1 parent d22761c commit 2f1be03

8 files changed

Lines changed: 194 additions & 3 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141

4242
![Workspace Overview](docs/help/assets/screenshot-workspace-overview.png)
4343

44-
![Demo](docs/assets/demo.gif)
44+
[![Watch Demo Video](docs/assets/demo-poster.png)](docs/assets/demo.mp4)
45+
46+
<div align="center">Watch the recorded demo video</div>
4547

4648

4749
---

README.zh-CN.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141

4242
![工作区界面](docs/help/assets/screenshot-workspace-overview.png)
4343

44-
![Demo](docs/assets/demo.gif)
44+
[![观看演示视频](docs/assets/demo-poster.png)](docs/assets/demo.mp4)
45+
46+
<div align="center">点击封面查看录制演示视频</div>
4547

4648

4749
---

docs/assets/demo-poster.png

126 KB
Loading

docs/assets/demo.gif

-13.2 KB
Binary file not shown.

docs/assets/demo.mp4

100 KB
Binary file not shown.

packages/web/src/main.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Application Entry Point
33
*/
44

5+
import "@vitejs/plugin-react/preamble";
56
import { Provider } from "jotai";
67
import React from "react";
78
import ReactDOM from "react-dom/client";

packages/web/vite.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import react from "@vitejs/plugin-react";
22
import path from "path";
3-
import { defineConfig } from "vitest/config";
3+
import { defineConfig } from "vite";
44

55
const backendHttpTarget = process.env.VITE_BACKEND_HTTP_URL || "http://127.0.0.1:4173";
66
const backendWsTarget = process.env.VITE_BACKEND_WS_URL || "ws://127.0.0.1:4173";
7+
const disableHmr = process.env.CODER_STUDIO_DISABLE_HMR === "true";
78

89
export default defineConfig({
910
plugins: [react()],
@@ -15,6 +16,7 @@ export default defineConfig({
1516
server: {
1617
port: 5173,
1718
host: true,
19+
hmr: disableHmr ? false : undefined,
1820
proxy: {
1921
"/ws": {
2022
target: backendWsTarget,

scripts/demo/record-demo.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { spawn } from "node:child_process";
2+
import { constants } from "node:fs";
3+
import { access, mkdir, readdir, rename, rm } from "node:fs/promises";
4+
import { join } from "node:path";
5+
import { chromium, type Page } from "playwright";
6+
7+
const OUTPUT_DIR = join(process.cwd(), "docs", "assets");
8+
const OUTPUT_VIDEO = join(OUTPUT_DIR, "demo.mp4");
9+
const OUTPUT_POSTER = join(OUTPUT_DIR, "demo-poster.png");
10+
const BASE_URL = process.env.DEMO_BASE_URL ?? "http://127.0.0.1:5173";
11+
12+
const SCENES = [
13+
"ui-preview.html?scene=workspace-desktop&theme=mint-dark&locale=en&device=desktop",
14+
"ui-preview.html?scene=command-palette&theme=mint-dark&locale=en&device=desktop",
15+
"ui-preview.html?scene=workspace-launch-modal&theme=mint-dark&locale=en&device=desktop",
16+
"ui-preview.html?scene=workspace-mobile&theme=mint-dark&locale=en&device=mobile",
17+
] as const;
18+
19+
function sleep(ms: number): Promise<void> {
20+
return new Promise((resolve) => setTimeout(resolve, ms));
21+
}
22+
23+
async function pathExists(path: string): Promise<boolean> {
24+
try {
25+
await access(path, constants.F_OK);
26+
return true;
27+
} catch {
28+
return false;
29+
}
30+
}
31+
32+
function runCommand(command: string, args: string[]): Promise<void> {
33+
return new Promise((resolve, reject) => {
34+
const child = spawn(command, args, { stdio: "inherit" });
35+
child.once("error", reject);
36+
child.once("close", (code) => {
37+
if (code === 0) {
38+
resolve();
39+
return;
40+
}
41+
reject(new Error(`${command} exited with code ${code ?? -1}`));
42+
});
43+
});
44+
}
45+
46+
async function waitForUrl(page: Page, url: string): Promise<void> {
47+
for (let attempt = 0; attempt < 30; attempt += 1) {
48+
try {
49+
const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: 5_000 });
50+
if (response?.ok()) {
51+
await page.waitForLoadState("networkidle");
52+
return;
53+
}
54+
} catch {}
55+
await sleep(500);
56+
}
57+
58+
throw new Error(`Unable to reach ${url}`);
59+
}
60+
61+
async function cleanupOldArtifacts(): Promise<void> {
62+
await mkdir(OUTPUT_DIR, { recursive: true });
63+
await rm(OUTPUT_VIDEO, { force: true });
64+
await rm(OUTPUT_POSTER, { force: true });
65+
66+
const files = await readdir(OUTPUT_DIR);
67+
await Promise.all(
68+
files
69+
.filter((file) => file.endsWith(".webm"))
70+
.map((file) => rm(join(OUTPUT_DIR, file), { force: true }))
71+
);
72+
}
73+
74+
async function captureScene(page: Page, scenePath: string, index: number): Promise<void> {
75+
const url = `${BASE_URL}/${scenePath}`;
76+
await waitForUrl(page, url);
77+
await sleep(1200);
78+
79+
if (index === 0) {
80+
await page.screenshot({ path: OUTPUT_POSTER, fullPage: false });
81+
}
82+
83+
if (scenePath.includes("workspace-desktop")) {
84+
await page.mouse.move(180, 150);
85+
await sleep(500);
86+
await page.mouse.move(620, 310, { steps: 12 });
87+
await sleep(1500);
88+
return;
89+
}
90+
91+
if (scenePath.includes("command-palette")) {
92+
await page.mouse.move(610, 240);
93+
await sleep(500);
94+
await page.keyboard.press("ArrowDown");
95+
await sleep(400);
96+
await page.keyboard.press("ArrowDown");
97+
await sleep(1200);
98+
return;
99+
}
100+
101+
if (scenePath.includes("workspace-launch-modal")) {
102+
await page.mouse.move(530, 410);
103+
await sleep(500);
104+
await page.mouse.wheel(0, 320);
105+
await sleep(1200);
106+
return;
107+
}
108+
109+
if (scenePath.includes("workspace-mobile")) {
110+
await page.mouse.move(250, 720);
111+
await sleep(600);
112+
await page.mouse.move(250, 120, { steps: 18 });
113+
await sleep(1200);
114+
}
115+
}
116+
117+
async function record(): Promise<string> {
118+
await cleanupOldArtifacts();
119+
120+
const browser = await chromium.launch({ headless: true });
121+
const context = await browser.newContext({
122+
viewport: { width: 1440, height: 900 },
123+
recordVideo: {
124+
dir: OUTPUT_DIR,
125+
size: { width: 1440, height: 900 },
126+
},
127+
});
128+
const page = await context.newPage();
129+
130+
for (const [index, scenePath] of SCENES.entries()) {
131+
await captureScene(page, scenePath, index);
132+
}
133+
134+
const video = page.video();
135+
await context.close();
136+
await browser.close();
137+
138+
const recordedPath = await video?.path();
139+
if (!recordedPath || !(await pathExists(recordedPath))) {
140+
throw new Error("Playwright did not produce a video file");
141+
}
142+
143+
return recordedPath;
144+
}
145+
146+
async function transcodeToMp4(inputPath: string): Promise<void> {
147+
const tempPath = join(OUTPUT_DIR, "demo-source.webm");
148+
if (inputPath !== tempPath) {
149+
await rename(inputPath, tempPath);
150+
}
151+
152+
await runCommand("ffmpeg", [
153+
"-y",
154+
"-i",
155+
tempPath,
156+
"-vf",
157+
"scale=1280:-2",
158+
"-c:v",
159+
"libx264",
160+
"-preset",
161+
"slow",
162+
"-crf",
163+
"28",
164+
"-pix_fmt",
165+
"yuv420p",
166+
"-movflags",
167+
"+faststart",
168+
OUTPUT_VIDEO,
169+
]);
170+
171+
await rm(tempPath, { force: true });
172+
}
173+
174+
async function main(): Promise<void> {
175+
const recordedPath = await record();
176+
await transcodeToMp4(recordedPath);
177+
console.log(`Saved video to ${OUTPUT_VIDEO}`);
178+
console.log(`Saved poster to ${OUTPUT_POSTER}`);
179+
}
180+
181+
main().catch((error) => {
182+
console.error(error instanceof Error ? error.message : String(error));
183+
process.exit(1);
184+
});

0 commit comments

Comments
 (0)