Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions examples/video-recording-advanced.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* Advanced Video Recording Demo
*
* Demonstrates advanced video recording features:
* - Custom resolution (1080p)
* - Custom output filename
* - Multiple recordings in one session
*/

import { SentienceBrowser } from '../src/browser';
import * as path from 'path';
import * as fs from 'fs';

async function recordWithCustomSettings() {
console.log('\n' + '='.repeat(60));
console.log('Advanced Video Recording Demo');
console.log('='.repeat(60) + '\n');

const videoDir = path.join(process.cwd(), 'recordings');

// Example 1: Custom Resolution (1080p)
console.log('📹 Example 1: Recording in 1080p (Full HD)\n');

const browser1 = new SentienceBrowser(
undefined,
undefined,
false,
undefined,
undefined,
undefined,
videoDir,
{ width: 1920, height: 1080 } // 1080p resolution
);

await browser1.start();
console.log(' Resolution: 1920x1080');

const page1 = browser1.getPage();
await page1.goto('https://example.com');
await page1.waitForTimeout(2000);

// Close with custom filename
const video1 = await browser1.close(path.join(videoDir, 'example_1080p.webm'));
console.log(` ✅ Saved: ${video1}\n`);

// Example 2: Custom Filename with Timestamp
console.log('📹 Example 2: Recording with timestamp filename\n');

const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const customFilename = `recording_${timestamp}.webm`;

const browser2 = new SentienceBrowser(
undefined, undefined, false, undefined, undefined, undefined,
videoDir
);

await browser2.start();

const page2 = browser2.getPage();
await page2.goto('https://example.com');
await page2.click('text=More information');
await page2.waitForTimeout(2000);

const video2 = await browser2.close(path.join(videoDir, customFilename));
console.log(` ✅ Saved: ${video2}\n`);

// Example 3: Organized by Project
console.log('📹 Example 3: Organized directory structure\n');

const projectDir = path.join(videoDir, 'my_project', 'tutorials');
const browser3 = new SentienceBrowser(
undefined, undefined, false, undefined, undefined, undefined,
projectDir
);

await browser3.start();
console.log(` Saving to: ${projectDir}`);

const page3 = browser3.getPage();
await page3.goto('https://example.com');
await page3.waitForTimeout(2000);

const video3 = await browser3.close(path.join(projectDir, 'tutorial_01.webm'));
console.log(` ✅ Saved: ${video3}\n`);

console.log('='.repeat(60));
console.log('All recordings completed!');
console.log(`Check ${path.resolve(videoDir)} for all videos`);
console.log('='.repeat(60) + '\n');
}

// Run the demo
recordWithCustomSettings().catch(error => {
console.error('Error:', error);
process.exit(1);
});
70 changes: 70 additions & 0 deletions examples/video-recording-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Video Recording Demo - Record browser sessions with SentienceBrowser
*
* This example demonstrates how to use the video recording feature
* to capture browser automation sessions.
*/

import { SentienceBrowser } from '../src/browser';
import * as path from 'path';
import * as fs from 'fs';

async function main() {
// Create output directory for videos
const videoDir = path.join(process.cwd(), 'recordings');
if (!fs.existsSync(videoDir)) {
fs.mkdirSync(videoDir, { recursive: true });
}

console.log('\n' + '='.repeat(60));
console.log('Video Recording Demo');
console.log('='.repeat(60) + '\n');

// Create browser with video recording enabled
const browser = new SentienceBrowser(
undefined, // apiKey
undefined, // apiUrl
false, // headless - set to false so you can see the recording
undefined, // proxy
undefined, // userDataDir
undefined, // storageState
videoDir // recordVideoDir - enables video recording
);

await browser.start();
console.log('🎥 Video recording enabled');
console.log(`📁 Videos will be saved to: ${path.resolve(videoDir)}\n`);

try {
const page = browser.getPage();

// Navigate to example.com
console.log('Navigating to example.com...');
await page.goto('https://example.com');
await page.waitForLoadState('networkidle');

// Perform some actions
console.log('Taking screenshot...');
await page.screenshot({ path: 'example_screenshot.png' });

console.log('Scrolling page...');
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(1000);

console.log('\n✅ Recording complete!');
console.log('Video will be saved when browser closes...\n');
} finally {
// Video is automatically saved when browser closes
const videoPath = await browser.close();
console.log('='.repeat(60));
console.log(`Video saved to: ${videoPath}`);
console.log(`Check ${path.resolve(videoDir)} for the recorded video (.webm)`);
console.log('='.repeat(60) + '\n');
}
}

