@openvidstudio/capture 0.0.0-stage → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/settle.js ADDED
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_QUIET_MS = exports.DEFAULT_SETTLE_TIMEOUT_MS = void 0;
37
+ exports.settlePage = settlePage;
38
+ exports.settleSidecarPath = settleSidecarPath;
39
+ exports.writeSettleSidecar = writeSettleSidecar;
40
+ const fs = __importStar(require("node:fs"));
41
+ exports.DEFAULT_SETTLE_TIMEOUT_MS = 5000;
42
+ exports.DEFAULT_QUIET_MS = 150;
43
+ /**
44
+ * Runs in the page. Returns once fonts, images and finite animations are all done, or
45
+ * resolves early with what is still outstanding if the deadline passes first.
46
+ */
47
+ /* c8 ignore start -- executes in the browser, covered by the real-browser E2E */
48
+ const settleInPage = async (opts) => {
49
+ const deadline = Date.now() + opts.timeoutMs;
50
+ const left = () => Math.max(0, deadline - Date.now());
51
+ const withDeadline = (p, fallback) => Promise.race([p.catch(() => fallback), new Promise((r) => setTimeout(() => r(fallback), left()))]);
52
+ let fontsReady = true;
53
+ if (opts.fonts && typeof document.fonts !== "undefined") {
54
+ fontsReady = await withDeadline(document.fonts.ready.then(() => true), false);
55
+ }
56
+ let imagesReady = true;
57
+ if (opts.images) {
58
+ const pending = Array.from(document.images).filter((img) => !img.complete);
59
+ imagesReady = await withDeadline(Promise.all(pending.map((img) => img
60
+ .decode()
61
+ .catch(() => undefined)
62
+ .then(() => undefined))).then(() => true), false);
63
+ }
64
+ let pendingAnimations = 0;
65
+ let loopingAnimations = 0;
66
+ if (opts.animations && typeof document.getAnimations === "function") {
67
+ const running = document.getAnimations().filter((a) => a.playState === "running");
68
+ const finite = running.filter((a) => {
69
+ // An infinite animation has no end to wait for. Waiting on its `finished` promise
70
+ // hangs until the timeout, which on a page with a single looping spinner would make
71
+ // every capture pay the full deadline for nothing.
72
+ const timing = a.effect?.getComputedTiming?.();
73
+ const iterations = timing?.iterations;
74
+ return !(iterations === Infinity || iterations === null);
75
+ });
76
+ loopingAnimations = running.length - finite.length;
77
+ const allDone = await withDeadline(Promise.all(finite.map((a) => a.finished.catch(() => undefined))).then(() => true), false);
78
+ pendingAnimations = allDone
79
+ ? 0
80
+ : document.getAnimations().filter((a) => {
81
+ if (a.playState !== "running")
82
+ return false;
83
+ const it = a.effect?.getComputedTiming?.()?.iterations;
84
+ return !(it === Infinity || it === null);
85
+ }).length;
86
+ }
87
+ return { settled: fontsReady && imagesReady && pendingAnimations === 0, pendingAnimations, loopingAnimations, fontsReady, imagesReady };
88
+ };
89
+ /* c8 ignore stop */
90
+ /**
91
+ * Settles the page, then pauses briefly.
92
+ *
93
+ * The trailing pause is not superstition: a transition can finish one frame before the
94
+ * compositor has painted its final state, and `requestAnimationFrame` ordering means the
95
+ * screenshot can otherwise land on the frame before the last one.
96
+ */
97
+ async function settlePage(page, opts = {}) {
98
+ const started = Date.now();
99
+ const report = await page.evaluate(settleInPage, {
100
+ timeoutMs: opts.timeoutMs ?? exports.DEFAULT_SETTLE_TIMEOUT_MS,
101
+ fonts: opts.fonts !== false,
102
+ images: opts.images !== false,
103
+ animations: opts.animations !== false,
104
+ });
105
+ const quiet = opts.quietMs ?? exports.DEFAULT_QUIET_MS;
106
+ if (quiet > 0)
107
+ await page.waitForTimeout(quiet);
108
+ return { ...report, waitedMs: Date.now() - started };
109
+ }
110
+ /** Sidecar path a SettleReport is written to, next to the capture artifact it describes. */
111
+ function settleSidecarPath(outPathAbs) {
112
+ return `${outPathAbs}.settle.json`;
113
+ }
114
+ /**
115
+ * Persists a SettleReport next to the artifact it describes. `settlePage`'s return value
116
+ * is otherwise only a field in a tool call's result, which nothing downstream keeps: a
117
+ * capture that timed out with animations still pending is a frame caught mid-transition,
118
+ * and this is what lets validate_scenes catch that statically (it reads scene sources and
119
+ * beats.json, it never launches a browser) instead of it going unnoticed until someone
120
+ * watches the render.
121
+ */
122
+ function writeSettleSidecar(outPathAbs, report) {
123
+ fs.writeFileSync(settleSidecarPath(outPathAbs), JSON.stringify(report, null, 2) + "\n", "utf8");
124
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Records a real command run as timed text rather than as pixels.
3
+ *
4
+ * Two backends, same cast format:
5
+ *
6
+ * **pty** allocates a real pseudo-terminal through the optional `node-pty` module. The
7
+ * program under capture sees a terminal, so it keeps its colour, its progress bars, and
8
+ * its cursor addressing, which is what makes a recording look like the thing the user
9
+ * actually ran. A pty merges stdout and stderr onto one stream by construction, so every
10
+ * event in a pty cast is tagged "o": that is the pty's behaviour, not a loss of fidelity
11
+ * this module introduced.
12
+ *
13
+ * **pipe** is the fallback, and the only mode available when `node-pty` is not installed.
14
+ * It keeps stdout and stderr separate and needs no native build, at the cost that many
15
+ * programs detect no terminal is attached and suppress colour entirely, and a full-screen
16
+ * TUI replays only as far as the replay component understands its control sequences.
17
+ *
18
+ * `node-pty` is deliberately an optional peer dependency rather than a dependency: it
19
+ * compiles a native addon, and requiring one from everybody to record `npm test` would be
20
+ * a bad trade. The mode actually used is always reported back, so a dull, colourless
21
+ * recording has a visible cause rather than being a mystery.
22
+ */
23
+ export type TerminalMode = "pty" | "pipe";
24
+ export interface TerminalStep {
25
+ type: "type" | "wait";
26
+ text?: string;
27
+ ms?: number;
28
+ }
29
+ export type TerminalEvent = [number, "o" | "e", string];
30
+ export interface TerminalCast {
31
+ version: 1;
32
+ command: string;
33
+ cols: number;
34
+ rows: number;
35
+ durationMs: number;
36
+ exitCode: number | null;
37
+ /** Which backend produced this cast. Absent in casts recorded before pty support existed. */
38
+ mode?: TerminalMode;
39
+ events: TerminalEvent[];
40
+ }
41
+ export interface RecordTerminalOptions {
42
+ command: string;
43
+ args?: string[];
44
+ cwd: string;
45
+ script?: TerminalStep[];
46
+ cols?: number;
47
+ rows?: number;
48
+ timeoutMs?: number;
49
+ /** "auto" uses a pty when node-pty is installed. "pipe" never does. "pty" fails if it is not. */
50
+ mode?: "auto" | "pty" | "pipe";
51
+ /**
52
+ * Stop recording after this many characters of output, and say so in the cast.
53
+ *
54
+ * The memory caps below are a safety net against a runaway process; this is a different
55
+ * thing, and a real one. A command whose output is genuinely enormous produces a cast
56
+ * nothing can replay: fetching one real installer for a demo returned 169,000 characters,
57
+ * which at any watchable typing speed is over an hour of screen time for a seven second
58
+ * beat. The first screenful is what a demo shows anyway, so bound it deliberately and
59
+ * mark the cast truncated rather than recording an unusable one or trimming it by hand
60
+ * afterwards, which would make the video stop matching the recording.
61
+ */
62
+ maxOutputChars?: number;
63
+ }
64
+ export interface RecordTerminalResult {
65
+ cast: TerminalCast;
66
+ truncated: boolean;
67
+ mode: TerminalMode;
68
+ /** Plain-language notices about the recording environment, for the caller to surface. */
69
+ warnings: string[];
70
+ }
71
+ /** A runaway process can emit output forever; cap what is retained rather than exhausting memory. */
72
+ export declare const MAX_EVENTS = 20000;
73
+ export declare const MAX_TOTAL_BYTES: number;
74
+ interface PtyProcess {
75
+ onData(cb: (data: string) => void): void;
76
+ onExit(cb: (e: {
77
+ exitCode: number;
78
+ signal?: number;
79
+ }) => void): void;
80
+ write(data: string): void;
81
+ kill(signal?: string): void;
82
+ }
83
+ interface PtyModule {
84
+ spawn(file: string, args: string[], opts: {
85
+ name: string;
86
+ cols: number;
87
+ rows: number;
88
+ cwd: string;
89
+ env: NodeJS.ProcessEnv;
90
+ }): PtyProcess;
91
+ }
92
+ /**
93
+ * Loads node-pty if it is present, and returns null rather than throwing if it is not.
94
+ *
95
+ * A native module can also be present but unloadable, typically after a Node version
96
+ * change rebuilt nothing, which throws a different error than "cannot find module". Both
97
+ * cases mean the same thing here: no pty is available, fall back.
98
+ */
99
+ export declare function loadPty(): PtyModule | null;
100
+ /**
101
+ * Python defaults that make a recorded run honest. Piped stdout is block-buffered, so without
102
+ * PYTHONUNBUFFERED a script's whole output arrives as one event at exit and the cast replays as
103
+ * a single dump; and on Windows the console code page is not UTF-8, so non-ASCII output is
104
+ * mangled or raises UnicodeEncodeError. These are the environment forms of `python -u` and
105
+ * `python -X utf8`, which means they apply however the program is launched. They sit below
106
+ * process.env so a caller who sets them deliberately still wins.
107
+ */
108
+ export declare const PYTHON_RECORDING_ENV: Readonly<Record<string, string>>;
109
+ export declare function childEnv(cols: number, rows: number): NodeJS.ProcessEnv;
110
+ /**
111
+ * On Windows a bare `bash` on PATH is usually the WSL launcher (System32/bash.exe).
112
+ * It starts a Linux distro with its own filesystem and PATH, so `bash -c "npm test"` runs
113
+ * something other than what the author's Windows shell would. Returns a warning to show, or null.
114
+ */
115
+ export declare function windowsShellWarning(command: string, platform?: NodeJS.Platform): string | null;
116
+ export declare function recordTerminal(opts: RecordTerminalOptions): Promise<RecordTerminalResult>;
117
+ export {};
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PYTHON_RECORDING_ENV = exports.MAX_TOTAL_BYTES = exports.MAX_EVENTS = void 0;
4
+ exports.loadPty = loadPty;
5
+ exports.childEnv = childEnv;
6
+ exports.windowsShellWarning = windowsShellWarning;
7
+ exports.recordTerminal = recordTerminal;
8
+ const node_child_process_1 = require("node:child_process");
9
+ /** A runaway process can emit output forever; cap what is retained rather than exhausting memory. */
10
+ exports.MAX_EVENTS = 20000;
11
+ exports.MAX_TOTAL_BYTES = 4 * 1024 * 1024;
12
+ const DEFAULT_COLS = 100;
13
+ const DEFAULT_ROWS = 30;
14
+ const DEFAULT_TIMEOUT_MS = 120000;
15
+ /**
16
+ * Loads node-pty if it is present, and returns null rather than throwing if it is not.
17
+ *
18
+ * A native module can also be present but unloadable, typically after a Node version
19
+ * change rebuilt nothing, which throws a different error than "cannot find module". Both
20
+ * cases mean the same thing here: no pty is available, fall back.
21
+ */
22
+ function loadPty() {
23
+ try {
24
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
25
+ return require("node-pty");
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ /** Collects timestamped output chunks, enforcing the retention caps once for both backends. */
32
+ class CastRecorder {
33
+ constructor(maxChars) {
34
+ this.maxChars = maxChars;
35
+ this.events = [];
36
+ this.truncated = false;
37
+ this.totalBytes = 0;
38
+ this.totalChars = 0;
39
+ this.started = Date.now();
40
+ }
41
+ record(stream, text) {
42
+ if (this.truncated)
43
+ return;
44
+ this.totalBytes += Buffer.byteLength(text);
45
+ // A deliberate character bound cuts mid-chunk, because a chunk can itself be the whole
46
+ // file. The memory caps below drop the chunk instead, since by then the goal is only to
47
+ // stop growing.
48
+ if (this.maxChars !== undefined && this.totalChars + text.length > this.maxChars) {
49
+ const room = Math.max(0, this.maxChars - this.totalChars);
50
+ if (room > 0)
51
+ this.events.push([this.elapsed(), stream, text.slice(0, room)]);
52
+ this.totalChars = this.maxChars;
53
+ this.truncated = true;
54
+ this.events.push([this.elapsed(), "e", "\n[openvidstudio: output truncated]\n"]);
55
+ return;
56
+ }
57
+ if (this.events.length >= exports.MAX_EVENTS || this.totalBytes > exports.MAX_TOTAL_BYTES) {
58
+ this.truncated = true;
59
+ this.events.push([this.elapsed(), "e", "\n[openvidstudio: output truncated]\n"]);
60
+ return;
61
+ }
62
+ this.totalChars += text.length;
63
+ this.events.push([this.elapsed(), stream, text]);
64
+ }
65
+ elapsed() {
66
+ return Date.now() - this.started;
67
+ }
68
+ }
69
+ /**
70
+ * Python defaults that make a recorded run honest. Piped stdout is block-buffered, so without
71
+ * PYTHONUNBUFFERED a script's whole output arrives as one event at exit and the cast replays as
72
+ * a single dump; and on Windows the console code page is not UTF-8, so non-ASCII output is
73
+ * mangled or raises UnicodeEncodeError. These are the environment forms of `python -u` and
74
+ * `python -X utf8`, which means they apply however the program is launched. They sit below
75
+ * process.env so a caller who sets them deliberately still wins.
76
+ */
77
+ exports.PYTHON_RECORDING_ENV = {
78
+ PYTHONUNBUFFERED: "1",
79
+ PYTHONUTF8: "1",
80
+ PYTHONIOENCODING: "utf-8",
81
+ };
82
+ function childEnv(cols, rows) {
83
+ return {
84
+ ...exports.PYTHON_RECORDING_ENV,
85
+ ...process.env,
86
+ COLUMNS: String(cols),
87
+ LINES: String(rows),
88
+ TERM: "xterm-256color",
89
+ };
90
+ }
91
+ /**
92
+ * On Windows a bare `bash` on PATH is usually the WSL launcher (System32/bash.exe).
93
+ * It starts a Linux distro with its own filesystem and PATH, so `bash -c "npm test"` runs
94
+ * something other than what the author's Windows shell would. Returns a warning to show, or null.
95
+ */
96
+ function windowsShellWarning(command, platform = process.platform) {
97
+ if (platform !== "win32")
98
+ return null;
99
+ const base = command.split(/[\\/]/).pop().toLowerCase().replace(/\.exe$/, "");
100
+ if (base !== "bash")
101
+ return null;
102
+ // A path the caller chose (Git Bash under Program Files, say) is taken at its word. Only a
103
+ // bare name, which Windows resolves through PATH, or the System32 launcher itself is the trap.
104
+ const hasPath = /[\\/]/.test(command);
105
+ if (hasPath && !/[\\/]system32[\\/]/i.test(command))
106
+ return null;
107
+ return (`"${command}" on Windows usually resolves to the WSL launcher (System32/bash.exe), which runs inside a ` +
108
+ `Linux distro with a different filesystem and PATH. Use Git Bash by full path, or run the program ` +
109
+ `directly (for example command "node" with args) so the recording shows what a Windows user would see.`);
110
+ }
111
+ /**
112
+ * Replays scripted input against the child's stdin. Typing is sent as one write per step
113
+ * rather than per character: the replay component controls on-screen typing cadence, so
114
+ * simulating keystroke timing at the process level would double it.
115
+ */
116
+ async function replayScript(steps, write, done, isSettled) {
117
+ for (const step of steps ?? []) {
118
+ if (isSettled())
119
+ return;
120
+ if (step.type === "wait") {
121
+ await new Promise((r) => setTimeout(r, step.ms ?? 0));
122
+ }
123
+ else if (step.type === "type") {
124
+ write(step.text ?? "");
125
+ }
126
+ }
127
+ done();
128
+ }
129
+ async function recordWithPty(pty, opts, cols, rows, timeoutMs) {
130
+ const recorder = new CastRecorder(opts.maxOutputChars);
131
+ const child = pty.spawn(opts.command, opts.args ?? [], {
132
+ name: "xterm-256color",
133
+ cols,
134
+ rows,
135
+ cwd: opts.cwd,
136
+ env: childEnv(cols, rows),
137
+ });
138
+ const exitCode = await new Promise((resolve) => {
139
+ let settled = false;
140
+ const finish = (code) => {
141
+ if (settled)
142
+ return;
143
+ settled = true;
144
+ clearTimeout(timer);
145
+ resolve(code);
146
+ };
147
+ const timer = setTimeout(() => {
148
+ recorder.record("e", `\n[openvidstudio: timed out after ${timeoutMs / 1000}s]\n`);
149
+ child.kill();
150
+ }, timeoutMs);
151
+ // A pty is one stream by construction: the program's stderr is written to the same
152
+ // terminal its stdout is, which is exactly why colour survives.
153
+ child.onData((data) => recorder.record("o", data));
154
+ child.onExit(({ exitCode: code }) => finish(code));
155
+ void replayScript(opts.script, (text) => child.write(text), () => {
156
+ /* A pty has no stdin to close: EOF is sent by the script itself, as Ctrl-D. */
157
+ }, () => settled);
158
+ });
159
+ return { recorder, exitCode };
160
+ }
161
+ async function recordWithPipes(opts, cols, rows, timeoutMs) {
162
+ const recorder = new CastRecorder(opts.maxOutputChars);
163
+ // shell:false with an argv array, same discipline as every other spawn in this package:
164
+ // `command` and `args` come from a beats.json a human approved, but they are still not
165
+ // concatenated into a command line.
166
+ const child = (0, node_child_process_1.spawn)(opts.command, opts.args ?? [], {
167
+ cwd: opts.cwd,
168
+ shell: false,
169
+ env: childEnv(cols, rows),
170
+ });
171
+ const exitCode = await new Promise((resolve, reject) => {
172
+ let settled = false;
173
+ const finish = (code) => {
174
+ if (settled)
175
+ return;
176
+ settled = true;
177
+ clearTimeout(timer);
178
+ resolve(code);
179
+ };
180
+ const timer = setTimeout(() => {
181
+ recorder.record("e", `\n[openvidstudio: timed out after ${timeoutMs / 1000}s]\n`);
182
+ child.kill("SIGTERM");
183
+ setTimeout(() => child.kill("SIGKILL"), 3000).unref?.();
184
+ }, timeoutMs);
185
+ child.stdout?.on("data", (c) => recorder.record("o", c.toString("utf8")));
186
+ child.stderr?.on("data", (c) => recorder.record("e", c.toString("utf8")));
187
+ child.on("error", (err) => {
188
+ if (settled)
189
+ return;
190
+ settled = true;
191
+ clearTimeout(timer);
192
+ const msg = /ENOENT/.test(String(err))
193
+ ? `Command not found: "${opts.command}". Check it is installed and on PATH for this process.`
194
+ : String(err);
195
+ reject(new Error(msg));
196
+ });
197
+ child.on("close", (code) => finish(code));
198
+ void replayScript(opts.script, (text) => child.stdin?.write(text), () => child.stdin?.end(), () => settled);
199
+ });
200
+ return { recorder, exitCode };
201
+ }
202
+ async function recordTerminal(opts) {
203
+ const cols = opts.cols ?? DEFAULT_COLS;
204
+ const rows = opts.rows ?? DEFAULT_ROWS;
205
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
206
+ const requested = opts.mode ?? "auto";
207
+ const pty = requested === "pipe" ? null : loadPty();
208
+ if (requested === "pty" && !pty) {
209
+ throw new Error(`mode "pty" was requested but node-pty is not loadable. Install it in the project that runs this ` +
210
+ `capture ("npm install node-pty"), which compiles a native addon, or use mode "auto" to fall back to ` +
211
+ `piped stdout/stderr.`);
212
+ }
213
+ const mode = pty ? "pty" : "pipe";
214
+ const { recorder, exitCode } = pty
215
+ ? await recordWithPty(pty, opts, cols, rows, timeoutMs)
216
+ : await recordWithPipes(opts, cols, rows, timeoutMs);
217
+ return {
218
+ cast: {
219
+ version: 1,
220
+ command: [opts.command, ...(opts.args ?? [])].join(" "),
221
+ cols,
222
+ rows,
223
+ durationMs: recorder.elapsed(),
224
+ exitCode,
225
+ mode,
226
+ events: recorder.events,
227
+ },
228
+ truncated: recorder.truncated,
229
+ mode,
230
+ warnings: [windowsShellWarning(opts.command)].filter((w) => w !== null),
231
+ };
232
+ }
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@openvidstudio/capture",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.4.0",
4
+ "description": "Zoom-compensated browser capture plus desktop, mobile, and terminal recording backends. No Remotion, no React, no video pipeline.",
5
+ "license": "Apache-2.0",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "type": "commonjs",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "dependencies": {
16
+ "playwright": "^1.64.0",
17
+ "zod": "^4.6.6"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^26.6.5",
21
+ "typescript": "7.0.2"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/AnayDhawan/openvidstudio.git",
29
+ "directory": "packages/capture"
30
+ },
31
+ "homepage": "https://github.com/AnayDhawan/openvidstudio#readme",
32
+ "bugs": {
33
+ "url": "https://github.com/AnayDhawan/openvidstudio/issues"
34
+ },
35
+ "keywords": [
36
+ "playwright",
37
+ "screenshot",
38
+ "screen-recording",
39
+ "capture",
40
+ "ffmpeg",
41
+ "adb"
42
+ ],
43
+ "scripts": {
44
+ "build": "tsc",
45
+ "typecheck": "tsc --noEmit"
46
+ }
6
47
  }