Backend:
- VideoCaptureSource dataclass with url, loop, playback_speed, start/end_time,
resolution_limit, clock_id, target_fps fields
- VideoCaptureStream: OpenCV decode thread with frame-accurate sync clock seeking,
loop, trim range, resolution downscale at decode time
- YouTube URL resolution via yt-dlp (auto-detects youtube.com, youtu.be, shorts)
- Thumbnail extraction from first frame (GET /picture-sources/{id}/thumbnail)
- Video test WS preview: streams JPEG frames with elapsed/frame_count metadata
- Run video_stream.start() in executor to avoid blocking event loop during
yt-dlp resolution
- Full CRUD via existing picture source API (stream_type: "video")
- Wired into LiveStreamManager for target streaming
Frontend:
- Video icon (film) in picture source type map and graph node subtypes
- Video tree nav node in Sources tab with CardSection
- Video fields in stream add/edit modal: URL, loop toggle, playback speed slider,
target FPS, start/end trim times, resolution limit
- Video card rendering with URL, FPS, loop, speed badges
- Clone data support for video sources
- i18n keys for video source in en/ru/zh
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
108 lines
6.2 KiB
Python
108 lines
6.2 KiB
Python
"""Picture source schemas."""
|
|
|
|
from datetime import datetime
|
|
from typing import List, Literal, Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class PictureSourceCreate(BaseModel):
|
|
"""Request to create a picture source."""
|
|
|
|
name: str = Field(description="Stream name", min_length=1, max_length=100)
|
|
stream_type: Literal["raw", "processed", "static_image", "video"] = Field(description="Stream type")
|
|
display_index: Optional[int] = Field(None, description="Display index (raw streams)", ge=0)
|
|
capture_template_id: Optional[str] = Field(None, description="Capture template ID (raw streams)")
|
|
target_fps: Optional[int] = Field(None, description="Target FPS", ge=1, le=90)
|
|
source_stream_id: Optional[str] = Field(None, description="Source stream ID (processed streams)")
|
|
postprocessing_template_id: Optional[str] = Field(None, description="Postprocessing template ID (processed streams)")
|
|
image_source: Optional[str] = Field(None, description="Image URL or file path (static_image streams)")
|
|
description: Optional[str] = Field(None, description="Stream description", max_length=500)
|
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
|
# Video fields
|
|
url: Optional[str] = Field(None, description="Video URL, file path, or YouTube URL")
|
|
loop: bool = Field(True, description="Loop video playback")
|
|
playback_speed: float = Field(1.0, description="Playback speed multiplier", ge=0.1, le=10.0)
|
|
start_time: Optional[float] = Field(None, description="Trim start time in seconds", ge=0)
|
|
end_time: Optional[float] = Field(None, description="Trim end time in seconds", ge=0)
|
|
resolution_limit: Optional[int] = Field(None, description="Max width in pixels for decode downscale", ge=64, le=7680)
|
|
clock_id: Optional[str] = Field(None, description="Sync clock ID for frame-accurate timing")
|
|
|
|
|
|
class PictureSourceUpdate(BaseModel):
|
|
"""Request to update a picture source."""
|
|
|
|
name: Optional[str] = Field(None, description="Stream name", min_length=1, max_length=100)
|
|
display_index: Optional[int] = Field(None, description="Display index (raw streams)", ge=0)
|
|
capture_template_id: Optional[str] = Field(None, description="Capture template ID (raw streams)")
|
|
target_fps: Optional[int] = Field(None, description="Target FPS", ge=1, le=90)
|
|
source_stream_id: Optional[str] = Field(None, description="Source stream ID (processed streams)")
|
|
postprocessing_template_id: Optional[str] = Field(None, description="Postprocessing template ID (processed streams)")
|
|
image_source: Optional[str] = Field(None, description="Image URL or file path (static_image streams)")
|
|
description: Optional[str] = Field(None, description="Stream description", max_length=500)
|
|
tags: Optional[List[str]] = None
|
|
# Video fields
|
|
url: Optional[str] = Field(None, description="Video URL, file path, or YouTube URL")
|
|
loop: Optional[bool] = Field(None, description="Loop video playback")
|
|
playback_speed: Optional[float] = Field(None, description="Playback speed multiplier", ge=0.1, le=10.0)
|
|
start_time: Optional[float] = Field(None, description="Trim start time in seconds", ge=0)
|
|
end_time: Optional[float] = Field(None, description="Trim end time in seconds", ge=0)
|
|
resolution_limit: Optional[int] = Field(None, description="Max width in pixels for decode downscale", ge=64, le=7680)
|
|
clock_id: Optional[str] = Field(None, description="Sync clock ID for frame-accurate timing")
|
|
|
|
|
|
class PictureSourceResponse(BaseModel):
|
|
"""Picture source information response."""
|
|
|
|
id: str = Field(description="Stream ID")
|
|
name: str = Field(description="Stream name")
|
|
stream_type: str = Field(description="Stream type (raw, processed, static_image, or video)")
|
|
display_index: Optional[int] = Field(None, description="Display index")
|
|
capture_template_id: Optional[str] = Field(None, description="Capture template ID")
|
|
target_fps: Optional[int] = Field(None, description="Target FPS")
|
|
source_stream_id: Optional[str] = Field(None, description="Source stream ID")
|
|
postprocessing_template_id: Optional[str] = Field(None, description="Postprocessing template ID")
|
|
image_source: Optional[str] = Field(None, description="Image URL or file path")
|
|
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
|
created_at: datetime = Field(description="Creation timestamp")
|
|
updated_at: datetime = Field(description="Last update timestamp")
|
|
description: Optional[str] = Field(None, description="Stream description")
|
|
# Video fields
|
|
url: Optional[str] = Field(None, description="Video URL")
|
|
loop: Optional[bool] = Field(None, description="Loop video playback")
|
|
playback_speed: Optional[float] = Field(None, description="Playback speed multiplier")
|
|
start_time: Optional[float] = Field(None, description="Trim start time in seconds")
|
|
end_time: Optional[float] = Field(None, description="Trim end time in seconds")
|
|
resolution_limit: Optional[int] = Field(None, description="Max width for decode")
|
|
clock_id: Optional[str] = Field(None, description="Sync clock ID")
|
|
|
|
|
|
class PictureSourceListResponse(BaseModel):
|
|
"""List of picture sources response."""
|
|
|
|
streams: List[PictureSourceResponse] = Field(description="List of picture sources")
|
|
count: int = Field(description="Number of streams")
|
|
|
|
|
|
class PictureSourceTestRequest(BaseModel):
|
|
"""Request to test a picture source."""
|
|
|
|
capture_duration: float = Field(default=5.0, ge=0.0, le=30.0, description="Duration to capture in seconds (0 = single frame)")
|
|
border_width: int = Field(default=10, ge=1, le=100, description="Border width in pixels for preview")
|
|
|
|
|
|
class ImageValidateRequest(BaseModel):
|
|
"""Request to validate an image source (URL or file path)."""
|
|
|
|
image_source: str = Field(description="Image URL or local file path")
|
|
|
|
|
|
class ImageValidateResponse(BaseModel):
|
|
"""Response from image validation."""
|
|
|
|
valid: bool = Field(description="Whether the image source is accessible and valid")
|
|
width: Optional[int] = Field(None, description="Image width in pixels")
|
|
height: Optional[int] = Field(None, description="Image height in pixels")
|
|
preview: Optional[str] = Field(None, description="Base64-encoded JPEG thumbnail")
|
|
error: Optional[str] = Field(None, description="Error message if invalid")
|