Spaces:
Running
Running
File size: 7,424 Bytes
399b80c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | import aiohttp
from typing import Optional
from openspace.utils.logging import Logger
from .config import get_client_base_url
logger = Logger.get_logger(__name__)
class RecordingClient:
"""
Client for screen recording via HTTP API.
This client directly calls the local server's recording endpoints:
- POST /start_recording
- POST /end_recording
"""
def __init__(
self,
base_url: Optional[str] = None,
timeout: int = 30
):
"""
Initialize recording client.
Args:
base_url: Base URL of the local server
(default: read from local_server/config.json or env LOCAL_SERVER_URL)
timeout: Request timeout in seconds
"""
# Get base_url: priority is explicit > env > config file
if base_url is None:
base_url = get_client_base_url()
self.base_url = base_url.rstrip("/")
self.timeout = timeout
self._session: Optional[aiohttp.ClientSession] = None
async def _get_session(self) -> aiohttp.ClientSession:
"""Get or create aiohttp session."""
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=self.timeout)
)
return self._session
async def start_recording(self, auto_cleanup: bool = True) -> bool:
"""
Start screen recording.
Args:
auto_cleanup: If True, automatically end previous recording if one is in progress
"""
try:
session = await self._get_session()
url = f"{self.base_url}/start_recording"
async with session.post(url) as response:
if response.status == 200:
logger.info("Screen recording started")
return True
elif response.status == 400 and auto_cleanup:
# Check if error is due to recording already in progress
error_text = await response.text()
if "already in progress" in error_text.lower():
logger.warning("Recording already in progress, stopping previous recording...")
# Try to end the previous recording
video_bytes = await self.end_recording()
if video_bytes:
logger.info("Previous recording ended successfully, retrying start...")
else:
logger.warning("Failed to end previous recording, but will retry start anyway...")
# Retry starting recording (without auto_cleanup to avoid infinite loop)
return await self.start_recording(auto_cleanup=False)
else:
logger.error(f"Failed to start recording: HTTP {response.status} - {error_text}")
return False
else:
error_text = await response.text()
logger.error(f"Failed to start recording: HTTP {response.status} - {error_text}")
return False
except Exception as e:
logger.error(f"Failed to start recording: {e}")
return False
async def end_recording(self, dest: Optional[str] = None) -> Optional[bytes]:
"""
End screen recording and optionally save to file.
"""
try:
session = await self._get_session()
url = f"{self.base_url}/end_recording"
# Use longer timeout for end_recording (file may be large)
async with session.post(url, timeout=aiohttp.ClientTimeout(total=60)) as response:
if response.status == 200:
video_bytes = await response.read()
# Save to file if destination provided
if dest:
try:
with open(dest, "wb") as f:
f.write(video_bytes)
logger.info(f"Recording saved to: {dest}")
except Exception as e:
logger.error(f"Failed to save recording file: {e}")
return None
logger.info("Screen recording ended")
return video_bytes
else:
error_text = await response.text()
logger.error(f"Failed to end recording: HTTP {response.status} - {error_text}")
return None
except Exception as e:
logger.error(f"Failed to end recording: {e}")
return None
async def close(self):
"""Close the HTTP session."""
if self._session and not self._session.closed:
await self._session.close()
# Give aiohttp time to finish cleanup callbacks
import asyncio
await asyncio.sleep(0.25)
logger.debug("Recording client session closed")
async def __aenter__(self):
"""Context manager entry."""
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
await self.close()
return False
class RecordingContextManager:
def __init__(
self,
base_url: Optional[str] = None,
output_path: Optional[str] = None,
timeout: Optional[int] = None
):
"""
Initialize recording context manager.
Args:
base_url: Base URL of the local server (default: from config)
output_path: Path to save recording (default: from config)
timeout: Request timeout in seconds (default: from config)
"""
# Load output_path from config if not provided
if output_path is None:
try:
from openspace.config import get_config
config = get_config()
if config.recording.screen_recording_path:
output_path = config.recording.screen_recording_path
except Exception:
pass
self.client = RecordingClient(base_url=base_url, timeout=timeout)
self.output_path = output_path
self.recording_started = False
async def __aenter__(self) -> RecordingClient:
"""Start recording on context entry."""
success = await self.client.start_recording()
if success:
self.recording_started = True
logger.info("Recording context started")
else:
logger.warning("Failed to start recording in context")
return self.client
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Stop recording on context exit."""
if self.recording_started:
try:
await self.client.end_recording(dest=self.output_path)
logger.info("Recording context ended")
except Exception as e:
logger.error(f"Failed to end recording in context: {e}")
await self.client.close()
return False |