@fullstackcraftllc/codevideo-cli 0.0.7
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/LICENSE +21 -0
- package/README.md +11 -0
- package/bin/codevideo-cli.js +33 -0
- package/doctor.js +35 -0
- package/index.d.ts +19 -0
- package/index.js +86 -0
- package/install-browser.js +34 -0
- package/package.json +52 -0
- package/runtime/package.json +3 -0
- package/runtime/recordVideoV3.js +245 -0
- package/runtime/runtimePaths.js +126 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 CodeVideo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# CodeVideo CLI
|
|
2
|
+
|
|
3
|
+
Cross-platform npm distribution of the CodeVideo video rendering CLI.
|
|
4
|
+
|
|
5
|
+
```shell
|
|
6
|
+
npx @fullstackcraftllc/codevideo-cli doctor
|
|
7
|
+
npx @fullstackcraftllc/codevideo-cli install-browser
|
|
8
|
+
npx @fullstackcraftllc/codevideo-cli --version
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Rendering requires FFmpeg on `PATH` (or `CODEVIDEO_FFMPEG_PATH`). Chrome is detected automatically; if none is installed, run `codevideo-cli install-browser`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { runDoctor } from "../doctor.js";
|
|
5
|
+
import { installBrowser } from "../install-browser.js";
|
|
6
|
+
import { resolveCodeVideoCliRuntime } from "../index.js";
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
|
|
10
|
+
if (args[0] === "doctor") {
|
|
11
|
+
process.exitCode = await runDoctor();
|
|
12
|
+
} else if (args[0] === "install-browser") {
|
|
13
|
+
await installBrowser();
|
|
14
|
+
} else {
|
|
15
|
+
const runtime = resolveCodeVideoCliRuntime();
|
|
16
|
+
const child = spawn(runtime.binaryPath, args, {
|
|
17
|
+
cwd: process.cwd(),
|
|
18
|
+
env: runtime.env,
|
|
19
|
+
stdio: "inherit"
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
23
|
+
process.on(signal, () => child.kill(signal));
|
|
24
|
+
}
|
|
25
|
+
child.once("error", (error) => {
|
|
26
|
+
console.error(`Failed to start CodeVideo CLI: ${error.message}`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
});
|
|
29
|
+
child.once("exit", (code, signal) => {
|
|
30
|
+
if (signal) process.kill(process.pid, signal);
|
|
31
|
+
else process.exitCode = code ?? 1;
|
|
32
|
+
});
|
|
33
|
+
}
|
package/doctor.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { resolveCodeVideoCliRuntime } from "./index.js";
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const runtimePaths = require("./runtime/runtimePaths.js");
|
|
8
|
+
|
|
9
|
+
function checkWritableDirectory(directory) {
|
|
10
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
11
|
+
fs.accessSync(directory, fs.constants.W_OK);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function runDoctor(options = {}) {
|
|
15
|
+
const checks = [];
|
|
16
|
+
try {
|
|
17
|
+
const runtime = resolveCodeVideoCliRuntime(options);
|
|
18
|
+
const version = spawnSync(runtime.binaryPath, ["--version"], { env: runtime.env, encoding: "utf8" });
|
|
19
|
+
if (version.status !== 0) throw new Error(version.stderr || "native binary version check failed");
|
|
20
|
+
checks.push(["native binary", runtime.binaryPath]);
|
|
21
|
+
checks.push(["Puppeteer runner", runtime.runnerPath]);
|
|
22
|
+
checks.push(["Chrome", runtimePaths.resolveChromeExecutable()]);
|
|
23
|
+
checks.push(["FFmpeg", runtimePaths.resolveFfmpegExecutable()]);
|
|
24
|
+
for (const directory of [runtime.workDir, runtime.logDir, runtime.outputDir]) {
|
|
25
|
+
checkWritableDirectory(directory);
|
|
26
|
+
}
|
|
27
|
+
checks.push(["writable directories", `${runtime.workDir}, ${runtime.logDir}, ${runtime.outputDir}`]);
|
|
28
|
+
for (const [name, value] of checks) console.log(`✓ ${name}: ${value}`);
|
|
29
|
+
return 0;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
for (const [name, value] of checks) console.log(`✓ ${name}: ${value}`);
|
|
32
|
+
console.error(`✗ ${error.message}`);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface CodeVideoCliRuntimeOptions {
|
|
2
|
+
platform?: NodeJS.Platform;
|
|
3
|
+
arch?: string;
|
|
4
|
+
env?: NodeJS.ProcessEnv;
|
|
5
|
+
binaryPath?: string;
|
|
6
|
+
runnerPath?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CodeVideoCliRuntime {
|
|
10
|
+
binaryPath: string;
|
|
11
|
+
runnerPath: string;
|
|
12
|
+
workDir: string;
|
|
13
|
+
logDir: string;
|
|
14
|
+
outputDir: string;
|
|
15
|
+
env: NodeJS.ProcessEnv;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export declare function getCodeVideoCliPlatformPackage(platform?: NodeJS.Platform, arch?: string): string;
|
|
19
|
+
export declare function resolveCodeVideoCliRuntime(options?: CodeVideoCliRuntimeOptions): CodeVideoCliRuntime;
|
package/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const packageRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
const platformPackages = {
|
|
11
|
+
"darwin-arm64": "@fullstackcraftllc/codevideo-cli-darwin-arm64",
|
|
12
|
+
"darwin-x64": "@fullstackcraftllc/codevideo-cli-darwin-x64",
|
|
13
|
+
"linux-arm64": "@fullstackcraftllc/codevideo-cli-linux-arm64",
|
|
14
|
+
"linux-x64": "@fullstackcraftllc/codevideo-cli-linux-x64",
|
|
15
|
+
"win32-arm64": "@fullstackcraftllc/codevideo-cli-win32-arm64",
|
|
16
|
+
"win32-x64": "@fullstackcraftllc/codevideo-cli-win32-x64"
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function getCodeVideoCliPlatformPackage(platform = process.platform, arch = process.arch) {
|
|
20
|
+
const packageName = platformPackages[`${platform}-${arch}`];
|
|
21
|
+
if (!packageName) {
|
|
22
|
+
throw new Error(`Unsupported CodeVideo CLI platform: ${platform}-${arch}`);
|
|
23
|
+
}
|
|
24
|
+
return packageName;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validateExecutable(candidate, label, platform) {
|
|
28
|
+
const stat = fs.statSync(candidate, { throwIfNoEntry: false });
|
|
29
|
+
if (!stat?.isFile()) throw new Error(`${label} is not a file: ${candidate}`);
|
|
30
|
+
if (platform !== "win32") {
|
|
31
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveCodeVideoCliRuntime(options = {}) {
|
|
36
|
+
const platform = options.platform || process.platform;
|
|
37
|
+
const arch = options.arch || process.arch;
|
|
38
|
+
const env = options.env || process.env;
|
|
39
|
+
let binaryPath = options.binaryPath || env.PATH_TO_CODEVIDEO_CLI;
|
|
40
|
+
|
|
41
|
+
if (binaryPath) {
|
|
42
|
+
binaryPath = path.resolve(binaryPath);
|
|
43
|
+
} else {
|
|
44
|
+
const platformPackage = getCodeVideoCliPlatformPackage(platform, arch);
|
|
45
|
+
let platformRoot;
|
|
46
|
+
try {
|
|
47
|
+
platformRoot = path.dirname(require.resolve(`${platformPackage}/package.json`));
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`The optional package ${platformPackage} is missing. ` +
|
|
51
|
+
"Reinstall @fullstackcraftllc/codevideo-cli without --omit=optional.",
|
|
52
|
+
{ cause: error }
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
binaryPath = path.join(platformRoot, "bin", platform === "win32" ? "codevideo-cli.exe" : "codevideo-cli");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
validateExecutable(binaryPath, "CodeVideo CLI", platform);
|
|
59
|
+
|
|
60
|
+
const runnerPath = path.resolve(
|
|
61
|
+
options.runnerPath || env.CODEVIDEO_PUPPETEER_RUNNER_PATH || path.join(packageRoot, "runtime", "recordVideoV3.js")
|
|
62
|
+
);
|
|
63
|
+
if (!fs.statSync(runnerPath, { throwIfNoEntry: false })?.isFile()) {
|
|
64
|
+
throw new Error(`CodeVideo Puppeteer runner is missing: ${runnerPath}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cacheRoot = path.join(os.homedir(), ".cache", "codevideo");
|
|
68
|
+
const workDir = path.resolve(env.CODEVIDEO_WORK_DIR || path.join(os.tmpdir(), "codevideo", "v3"));
|
|
69
|
+
const logDir = path.resolve(env.CODEVIDEO_LOG_DIR || path.join(cacheRoot, "logs"));
|
|
70
|
+
const outputDir = path.resolve(env.CODEVIDEO_OUTPUT_DIR || process.cwd());
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
binaryPath,
|
|
74
|
+
runnerPath,
|
|
75
|
+
workDir,
|
|
76
|
+
logDir,
|
|
77
|
+
outputDir,
|
|
78
|
+
env: {
|
|
79
|
+
...env,
|
|
80
|
+
CODEVIDEO_PUPPETEER_RUNNER_PATH: runnerPath,
|
|
81
|
+
CODEVIDEO_WORK_DIR: workDir,
|
|
82
|
+
CODEVIDEO_LOG_DIR: logDir,
|
|
83
|
+
CODEVIDEO_OUTPUT_DIR: outputDir
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import {
|
|
4
|
+
Browser,
|
|
5
|
+
BrowserTag,
|
|
6
|
+
detectBrowserPlatform,
|
|
7
|
+
install,
|
|
8
|
+
resolveBuildId
|
|
9
|
+
} from "@puppeteer/browsers";
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const { browserCacheDir } = require("./runtime/runtimePaths.js");
|
|
13
|
+
|
|
14
|
+
export async function installBrowser(options = {}) {
|
|
15
|
+
const browsers = options.browsers || {
|
|
16
|
+
detectBrowserPlatform,
|
|
17
|
+
install,
|
|
18
|
+
resolveBuildId
|
|
19
|
+
};
|
|
20
|
+
const platform = browsers.detectBrowserPlatform();
|
|
21
|
+
if (!platform) throw new Error(`Unsupported browser platform: ${process.platform}/${process.arch}`);
|
|
22
|
+
const cacheDir = path.resolve(browserCacheDir(options.env || process.env));
|
|
23
|
+
const buildId = await browsers.resolveBuildId(Browser.CHROME, platform, BrowserTag.STABLE);
|
|
24
|
+
const installed = await browsers.install({
|
|
25
|
+
browser: Browser.CHROME,
|
|
26
|
+
buildId,
|
|
27
|
+
buildIdAlias: BrowserTag.STABLE,
|
|
28
|
+
cacheDir,
|
|
29
|
+
platform,
|
|
30
|
+
downloadProgressCallback: "default"
|
|
31
|
+
});
|
|
32
|
+
console.log(`Chrome for Testing installed at ${installed.executablePath}`);
|
|
33
|
+
return installed.executablePath;
|
|
34
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fullstackcraftllc/codevideo-cli",
|
|
3
|
+
"version": "0.0.7",
|
|
4
|
+
"description": "Cross-platform npm distribution of the CodeVideo CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"import": "./index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"codevideo-cli": "bin/codevideo-cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin/",
|
|
19
|
+
"runtime/",
|
|
20
|
+
"index.js",
|
|
21
|
+
"index.d.ts",
|
|
22
|
+
"doctor.js",
|
|
23
|
+
"install-browser.js",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@puppeteer/browsers": "^2.10.9",
|
|
32
|
+
"puppeteer-stream": "^3.0.22",
|
|
33
|
+
"yargs": "^17.7.2"
|
|
34
|
+
},
|
|
35
|
+
"optionalDependencies": {
|
|
36
|
+
"@fullstackcraftllc/codevideo-cli-darwin-arm64": "0.0.7",
|
|
37
|
+
"@fullstackcraftllc/codevideo-cli-darwin-x64": "0.0.7",
|
|
38
|
+
"@fullstackcraftllc/codevideo-cli-linux-arm64": "0.0.7",
|
|
39
|
+
"@fullstackcraftllc/codevideo-cli-linux-x64": "0.0.7",
|
|
40
|
+
"@fullstackcraftllc/codevideo-cli-win32-arm64": "0.0.7",
|
|
41
|
+
"@fullstackcraftllc/codevideo-cli-win32-x64": "0.0.7"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/codevideo/codevideo-cli.git",
|
|
46
|
+
"directory": "npm/packages/codevideo-cli"
|
|
47
|
+
},
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
const { launch, getStream, wss } = require("puppeteer-stream");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const yargs = require('yargs/yargs');
|
|
5
|
+
const { hideBin } = require('yargs/helpers');
|
|
6
|
+
const { resolveChromeExecutable } = require('./runtimePaths');
|
|
7
|
+
|
|
8
|
+
// define sleep helper function
|
|
9
|
+
const sleep = ms => new Promise(res => setTimeout(res, ms));
|
|
10
|
+
|
|
11
|
+
// Parse command line arguments
|
|
12
|
+
const argv = yargs(hideBin(process.argv))
|
|
13
|
+
.option('uuid', {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'Path to the manifest file'
|
|
16
|
+
})
|
|
17
|
+
.option('os', {
|
|
18
|
+
type: 'string',
|
|
19
|
+
default: 'linux',
|
|
20
|
+
description: 'Operating system (linux or mac)'
|
|
21
|
+
})
|
|
22
|
+
.option('resolution', {
|
|
23
|
+
type: 'string',
|
|
24
|
+
default: '1080p',
|
|
25
|
+
description: 'Video resolution (1080p or 4K)'
|
|
26
|
+
})
|
|
27
|
+
.option('orientation', {
|
|
28
|
+
type: 'string',
|
|
29
|
+
default: 'landscape',
|
|
30
|
+
description: 'Video orientation (landscape or portrait)'
|
|
31
|
+
})
|
|
32
|
+
.option('debug', {
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
default: false,
|
|
35
|
+
description: 'Run in non-headless mode for debugging'
|
|
36
|
+
})
|
|
37
|
+
.option('output-webm', {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: 'Absolute output path for the recorded WebM file'
|
|
40
|
+
})
|
|
41
|
+
.option('manifest-path', {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Absolute path to the render manifest'
|
|
44
|
+
})
|
|
45
|
+
.argv;
|
|
46
|
+
|
|
47
|
+
// parse uuid, resolution and orientation from command line arguments
|
|
48
|
+
const uuid = argv.uuid;
|
|
49
|
+
const os = argv.os;
|
|
50
|
+
const resolution = argv.resolution;
|
|
51
|
+
const orientation = argv.orientation;
|
|
52
|
+
const debug = argv.debug;
|
|
53
|
+
|
|
54
|
+
// set width and height based on resolution and orientation
|
|
55
|
+
let width, height;
|
|
56
|
+
if (resolution === '4K') {
|
|
57
|
+
width = orientation === 'landscape' ? 3840 : 2160;
|
|
58
|
+
height = orientation === 'landscape' ? 2160 : 3840;
|
|
59
|
+
} else if (resolution === '1080p') {
|
|
60
|
+
width = orientation === 'landscape' ? 1920 : 1080;
|
|
61
|
+
height = orientation === 'landscape' ? 1080 : 1920;
|
|
62
|
+
}
|
|
63
|
+
// if no manifest is provided, exit
|
|
64
|
+
if (!uuid) {
|
|
65
|
+
console.error("Please provide a manifest file path.");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function recordVideoV3() {
|
|
70
|
+
console.log("Starting recording for UUID: ", uuid);
|
|
71
|
+
console.log("Debug mode:", debug ? "ENABLED - Browser will be visible" : "disabled");
|
|
72
|
+
|
|
73
|
+
const outputWebm = argv.outputWebm
|
|
74
|
+
? path.resolve(argv.outputWebm)
|
|
75
|
+
: path.join(__dirname, `../../tmp/v3/video/${uuid}.webm`);
|
|
76
|
+
fs.mkdirSync(path.dirname(outputWebm), { recursive: true });
|
|
77
|
+
const file = fs.createWriteStream(outputWebm);
|
|
78
|
+
|
|
79
|
+
console.log("Launching browser with resolution:", width, "x", height, orientation, width === 3840 ? " (4K)" : " (1080p)");
|
|
80
|
+
|
|
81
|
+
const browser = await launch({
|
|
82
|
+
dumpio: true,
|
|
83
|
+
startDelay: 1000,
|
|
84
|
+
executablePath: resolveChromeExecutable(),
|
|
85
|
+
headless: debug ? false : "new", // Use non-headless mode when debugging
|
|
86
|
+
defaultViewport: { width, height },
|
|
87
|
+
args: [
|
|
88
|
+
`--window-size=${width},${height}`,
|
|
89
|
+
'--start-fullscreen',
|
|
90
|
+
// `--ozone-override-screen-size=${width},${height}`, // for linux
|
|
91
|
+
'--no-sandbox', // Chrome's sandbox can't init as root (server/systemd) -> instant "Target closed" without this
|
|
92
|
+
'--disable-dev-shm-usage', // avoid crashes from a small /dev/shm on servers
|
|
93
|
+
'--autoplay-policy=no-user-gesture-required',
|
|
94
|
+
'--enable-extensions',
|
|
95
|
+
// '--disable-web-security',
|
|
96
|
+
// '--enable-logging=stderr', // Enable detailed logging
|
|
97
|
+
'--v=1', // Increase verbosity level
|
|
98
|
+
'--allowlisted-extension-id=jjndjgheafjngoipoacpjgeicjeomjli', // allowlist the puppeteer-stream extension
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const page = await browser.newPage();
|
|
103
|
+
|
|
104
|
+
if (argv.manifestPath) {
|
|
105
|
+
const manifestPath = path.resolve(argv.manifestPath);
|
|
106
|
+
await page.setRequestInterception(true);
|
|
107
|
+
page.on('request', request => {
|
|
108
|
+
const requestedUrl = new URL(request.url());
|
|
109
|
+
const isManifestRequest = requestedUrl.hostname === 'localhost'
|
|
110
|
+
&& requestedUrl.port === '7000'
|
|
111
|
+
&& requestedUrl.pathname === '/get-manifest-v3';
|
|
112
|
+
if (!isManifestRequest) {
|
|
113
|
+
request.continue();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
request.respond({
|
|
118
|
+
status: 200,
|
|
119
|
+
contentType: 'application/json',
|
|
120
|
+
headers: { 'Access-Control-Allow-Origin': '*' },
|
|
121
|
+
body: fs.readFileSync(manifestPath)
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(`Unable to read manifest at ${manifestPath}:`, error);
|
|
125
|
+
request.respond({ status: 500, body: 'Manifest unavailable' });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Log domcontentloaded
|
|
131
|
+
page.once('domcontentloaded', () => {
|
|
132
|
+
console.log('DOM content loaded');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Log load
|
|
136
|
+
page.once('load', () => {
|
|
137
|
+
console.log('Page fully loaded');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Log all browser console messages.
|
|
141
|
+
page.on('console', msg => {
|
|
142
|
+
console.log('BROWSER LOG:', msg.text());
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Log page errors
|
|
146
|
+
page.on('pageerror', error => {
|
|
147
|
+
console.log('PAGE ERROR:', error.message);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Log network failures
|
|
151
|
+
page.on('requestfailed', req => {
|
|
152
|
+
console.log('REQUEST FAILED:', req.url(), req.failure().errorText);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Create a promise that resolves when the final progress update is received.
|
|
156
|
+
let resolveFinalProgress;
|
|
157
|
+
const finalProgressPromise = new Promise(resolve => {
|
|
158
|
+
resolveFinalProgress = resolve;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Expose __onActionProgress so that progress stats from the client are logged in Node.
|
|
162
|
+
// When a final progress update is received, we resolve the promise.
|
|
163
|
+
await page.exposeFunction('__onActionProgress', (progress) => {
|
|
164
|
+
console.log("Progress update:", progress);
|
|
165
|
+
// Check if this progress update indicates completion.
|
|
166
|
+
if (progress.progress === "100.0" || progress.currentAction >= progress.totalActions) {
|
|
167
|
+
resolveFinalProgress();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
console.log("added __onActionProgress");
|
|
171
|
+
|
|
172
|
+
// Navigate to the puppeteer page.
|
|
173
|
+
await page.setViewport({ width: 0, height: 0 });
|
|
174
|
+
|
|
175
|
+
const url = `http://localhost:7001/v3?uuid=${uuid}`;
|
|
176
|
+
console.log(`Navigating to ${url}`);
|
|
177
|
+
await page.goto(url);
|
|
178
|
+
console.log("Page navigated");
|
|
179
|
+
|
|
180
|
+
// click body to trigger interaction
|
|
181
|
+
await page.click("body");
|
|
182
|
+
console.log("Clicked body");
|
|
183
|
+
|
|
184
|
+
// Inject CSS to remove margins/padding so the video fills the viewport
|
|
185
|
+
await page.addStyleTag({ content: `body { margin: 0; padding: 0; }` });
|
|
186
|
+
console.log("Added style tag");
|
|
187
|
+
|
|
188
|
+
const videoConstraints = {
|
|
189
|
+
mandatory: {
|
|
190
|
+
minWidth: width,
|
|
191
|
+
minHeight: height,
|
|
192
|
+
maxWidth: width,
|
|
193
|
+
maxHeight: height,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Start the stream and pipe it to a file.
|
|
198
|
+
const stream = await getStream(page, {
|
|
199
|
+
audio: true,
|
|
200
|
+
video: true,
|
|
201
|
+
mimeType: "video/webm", // WebM is well-supported for high-quality web video
|
|
202
|
+
audioBitsPerSecond: 384000, // 384 kbps for high-quality stereo audio
|
|
203
|
+
videoBitsPerSecond: width === 3840 ? 80000000: 20000000, // 80 Mpbs for 4K; 20 Mbps (20,000 kbps) for high-quality 1080p video
|
|
204
|
+
frameSize: 16, // approx 60 FPS
|
|
205
|
+
videoConstraints
|
|
206
|
+
});
|
|
207
|
+
stream.pipe(file);
|
|
208
|
+
console.log("Recording started");
|
|
209
|
+
|
|
210
|
+
// Wait a moment before triggering start.
|
|
211
|
+
await sleep(1000);
|
|
212
|
+
|
|
213
|
+
// Send signal to react component to start recording
|
|
214
|
+
console.log("Triggering recording start...");
|
|
215
|
+
await page.evaluate(() => {
|
|
216
|
+
window.__startRecording();
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// Wait until the client sends a final progress update.
|
|
220
|
+
await finalProgressPromise;
|
|
221
|
+
|
|
222
|
+
// Wait a moment before stopping the recording.
|
|
223
|
+
await sleep(1000);
|
|
224
|
+
|
|
225
|
+
// Once complete, tear down the recording.
|
|
226
|
+
console.log("Final progress received. Stopping recording...");
|
|
227
|
+
|
|
228
|
+
// If debug mode is enabled, wait longer to allow inspection
|
|
229
|
+
if (debug) {
|
|
230
|
+
console.log("Debug mode: Waiting 5 seconds before closing browser...");
|
|
231
|
+
await sleep(5000);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await stream.destroy();
|
|
235
|
+
file.close();
|
|
236
|
+
console.log("Recording finished");
|
|
237
|
+
|
|
238
|
+
await browser.close();
|
|
239
|
+
(await wss).close();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
recordVideoV3().catch(err => {
|
|
243
|
+
console.error("Error during recording:", err);
|
|
244
|
+
process.exitCode = 1;
|
|
245
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
function isExecutableFile(candidate) {
|
|
6
|
+
if (!candidate) return false;
|
|
7
|
+
try {
|
|
8
|
+
const stat = fs.statSync(candidate);
|
|
9
|
+
if (!stat.isFile()) return false;
|
|
10
|
+
if (process.platform !== "win32") {
|
|
11
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
12
|
+
}
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function findOnPath(names) {
|
|
20
|
+
const entries = (process.env.PATH || "").split(path.delimiter).filter(Boolean);
|
|
21
|
+
const extensions = process.platform === "win32"
|
|
22
|
+
? (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";")
|
|
23
|
+
: [""];
|
|
24
|
+
for (const name of names) {
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
for (const extension of extensions) {
|
|
27
|
+
const candidate = path.join(entry, process.platform === "win32" ? `${name}${extension}` : name);
|
|
28
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function chromeSubpath() {
|
|
36
|
+
const macApp = "Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing";
|
|
37
|
+
if (process.platform === "darwin") {
|
|
38
|
+
return process.arch === "arm64" ? `chrome-mac-arm64/${macApp}` : `chrome-mac-x64/${macApp}`;
|
|
39
|
+
}
|
|
40
|
+
if (process.platform === "win32") return "chrome-win64/chrome.exe";
|
|
41
|
+
return "chrome-linux64/chrome";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findCachedChrome(chromeRoot) {
|
|
45
|
+
if (!fs.existsSync(chromeRoot)) return null;
|
|
46
|
+
const builds = fs.readdirSync(chromeRoot)
|
|
47
|
+
.map((name) => path.join(chromeRoot, name))
|
|
48
|
+
.filter((candidate) => {
|
|
49
|
+
try { return fs.statSync(candidate).isDirectory(); } catch { return false; }
|
|
50
|
+
})
|
|
51
|
+
.sort()
|
|
52
|
+
.reverse();
|
|
53
|
+
for (const build of builds) {
|
|
54
|
+
const candidate = path.join(build, chromeSubpath());
|
|
55
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function browserCacheDir(env = process.env) {
|
|
61
|
+
return env.CODEVIDEO_BROWSER_CACHE_DIR || path.join(os.homedir(), ".cache", "codevideo", "chrome");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function systemChromeCandidates() {
|
|
65
|
+
if (process.platform === "darwin") {
|
|
66
|
+
return [
|
|
67
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
68
|
+
"/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
|
|
69
|
+
path.join(os.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
if (process.platform === "win32") {
|
|
73
|
+
return [
|
|
74
|
+
process.env.PROGRAMFILES && path.join(process.env.PROGRAMFILES, "Google/Chrome/Application/chrome.exe"),
|
|
75
|
+
process.env["PROGRAMFILES(X86)"] && path.join(process.env["PROGRAMFILES(X86)"], "Google/Chrome/Application/chrome.exe"),
|
|
76
|
+
process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, "Google/Chrome/Application/chrome.exe"),
|
|
77
|
+
].filter(Boolean);
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveChromeExecutable() {
|
|
83
|
+
if (process.env.CODEVIDEO_CHROME_PATH) {
|
|
84
|
+
if (!isExecutableFile(process.env.CODEVIDEO_CHROME_PATH)) {
|
|
85
|
+
throw new Error(`CODEVIDEO_CHROME_PATH is not an executable file: ${process.env.CODEVIDEO_CHROME_PATH}`);
|
|
86
|
+
}
|
|
87
|
+
return process.env.CODEVIDEO_CHROME_PATH;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const managed = findCachedChrome(path.join(browserCacheDir(), "chrome"));
|
|
91
|
+
if (managed) return managed;
|
|
92
|
+
|
|
93
|
+
const legacy = findCachedChrome(path.join(__dirname, "chrome"));
|
|
94
|
+
if (legacy) return legacy;
|
|
95
|
+
|
|
96
|
+
for (const candidate of systemChromeCandidates()) {
|
|
97
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const fromPath = findOnPath(["google-chrome-stable", "google-chrome", "chromium", "chromium-browser"]);
|
|
101
|
+
if (fromPath) return fromPath;
|
|
102
|
+
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Chrome was not found for ${process.platform}/${process.arch}. ` +
|
|
105
|
+
"Set CODEVIDEO_CHROME_PATH or run `codevideo-cli install-browser`."
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveFfmpegExecutable() {
|
|
110
|
+
if (process.env.CODEVIDEO_FFMPEG_PATH) {
|
|
111
|
+
if (!path.isAbsolute(process.env.CODEVIDEO_FFMPEG_PATH) || !isExecutableFile(process.env.CODEVIDEO_FFMPEG_PATH)) {
|
|
112
|
+
throw new Error(`CODEVIDEO_FFMPEG_PATH is not an absolute executable file: ${process.env.CODEVIDEO_FFMPEG_PATH}`);
|
|
113
|
+
}
|
|
114
|
+
return process.env.CODEVIDEO_FFMPEG_PATH;
|
|
115
|
+
}
|
|
116
|
+
const executable = findOnPath(["ffmpeg"]);
|
|
117
|
+
if (executable) return executable;
|
|
118
|
+
throw new Error("FFmpeg was not found. Install ffmpeg on PATH or set CODEVIDEO_FFMPEG_PATH.");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
browserCacheDir,
|
|
123
|
+
isExecutableFile,
|
|
124
|
+
resolveChromeExecutable,
|
|
125
|
+
resolveFfmpegExecutable,
|
|
126
|
+
};
|