Skip to content

Commit 958d883

Browse files
author
SentienceDEV
committed
Merge pull request #83 from SentienceAPI/video_recording
browser video recording
2 parents c299ba8 + e037dbd commit 958d883

File tree

15 files changed

+637
-185
lines changed

15 files changed

+637
-185
lines changed

.github/workflows/release.yml

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
- name: Verify extension files are present
5454
run: |
5555
echo "🔍 Verifying extension files are included..."
56-
56+
5757
# Check required extension files exist
5858
REQUIRED_FILES=(
5959
"sentience/extension/manifest.json"
@@ -63,14 +63,14 @@ jobs:
6363
"sentience/extension/pkg/sentience_core.js"
6464
"sentience/extension/pkg/sentience_core_bg.wasm"
6565
)
66-
66+
6767
MISSING_FILES=()
6868
for file in "${REQUIRED_FILES[@]}"; do
6969
if [ ! -f "$file" ]; then
7070
MISSING_FILES+=("$file")
7171
fi
7272
done
73-
73+
7474
if [ ${#MISSING_FILES[@]} -ne 0 ]; then
7575
echo "❌ Error: Missing required extension files:"
7676
printf ' - %s\n' "${MISSING_FILES[@]}"
@@ -79,14 +79,14 @@ jobs:
7979
echo "Run the sync-extension workflow or manually sync extension files."
8080
exit 1
8181
fi
82-
82+
8383
# Verify findTextRect function exists in injected_api.js
8484
if ! grep -q "findTextRect:" sentience/extension/injected_api.js; then
8585
echo "❌ Error: findTextRect function not found in injected_api.js"
8686
echo "The extension may be out of date. Please sync the extension before releasing."
8787
exit 1
8888
fi
89-
89+
9090
echo "✅ All extension files verified"
9191
echo "📦 Extension files that will be included:"
9292
find sentience/extension -type f | sort
@@ -98,38 +98,38 @@ jobs:
9898
- name: Check package
9999
run: |
100100
twine check dist/*
101-
101+
102102
- name: Verify extension files in built package
103103
run: |
104104
echo "🔍 Verifying extension files are included in the built package..."
105-
105+
106106
# Extract wheel to check contents
107107
WHEEL_FILE=$(ls dist/*.whl | head -1)
108108
WHEEL_PATH=$(realpath "$WHEEL_FILE")
109109
echo "Checking wheel: $WHEEL_PATH"
110-
110+
111111
# Create temp directory for extraction
112112
TEMP_DIR=$(mktemp -d)
113113
cd "$TEMP_DIR"
114-
114+
115115
# Extract wheel (it's a zip file)
116116
unzip -q "$WHEEL_PATH"
117-
117+
118118
# Check for required extension files in the wheel
119119
REQUIRED_IN_WHEEL=(
120120
"sentience/extension/manifest.json"
121121
"sentience/extension/injected_api.js"
122122
"sentience/extension/pkg/sentience_core.js"
123123
"sentience/extension/pkg/sentience_core_bg.wasm"
124124
)
125-
125+
126126
MISSING_IN_WHEEL=()
127127
for file in "${REQUIRED_IN_WHEEL[@]}"; do
128128
if [ ! -f "$file" ]; then
129129
MISSING_IN_WHEEL+=("$file")
130130
fi
131131
done
132-
132+
133133
if [ ${#MISSING_IN_WHEEL[@]} -ne 0 ]; then
134134
echo "❌ Error: Extension files missing from built wheel:"
135135
printf ' - %s\n' "${MISSING_IN_WHEEL[@]}"
@@ -138,17 +138,17 @@ jobs:
138138
echo "Check MANIFEST.in and pyproject.toml package-data settings."
139139
exit 1
140140
fi
141-
141+
142142
# Verify findTextRect is in the packaged injected_api.js
143143
if ! grep -q "findTextRect:" sentience/extension/injected_api.js; then
144144
echo "❌ Error: findTextRect not found in packaged injected_api.js"
145145
exit 1
146146
fi
147-
147+
148148
echo "✅ All extension files verified in built package"
149149
echo "📦 Extension files found in wheel:"
150150
find sentience/extension -type f | sort
151-
151+
152152
# Cleanup
153153
rm -rf "$TEMP_DIR"
154154
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
from datetime import datetime
11+
from pathlib import Path
12+
13+
from sentience import SentienceBrowser
14+
15+
16+
def main():
17+
print("\n" + "=" * 60)
18+
print("Advanced Video Recording Demo")
19+
print("=" * 60 + "\n")
20+
21+
video_dir = Path("./recordings")
22+
video_dir.mkdir(exist_ok=True)
23+
24+
# Example 1: Custom Resolution (1080p)
25+
print("📹 Example 1: Recording in 1080p (Full HD)\n")
26+
27+
with SentienceBrowser(
28+
record_video_dir=str(video_dir),
29+
record_video_size={"width": 1920, "height": 1080}, # 1080p resolution
30+
) as browser:
31+
print(" Resolution: 1920x1080")
32+
browser.page.goto("https://example.com")
33+
browser.page.wait_for_load_state("networkidle")
34+
browser.page.wait_for_timeout(2000)
35+
36+
# Close with custom filename
37+
video_path = browser.close(output_path=video_dir / "example_1080p.webm")
38+
print(f" ✅ Saved: {video_path}\n")
39+
40+
# Example 2: Custom Filename with Timestamp
41+
print("📹 Example 2: Recording with timestamp filename\n")
42+
43+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
44+
custom_filename = f"recording_{timestamp}.webm"
45+
46+
with SentienceBrowser(record_video_dir=str(video_dir)) as browser:
47+
browser.page.goto("https://example.com")
48+
browser.page.click("text=More information")
49+
browser.page.wait_for_timeout(2000)
50+
51+
video_path = browser.close(output_path=video_dir / custom_filename)
52+
print(f" ✅ Saved: {video_path}\n")
53+
54+
# Example 3: Organized by Project
55+
print("📹 Example 3: Organized directory structure\n")
56+
57+
project_dir = Path("./recordings/my_project/tutorials")
58+
59+
with SentienceBrowser(record_video_dir=str(project_dir)) as browser:
60+
print(f" Saving to: {project_dir}")
61+
browser.page.goto("https://example.com")
62+
browser.page.wait_for_timeout(2000)
63+
64+
video_path = browser.close(output_path=project_dir / "tutorial_01.webm")
65+
print(f" ✅ Saved: {video_path}\n")
66+
67+
# Example 4: Multiple videos with descriptive names
68+
print("📹 Example 4: Tutorial series with descriptive names\n")
69+
70+
tutorials = [
71+
("intro", "https://example.com"),
72+
("navigation", "https://example.com"),
73+
("features", "https://example.com"),
74+
]
75+
76+
for name, url in tutorials:
77+
with SentienceBrowser(record_video_dir=str(video_dir)) as browser:
78+
browser.page.goto(url)
79+
browser.page.wait_for_timeout(1000)
80+
81+
video_path = browser.close(output_path=video_dir / f"{name}.webm")
82+
print(f" ✅ {name}: {video_path}")
83+
84+
print("\n" + "=" * 60)
85+
print("All recordings completed!")
86+
print(f"Check {video_dir.absolute()} for all videos")
87+
print("=" * 60 + "\n")
88+
89+
90+
if __name__ == "__main__":
91+
main()

examples/video_recording_demo.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
from pathlib import Path
9+
10+
from sentience import SentienceBrowser
11+
12+
13+
def main():
14+
# Create output directory for videos
15+
video_dir = Path("./recordings")
16+
video_dir.mkdir(exist_ok=True)
17+
18+
print("\n" + "=" * 60)
19+
print("Video Recording Demo")
20+
print("=" * 60 + "\n")
21+
22+
# Create browser with video recording enabled
23+
with SentienceBrowser(record_video_dir=str(video_dir)) as browser:
24+
print("🎥 Video recording enabled")
25+
print(f"📁 Videos will be saved to: {video_dir.absolute()}\n")
26+
27+
# Navigate to example.com
28+
print("Navigating to example.com...")
29+
browser.page.goto("https://example.com")
30+
browser.page.wait_for_load_state("networkidle")
31+
32+
# Perform some actions
33+
print("Taking screenshot...")
34+
browser.page.screenshot(path="example_screenshot.png")
35+
36+
print("Scrolling page...")
37+
browser.page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
38+
browser.page.wait_for_timeout(1000)
39+
40+
print("\n✅ Recording complete!")
41+
print("Video will be saved when browser closes...\n")
42+
43+
# Video is automatically saved when context manager exits
44+
print("=" * 60)
45+
print(f"Check {video_dir.absolute()} for the recorded video (.webm)")
46+
print("=" * 60 + "\n")
47+
48+
49+
if __name__ == "__main__":
50+
main()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "sentienceapi"
7-
version = "0.90.11"
7+
version = "0.90.12"
88
description = "Python SDK for Sentience AI Agent Browser Automation"
99
readme = "README.md"
1010
requires-python = ">=3.11"

sentience/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
)
7171
from .wait import wait_for
7272

73-
__version__ = "0.90.11"
73+
__version__ = "0.90.12"
7474

7575
__all__ = [
7676
# Core SDK

sentience/browser.py

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ def __init__(
3333
proxy: str | None = None,
3434
user_data_dir: str | None = None,
3535
storage_state: str | Path | StorageState | dict | None = None,
36+
record_video_dir: str | Path | None = None,
37+
record_video_size: dict[str, int] | None = None,
3638
):
3739
"""
3840
Initialize Sentience browser
@@ -57,6 +59,14 @@ def __init__(
5759
- StorageState object
5860
- Dictionary with 'cookies' and/or 'origins' keys
5961
If provided, browser starts with pre-injected authentication.
62+
record_video_dir: Optional directory path to save video recordings.
63+
If provided, browser will record video of all pages.
64+
Videos are saved as .webm files in the specified directory.
65+
If None, no video recording is performed.
66+
record_video_size: Optional video resolution as dict with 'width' and 'height' keys.
67+
Examples: {"width": 1280, "height": 800} (default)
68+
{"width": 1920, "height": 1080} (1080p)
69+
If None, defaults to 1280x800.
6070
"""
6171
self.api_key = api_key
6272
# Only set api_url if api_key is provided, otherwise None (free tier)
@@ -80,6 +90,10 @@ def __init__(
8090
self.user_data_dir = user_data_dir
8191
self.storage_state = storage_state
8292

93+
# Video recording support
94+
self.record_video_dir = record_video_dir
95+
self.record_video_size = record_video_size or {"width": 1280, "height": 800}
96+
8397
self.playwright: Playwright | None = None
8498
self.context: BrowserContext | None = None
8599
self.page: Page | None = None
@@ -209,6 +223,17 @@ def start(self) -> None:
209223
launch_params["ignore_https_errors"] = True
210224
print(f"🌐 [Sentience] Using proxy: {proxy_config.server}")
211225

226+
# Add video recording if configured
227+
if self.record_video_dir:
228+
video_dir = Path(self.record_video_dir)
229+
video_dir.mkdir(parents=True, exist_ok=True)
230+
launch_params["record_video_dir"] = str(video_dir)
231+
launch_params["record_video_size"] = self.record_video_size
232+
print(f"🎥 [Sentience] Recording video to: {video_dir}")
233+
print(
234+
f" Resolution: {self.record_video_size['width']}x{self.record_video_size['height']}"
235+
)
236+
212237
# Launch persistent context (required for extensions)
213238
# Note: We pass headless=False to launch_persistent_context because we handle
214239
# headless mode via the --headless=new arg above. This is a Playwright workaround.
@@ -390,15 +415,71 @@ def _wait_for_extension(self, timeout_sec: float = 5.0) -> bool:
390415

391416
return False
392417

393-
def close(self) -> None:
394-
"""Close browser and cleanup"""
418+
def close(self, output_path: str | Path | None = None) -> str | None:
419+
"""
420+
Close browser and cleanup
421+
422+
Args:
423+
output_path: Optional path to rename the video file to.
424+
If provided, the recorded video will be moved to this location.
425+
Useful for giving videos meaningful names instead of random hashes.
426+
427+
Returns:
428+
Path to video file if recording was enabled, None otherwise
429+
Note: Video files are saved automatically by Playwright when context closes.
430+
If multiple pages exist, returns the path to the first page's video.
431+
"""
432+
temp_video_path = None
433+
434+
# Get video path before closing (if recording was enabled)
435+
# Note: Playwright saves videos when pages/context close, but we can get the
436+
# expected path before closing. The actual file will be available after close.
437+
if self.record_video_dir:
438+
try:
439+
# Try to get video path from the first page
440+
if self.page and self.page.video:
441+
temp_video_path = self.page.video.path()
442+
# If that fails, check all pages in the context
443+
elif self.context:
444+
for page in self.context.pages:
445+
if page.video:
446+
temp_video_path = page.video.path()
447+
break
448+
except Exception:
449+
# Video path might not be available until after close
450+
# In that case, we'll return None and user can check the directory
451+
pass
452+
453+
# Close context (this triggers video file finalization)
395454
if self.context:
396455
self.context.close()
456+
457+
# Close playwright
397458
if self.playwright:
398459
self.playwright.stop()
460+
461+
# Clean up extension directory
399462
if self._extension_path and os.path.exists(self._extension_path):
400463
shutil.rmtree(self._extension_path)
401464

465+
# Rename/move video if output_path is specified
466+
final_path = temp_video_path
467+
if temp_video_path and output_path and os.path.exists(temp_video_path):
468+
try:
469+
output_path = str(output_path)
470+
# Ensure parent directory exists
471+
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
472+
shutil.move(temp_video_path, output_path)
473+
final_path = output_path
474+
except Exception as e:
475+
import warnings
476+
477+
warnings.warn(f"Failed to rename video file: {e}")
478+
# Return original path if rename fails
479+
final_path = temp_video_path
480+
481+
return final_path
482+
402483
def __enter__(self):
403484
"""Context manager entry"""
404485
self.start()

0 commit comments

Comments
 (0)