@@ -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