Skip to content

Commit 310bd45

Browse files
authored
Merge pull request #77 from SentienceAPI/video_recording
browser video recording
2 parents 67ad58e + 8695edd commit 310bd45

5 files changed

Lines changed: 546 additions & 10 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Advanced Video Recording Demo
3+
*
4+
* Demonstrates advanced video recording features:
5+
* - Custom resolution (1080p)
6+
* - Custom output filename
7+
* - Multiple recordings in one session
8+
*/
9+
10+
import { SentienceBrowser } from '../src/browser';
11+
import * as path from 'path';
12+
import * as fs from 'fs';
13+
14+
async function recordWithCustomSettings() {
15+
console.log('\n' + '='.repeat(60));
16+
console.log('Advanced Video Recording Demo');
17+
console.log('='.repeat(60) + '\n');
18+
19+
const videoDir = path.join(process.cwd(), 'recordings');
20+
21+
// Example 1: Custom Resolution (1080p)
22+
console.log('📹 Example 1: Recording in 1080p (Full HD)\n');
23+
24+
const browser1 = new SentienceBrowser(
25+
undefined,
26+
undefined,
27+
false,
28+
undefined,
29+
undefined,
30+
undefined,
31+
videoDir,
32+
{ width: 1920, height: 1080 } // 1080p resolution
33+
);
34+
35+
await browser1.start();
36+
console.log(' Resolution: 1920x1080');
37+
38+
const page1 = browser1.getPage();
39+
await page1.goto('https://example.com');
40+
await page1.waitForTimeout(2000);
41+
42+
// Close with custom filename
43+
const video1 = await browser1.close(path.join(videoDir, 'example_1080p.webm'));
44+
console.log(` ✅ Saved: ${video1}\n`);
45+
46+
// Example 2: Custom Filename with Timestamp
47+
console.log('📹 Example 2: Recording with timestamp filename\n');
48+
49+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
50+
const customFilename = `recording_${timestamp}.webm`;
51+
52+
const browser2 = new SentienceBrowser(
53+
undefined, undefined, false, undefined, undefined, undefined,
54+
videoDir
55+
);
56+
57+
await browser2.start();
58+
59+
const page2 = browser2.getPage();
60+
await page2.goto('https://example.com');
61+
await page2.click('text=More information');
62+
await page2.waitForTimeout(2000);
63+
64+
const video2 = await browser2.close(path.join(videoDir, customFilename));
65+
console.log(` ✅ Saved: ${video2}\n`);
66+
67+
// Example 3: Organized by Project
68+
console.log('📹 Example 3: Organized directory structure\n');
69+
70+
const projectDir = path.join(videoDir, 'my_project', 'tutorials');
71+
const browser3 = new SentienceBrowser(
72+
undefined, undefined, false, undefined, undefined, undefined,
73+
projectDir
74+
);
75+
76+
await browser3.start();
77+
console.log(` Saving to: ${projectDir}`);
78+
79+
const page3 = browser3.getPage();
80+
await page3.goto('https://example.com');
81+
await page3.waitForTimeout(2000);
82+
83+
const video3 = await browser3.close(path.join(projectDir, 'tutorial_01.webm'));
84+
console.log(` ✅ Saved: ${video3}\n`);
85+
86+
console.log('='.repeat(60));
87+
console.log('All recordings completed!');
88+
console.log(`Check ${path.resolve(videoDir)} for all videos`);
89+
console.log('='.repeat(60) + '\n');
90+
}
91+
92+
// Run the demo
93+
recordWithCustomSettings().catch(error => {
94+
console.error('Error:', error);
95+
process.exit(1);
96+
});