// Run the demo
main().catch(error => {
console.error('Error:', error);
process.exit(1);
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sentienceapi",
"version": "0.90.11",
"version": "0.90.12",
"description": "TypeScript SDK for Sentience AI Agent Browser Automation",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
123 changes: 114 additions & 9 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ export class SentienceBrowser {
private _proxy?: string;
private _userDataDir?: string;
private _storageState?: string | StorageState | object;
private _recordVideoDir?: string;
private _recordVideoSize?: { width: number; height: number };

constructor(
apiKey?: string,
apiUrl?: string,
headless?: boolean,
proxy?: string,
userDataDir?: string,
storageState?: string | StorageState | object
storageState?: string | StorageState | object,
recordVideoDir?: string,
recordVideoSize?: { width: number; height: number }
) {
this._apiKey = apiKey;

Expand All @@ -54,6 +58,10 @@ export class SentienceBrowser {
// Auth injection support
this._userDataDir = userDataDir;
this._storageState = storageState;

// Video recording support
this._recordVideoDir = recordVideoDir;
this._recordVideoSize = recordVideoSize || { width: 1280, height: 800 };
}

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

// 5. Launch Browser
this.context = await chromium.launchPersistentContext(this.userDataDir, {
// 5. Setup video recording directory if requested
if (this._recordVideoDir) {
if (!fs.existsSync(this._recordVideoDir)) {
fs.mkdirSync(this._recordVideoDir, { recursive: true });
}
console.log(`🎥 [Sentience] Recording video to: ${this._recordVideoDir}`);
console.log(` Resolution: ${this._recordVideoSize!.width}x${this._recordVideoSize!.height}`);
}

// 6. Launch Browser
const launchOptions: any = {
headless: false, // Must be false here, handled via args above
args: args,
viewport: { width: 1920, height: 1080 },
Expand All @@ -139,7 +156,17 @@ export class SentienceBrowser {
proxy: proxyConfig, // Pass proxy configuration
// CRITICAL: Ignore HTTPS errors when using proxy (proxies often use self-signed certs)
ignoreHTTPSErrors: proxyConfig !== undefined
});
};

// Add video recording if configured
if (this._recordVideoDir) {
launchOptions.recordVideo = {
dir: this._recordVideoDir,
size: this._recordVideoSize
};
}

this.context = await chromium.launchPersistentContext(this.userDataDir, launchOptions);

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

Expand Down Expand Up @@ -622,10 +649,46 @@ export class SentienceBrowser {
return this.context;
}

async close(): Promise<void> {
async close(outputPath?: string): Promise<string | null> {
let tempVideoPath: string | null = null;

// Get video path before closing (if recording was enabled)
// Note: Playwright saves videos when pages/context close, but we can get the
// expected path before closing. The actual file will be available after close.
if (this._recordVideoDir) {
try {
// Try to get video path from the first page
if (this.page) {
const video = this.page.video();
if (video) {
tempVideoPath = await video.path();
}
}
// If that fails, check all pages in the context (before closing)
if (!tempVideoPath && this.context) {
const pages = this.context.pages();
for (const page of pages) {
try {
const video = page.video();
if (video) {
tempVideoPath = await video.path();
break;
}
} catch {
// Continue to next page
}
}
}
} catch {
// Video path might not be available until after close
// We'll use fallback mechanism below
}
}

const cleanup: Promise<void>[] = [];

// Close context first (this also closes the browser for persistent contexts)
// This triggers video file finalization
if (this.context) {
cleanup.push(
this.context.close().catch(() => {
Expand All @@ -634,7 +697,7 @@ export class SentienceBrowser {
);
this.context = null;
}

// Close browser if it exists (for non-persistent contexts)
if (this.browser) {
cleanup.push(
Expand All @@ -647,7 +710,7 @@ export class SentienceBrowser {

// Wait for all cleanup to complete
await Promise.all(cleanup);

// Clean up extension directory
if (this.extensionPath && fs.existsSync(this.extensionPath)) {
try {
Expand All @@ -657,7 +720,47 @@ export class SentienceBrowser {
}
this.extensionPath = null;
}


// After context closes, verify video file exists if we have a path
let finalPath = tempVideoPath;
if (tempVideoPath && fs.existsSync(tempVideoPath)) {
// Video file exists, proceed with rename if needed
} else if (this._recordVideoDir && fs.existsSync(this._recordVideoDir)) {
// Fallback: If we couldn't get the path but recording was enabled,
// check the directory for video files
try {
const videoFiles = fs.readdirSync(this._recordVideoDir)
.filter(f => f.endsWith('.webm'))
.map(f => ({
path: path.join(this._recordVideoDir!, f),
mtime: fs.statSync(path.join(this._recordVideoDir!, f)).mtime.getTime()
}))
.sort((a, b) => b.mtime - a.mtime); // Most recent first

if (videoFiles.length > 0) {
finalPath = videoFiles[0].path;
}
} catch {
// Ignore errors when scanning directory
}
}

// Rename/move video if output_path is specified
if (finalPath && outputPath && fs.existsSync(finalPath)) {
try {
// Ensure parent directory exists
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
fs.renameSync(finalPath, outputPath);
finalPath = outputPath;
} catch (error: any) {
console.warn(`Failed to rename video file: ${error.message}`);
// Return original path if rename fails
}
}

// Clean up user data directory (only if it's a temp directory)
// If user provided a custom userDataDir, we don't delete it (persistent sessions)
if (this.userDataDir && fs.existsSync(this.userDataDir)) {
Expand All @@ -672,5 +775,7 @@ export class SentienceBrowser {
}
this.userDataDir = null;
}

return finalPath;
}
}
Loading