Skip to content

Commit c707c7c

Browse files
committed
feat: update capture binary support and add session export
- Rename recorder references to capture in CaptureClient - Bump videodb-capture-bin dependency to >=0.2.8 - Add channels, export_status, exported_videos to CaptureSession - Add displays property and export() method to CaptureSession - Fix generate_clip docstring return type to SearchResult - Bump version to 0.4.1
1 parent 4800657 commit c707c7c

5 files changed

Lines changed: 58 additions & 19 deletions

File tree

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"websockets>=11.0.3",
4040
],
4141
extras_require={
42-
"capture": ["videodb-capture-bin>=0.2.7"],
42+
"capture": ["videodb-capture-bin>=0.2.8"],
4343
},
4444
classifiers=[
4545
"Intended Audience :: Developers",

videodb/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44

5-
__version__ = "0.4.0"
5+
__version__ = "0.4.1"
66
__title__ = "videodb"
77
__author__ = "videodb"
88
__email__ = "contact@videodb.io"

videodb/capture.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99

1010
logger = logging.getLogger(__name__)
1111

12-
def get_recorder_path():
12+
def get_capture_binary_path():
1313
"""
14-
Attempts to find the path to the recorder binary.
14+
Attempts to find the path to the capture binary.
1515
If the optional 'videodb-capture-bin' package is not installed,
1616
it raises a RuntimeError with instructions.
1717
"""
@@ -21,13 +21,13 @@ def get_recorder_path():
2121
except ImportError:
2222
error_msg = (
2323
"Capture runtime not found.\n"
24-
"To use recording features, please install the capture dependencies:\n"
24+
"To use capture features, please install the capture dependencies:\n"
2525
"pip install 'videodb[capture]'"
2626
)
2727
logger.error(error_msg)
2828
raise RuntimeError(error_msg)
2929
except Exception as e:
30-
logger.error(f"Failed to resolve recorder path: {e}")
30+
logger.error(f"Failed to resolve capture binary path: {e}")
3131
raise
3232

3333

@@ -167,14 +167,14 @@ def __init__(
167167
self._session_id: Optional[str] = None
168168
self._proc = None
169169
self._futures: Dict[str, asyncio.Future] = {}
170-
self._binary_path = get_recorder_path()
170+
self._binary_path = get_capture_binary_path()
171171
self._event_queue = asyncio.Queue()
172172

173173
def __repr__(self) -> str:
174174
return f"CaptureClient(base_url={self.base_url})"
175175

176176
async def _ensure_process(self):
177-
"""Ensure the recorder binary is running."""
177+
"""Ensure the capture binary is running."""
178178
if self._proc is not None and self._proc.returncode is None:
179179
return
180180

@@ -194,7 +194,7 @@ async def _ensure_process(self):
194194
async def _send_command(
195195
self, command: str, params: Optional[Dict[str, Any]] = None
196196
) -> Dict[str, Any]:
197-
"""Send a command to the recorder binary and await response.
197+
"""Send a command to the capture binary and await response.
198198
199199
:param str command: Command name.
200200
:param dict params: Command parameters.
@@ -210,7 +210,7 @@ async def _send_command(
210210
"params": params or {},
211211
}
212212

213-
# Framing: videodb_recorder|<JSON>\n
213+
# IPC protocol framing: videodb_recorder|<JSON>\n
214214
message = f"videodb_recorder|{json.dumps(payload)}\n"
215215
self._proc.stdin.write(message.encode("utf-8"))
216216
await self._proc.stdin.drain()
@@ -254,18 +254,18 @@ async def _read_stdout_loop(self):
254254
await self._event_queue.put(data)
255255

256256
except Exception as e:
257-
logger.error(f"Failed to parse recorder message: {e}")
257+
logger.error(f"Failed to parse capture message: {e}")
258258

259259
async def _read_stderr_loop(self):
260260
"""Loop to read stderr and log messages."""
261261
while True:
262262
line = await self._proc.stderr.readline()
263263
if not line:
264264
break
265-
logger.debug(f"[Recorder Binary]: {line.decode('utf-8', errors='replace').strip()}")
265+
logger.debug(f"[Capture Binary]: {line.decode('utf-8', errors='replace').strip()}")
266266

267267
async def shutdown(self):
268-
"""Cleanly terminate the recorder binary process."""
268+
"""Cleanly terminate the capture binary process."""
269269
if self._proc:
270270
try:
271271
# Try graceful shutdown command first
@@ -392,7 +392,7 @@ async def stop_session(self) -> None:
392392
await self._send_command("stopRecording", {"sessionId": self._session_id})
393393

394394
async def events(self):
395-
"""Async generator that yields events from the recorder."""
395+
"""Async generator that yields events from the capture binary."""
396396
while True:
397397
try:
398398
# Use a timeout so we can check if the process is still alive

videodb/capture_session.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import List
1+
from typing import List, Optional
2+
from videodb._constants import ApiPath
23
from videodb.rtstream import RTStream
34

45

@@ -10,6 +11,10 @@ class CaptureSession:
1011
:ivar str end_user_id: ID of the end user
1112
:ivar str client_id: Client-provided session ID
1213
:ivar str status: Current status of the session
14+
:ivar list channels: List of channel dicts with id, name, type, is_primary
15+
:ivar str primary_video_channel_id: Channel ID of the primary video source
16+
:ivar str export_status: Current export status (exporting, exported, failed)
17+
:ivar dict exported_videos: Mapping of channel_id to exported video_id
1318
"""
1419

1520
def __init__(self, _connection, id: str, collection_id: str, **kwargs) -> None:
@@ -35,6 +40,10 @@ def _update_attributes(self, data: dict) -> None:
3540
self.callback_url = data.get("callback_url")
3641
self.exported_video_id = data.get("exported_video_id")
3742
self.metadata = data.get("metadata", {})
43+
self.channels = data.get("channels", [])
44+
self.primary_video_channel_id = data.get("primary_video_channel_id")
45+
self.export_status = data.get("export_status")
46+
self.exported_videos = data.get("exported_videos", {})
3847

3948
self.rtstreams = []
4049
for rts_data in data.get("rtstreams", []):
@@ -43,6 +52,15 @@ def _update_attributes(self, data: dict) -> None:
4352
stream = RTStream(self._connection, **rts_data)
4453
self.rtstreams.append(stream)
4554

55+
@property
56+
def displays(self) -> list:
57+
"""Video channels in the session.
58+
59+
:return: List of channel dicts where type is 'video'
60+
:rtype: list[dict]
61+
"""
62+
return [ch for ch in self.channels if ch.get("type") == "video"]
63+
4664
def get_rtstream(self, category: str) -> List[RTStream]:
4765
"""Get list of RTStreams by category.
4866
@@ -59,3 +77,24 @@ def get_rtstream(self, category: str) -> List[RTStream]:
5977
filtered_streams.append(stream)
6078

6179
return filtered_streams
80+
81+
def export(self, video_channel_id: Optional[str] = None) -> dict:
82+
"""Trigger export for this capture session.
83+
84+
Returns the existing video_id immediately if the channel was already
85+
exported. Otherwise starts an async export and returns.
86+
87+
:param str video_channel_id: Optional channel ID of the video to export.
88+
Defaults to the primary video channel.
89+
:return: Export response with session_id, video_channel_id, and
90+
video_id (if already exported)
91+
:rtype: dict
92+
"""
93+
data = {}
94+
if video_channel_id:
95+
data["video_channel_id"] = video_channel_id
96+
97+
return self._connection.post(
98+
path=f"{ApiPath.collection}/{self.collection_id}/{ApiPath.capture}/{ApiPath.session}/{self.id}/{ApiPath.export}",
99+
data=data,
100+
)

videodb/video.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -707,10 +707,10 @@ def clip(
707707
) -> str:
708708
"""Generate a clip from the video using a prompt.
709709
:param str prompt: Prompt to generate the clip
710-
:param str content_type: Content type for the clip
711-
:param str model_name: Model name for generation
712-
:return: The stream url of the generated clip
713-
:rtype: str
710+
:param str content_type: Content type for the clip. Valid options: "spoken", "visual", "multimodal"
711+
:param str model_name: Model tier for generation. Valid options: "basic", "pro", "ultra"
712+
:return: The search result of the generated clip
713+
:rtype: :class:`SearchResult <SearchResult>`
714714
"""
715715

716716
clip_data = self._connection.post(

0 commit comments

Comments
 (0)