examples/video-recording-demo.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Video Recording Demo - Record browser sessions with SentienceBrowser
3+
*
4+
* This example demonstrates how to use the video recording feature
5+
* to capture browser automation sessions.
6+
*/
7+
8+
import { SentienceBrowser } from '../src/browser';
9+
import * as path from 'path';
10+
import * as fs from 'fs';
11+
12+
async function main() {
13+
// Create output directory for videos
14+
const videoDir = path.join(process.cwd(), 'recordings');
15+
if (!fs.existsSync(videoDir)) {
16+
fs.mkdirSync(videoDir, { recursive: true });
17+
}
18+
19+
console.log('\n' + '='.repeat(60));
20+
console.log('Video Recording Demo');
21+
console.log('='.repeat(60) + '\n');
22+
23+
// Create browser with video recording enabled
24+
const browser = new SentienceBrowser(
25+
undefined, // apiKey
26+
undefined, // apiUrl
27+
false, // headless - set to false so you can see the recording
28+
undefined, // proxy
29+
undefined, // userDataDir
30+
undefined, // storageState
31+
videoDir // recordVideoDir - enables video recording
32+
);
33+
34+
await browser.start();
35+
console.log('🎥 Video recording enabled');
36+
console.log(`📁 Videos will be saved to: ${path.resolve(videoDir)}\n`);
37+
38+
try {
39+
const page = browser.getPage();
40+
41+
// Navigate to example.com
42+
console.log('Navigating to example.com...');
43+
await page.goto('https://example.com');
44+
await page.waitForLoadState('networkidle');
45+
46+
// Perform some actions
47+
console.log('Taking screenshot...');
48+
await page.screenshot({ path: 'example_screenshot.png' });
49+
50+
console.log('Scrolling page...');
51+
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
52+
await page.waitForTimeout(1000);
53+
54+
console.log('\n✅ Recording complete!');
55+
console.log('Video will be saved when browser closes...\n');
56+
} finally {
57+
// Video is automatically saved when browser closes
58+
const videoPath = await browser.close();
59+
console.log('='.repeat(60));
60+
console.log(`Video saved to: ${videoPath}`);
61+
console.log(`Check ${path.resolve(videoDir)} for the recorded video (.webm)`);
62+
console.log('='.repeat(60) + '\n');
63+
}
64+
}
65+
66+
// Run the demo
67+
main().catch(error => {
68+
console.error('Error:', error);
69+
process.exit(1);
70+
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sentienceapi",
3-
"version": "0.90.11",
3+
"version": "0.90.12",
44
"description": "TypeScript SDK for Sentience AI Agent Browser Automation",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/browser.ts

Lines changed: 114 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,18 @@ export class SentienceBrowser {
2121
private _proxy?: string;
2222
private _userDataDir?: string;
2323
private _storageState?: string | StorageState | object;
24+
private _recordVideoDir?: string;
25+
private _recordVideoSize?: { width: number; height: number };
2426

2527
constructor(
2628
apiKey?: string,
2729
apiUrl?: string,
2830
headless?: boolean,
2931
proxy?: string,
3032
userDataDir?: string,
31-
storageState?: string | StorageState | object
33+
storageState?: string | StorageState | object,
34+
recordVideoDir?: string,
35+
recordVideoSize?: { width: number; height: number }
3236
) {
3337
this._apiKey = apiKey;
3438

@@ -54,6 +58,10 @@ export class SentienceBrowser {
5458
// Auth injection support
5559
this._userDataDir = userDataDir;
5660
this._storageState = storageState;
61+
62+
// Video recording support
63+
this._recordVideoDir = recordVideoDir;
64+
this._recordVideoSize = recordVideoSize || { width: 1280, height: 800 };
5765
}
5866

5967
async start(): Promise<void> {
@@ -129,8 +137,17 @@ export class SentienceBrowser {
129137
// 4. Parse proxy configuration
130138
const proxyConfig = this.parseProxy(this._proxy);
131139

132-
// 5. Launch Browser
133-
this.context = await chromium.launchPersistentContext(this.userDataDir, {
140+
// 5. Setup video recording directory if requested
141+
if (this._recordVideoDir) {
142+
if (!fs.existsSync(this._recordVideoDir)) {
143+
fs.mkdirSync(this._recordVideoDir, { recursive: true });
144+
}
145+
console.log(`🎥 [Sentience] Recording video to: ${this._recordVideoDir}`);
146+
console.log(` Resolution: ${this._recordVideoSize!.width}x${this._recordVideoSize!.height}`);
147+
}
148+
149+
// 6. Launch Browser
150+
const launchOptions: any = {
134151
headless: false, // Must be false here, handled via args above
135152
args: args,
136153
viewport: { width: 1920, height: 1080 },
@@ -139,7 +156,17 @@ export class SentienceBrowser {
139156
proxy: proxyConfig, // Pass proxy configuration
140157
// CRITICAL: Ignore HTTPS errors when using proxy (proxies often use self-signed certs)
141158
ignoreHTTPSErrors: proxyConfig !== undefined
142-
});
159+
};
160+
161+
// Add video recording if configured
162+
if (this._recordVideoDir) {
163+
launchOptions.recordVideo = {
164+
dir: this._recordVideoDir,
165+
size: this._recordVideoSize
166+
};
167+
}
168+
169+
this.context = await chromium.launchPersistentContext(this.userDataDir, launchOptions);
143170

144171
this.page = this.context.pages()[0] || await this.context.newPage();
145172

@@ -622,10 +649,46 @@ export class SentienceBrowser {
622649
return this.context;
623650
}
624651

625-
async close(): Promise<void> {
652+
async close(outputPath?: string): Promise<string | null> {
653+
let tempVideoPath: string | null = null;
654+
655+
// Get video path before closing (if recording was enabled)
656+
// Note: Playwright saves videos when pages/context close, but we can get the
657+
// expected path before closing. The actual file will be available after close.
658+
if (this._recordVideoDir) {
659+
try {
660+
// Try to get video path from the first page
661+
if (this.page) {
662+
const video = this.page.video();
663+
if (video) {
664+
tempVideoPath = await video.path();
665+
}
666+
}
667+
// If that fails, check all pages in the context (before closing)
668+
if (!tempVideoPath && this.context) {
669+
const pages = this.context.pages();
670+
for (const page of pages) {
671+
try {
672+
const video = page.video();
673+
if (video) {
674+
tempVideoPath = await video.path();
675+
break;
676+
}
677+
} catch {
678+
// Continue to next page
679+
}
680+
}
681+
}
682+
} catch {
683+
// Video path might not be available until after close
684+
// We'll use fallback mechanism below
685+
}
686+
}
687+
626688
const cleanup: Promise<void>[] = [];
627-
689+
628690
// Close context first (this also closes the browser for persistent contexts)
691+
// This triggers video file finalization
629692
if (this.context) {
630693
cleanup.push(
631694
this.context.close().catch(() => {
@@ -634,7 +697,7 @@ export class SentienceBrowser {
634697
);
635698
this.context = null;
636699
}
637-
700+
638701
// Close browser if it exists (for non-persistent contexts)
639702
if (this.browser) {
640703
cleanup.push(
@@ -647,7 +710,7 @@ export class SentienceBrowser {
647710

648711
// Wait for all cleanup to complete
649712
await Promise.all(cleanup);
650-
713+
651714
// Clean up extension directory
652715
if (this.extensionPath && fs.existsSync(this.extensionPath)) {
653716
try {
@@ -657,7 +720,47 @@ export class SentienceBrowser {
657720
}
658721
this.extensionPath = null;
659722
}
660-
723+
724+
// After context closes, verify video file exists if we have a path
725+
let finalPath = tempVideoPath;
726+
if (tempVideoPath && fs.existsSync(tempVideoPath)) {
727+
// Video file exists, proceed with rename if needed
728+
} else if (this._recordVideoDir && fs.existsSync(this._recordVideoDir)) {
729+
// Fallback: If we couldn't get the path but recording was enabled,
730+
// check the directory for video files
731+
try {
732+
const videoFiles = fs.readdirSync(this._recordVideoDir)
733+
.filter(f => f.endsWith('.webm'))
734+
.map(f => ({
735+
path: path.join(this._recordVideoDir!, f),
736+
mtime: fs.statSync(path.join(this._recordVideoDir!, f)).mtime.getTime()
737+
}))
738+
.sort((a, b) => b.mtime - a.mtime); // Most recent first
739+
740+
if (videoFiles.length > 0) {
741+
finalPath = videoFiles[0].path;
742+
}
743+
} catch {
744+
// Ignore errors when scanning directory
745+
}
746+
}
747+
748+
// Rename/move video if output_path is specified
749+
if (finalPath && outputPath && fs.existsSync(finalPath)) {
750+
try {
751+
// Ensure parent directory exists
752+
const outputDir = path.dirname(outputPath);
753+
if (!fs.existsSync(outputDir)) {
754+
fs.mkdirSync(outputDir, { recursive: true });
755+
}
756+
fs.renameSync(finalPath, outputPath);
757+
finalPath = outputPath;
758+
} catch (error: any) {
759+
console.warn(`Failed to rename video file: ${error.message}`);
760+
// Return original path if rename fails
761+
}
762+
}
763+
661764
// Clean up user data directory (only if it's a temp directory)
662765
// If user provided a custom userDataDir, we don't delete it (persistent sessions)
663766
if (this.userDataDir && fs.existsSync(this.userDataDir)) {
@@ -672,5 +775,7 @@ export class SentienceBrowser {
672775
}
673776
this.userDataDir = null;
674777
}
778+
779+
return finalPath;
675780
}
676781
}

0 commit comments

Comments
 (0)