rocket-welder-sdk 1.1.0__py3-none-any.whl → 1.1.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rocket_welder_sdk/__init__.py +22 -0
- rocket_welder_sdk/controllers.py +125 -66
- rocket_welder_sdk/external_controls/__init__.py +30 -0
- rocket_welder_sdk/external_controls/contracts.py +100 -0
- rocket_welder_sdk/external_controls/contracts_old.py +105 -0
- rocket_welder_sdk/gst_metadata.py +315 -144
- rocket_welder_sdk/rocket_welder_client.py +10 -13
- rocket_welder_sdk/ui/__init__.py +48 -0
- rocket_welder_sdk/ui/controls.py +362 -0
- rocket_welder_sdk/ui/icons.py +21628 -0
- rocket_welder_sdk/ui/ui_events_projection.py +226 -0
- rocket_welder_sdk/ui/ui_service.py +358 -0
- rocket_welder_sdk/ui/value_types.py +72 -0
- {rocket_welder_sdk-1.1.0.dist-info → rocket_welder_sdk-1.1.26.dist-info}/METADATA +4 -2
- rocket_welder_sdk-1.1.26.dist-info/RECORD +20 -0
- rocket_welder_sdk-1.1.0.dist-info/RECORD +0 -11
- {rocket_welder_sdk-1.1.0.dist-info → rocket_welder_sdk-1.1.26.dist-info}/WHEEL +0 -0
- {rocket_welder_sdk-1.1.0.dist-info → rocket_welder_sdk-1.1.26.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""External Controls event contracts for RocketWelder SDK (legacy - for backward compatibility)."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from uuid import UUID, uuid4
|
|
6
|
+
|
|
7
|
+
from rocket_welder_sdk.ui.value_types import ControlType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ArrowDirection(Enum):
|
|
11
|
+
"""Arrow directions for ArrowGrid control."""
|
|
12
|
+
|
|
13
|
+
UP = "Up"
|
|
14
|
+
DOWN = "Down"
|
|
15
|
+
LEFT = "Left"
|
|
16
|
+
RIGHT = "Right"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Container → UI Commands (Stream: ExternalCommands-{SessionId})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DefineControl:
|
|
24
|
+
"""Command to define a new control in the UI."""
|
|
25
|
+
|
|
26
|
+
control_id: str
|
|
27
|
+
type: ControlType
|
|
28
|
+
properties: dict[str, str]
|
|
29
|
+
region_name: str
|
|
30
|
+
id: UUID = field(default_factory=uuid4)
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict[str, object]:
|
|
33
|
+
"""Convert to dictionary for EventStore."""
|
|
34
|
+
return {
|
|
35
|
+
"Id": str(self.id),
|
|
36
|
+
"ControlId": self.control_id,
|
|
37
|
+
"Type": self.type.value,
|
|
38
|
+
"Properties": self.properties,
|
|
39
|
+
"RegionName": self.region_name,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class DeleteControl:
|
|
45
|
+
"""Command to delete a control from the UI."""
|
|
46
|
+
|
|
47
|
+
control_id: str
|
|
48
|
+
id: UUID = field(default_factory=uuid4)
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict[str, str]:
|
|
51
|
+
"""Convert to dictionary for EventStore."""
|
|
52
|
+
return {"Id": str(self.id), "ControlId": self.control_id}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class ChangeControls:
|
|
57
|
+
"""Command to update properties of multiple controls."""
|
|
58
|
+
|
|
59
|
+
updates: dict[str, dict[str, str]] # ControlId -> { PropertyId -> Value }
|
|
60
|
+
id: UUID = field(default_factory=uuid4)
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, object]:
|
|
63
|
+
"""Convert to dictionary for EventStore."""
|
|
64
|
+
return {"Id": str(self.id), "Updates": self.updates}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# UI → Container Events (Stream: ExternalEvents-{SessionId})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class ButtonDown:
|
|
72
|
+
"""Event when button is pressed."""
|
|
73
|
+
|
|
74
|
+
control_id: str
|
|
75
|
+
id: UUID = field(default_factory=uuid4)
|
|
76
|
+
|
|
77
|
+
def to_dict(self) -> dict[str, str]:
|
|
78
|
+
"""Convert to dictionary for EventStore."""
|
|
79
|
+
return {"Id": str(self.id), "ControlId": self.control_id}
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_dict(cls, data: dict[str, object]) -> "ButtonDown":
|
|
83
|
+
"""Create from EventStore data."""
|
|
84
|
+
return cls(
|
|
85
|
+
control_id=str(data["ControlId"]), id=UUID(str(data["Id"])) if "Id" in data else uuid4()
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class ButtonUp:
|
|
91
|
+
"""Event when button is released."""
|
|
92
|
+
|
|
93
|
+
control_id: str
|
|
94
|
+
id: UUID = field(default_factory=uuid4)
|
|
95
|
+
|
|
96
|
+
def to_dict(self) -> dict[str, str]:
|
|
97
|
+
"""Convert to dictionary for EventStore."""
|
|
98
|
+
return {"Id": str(self.id), "ControlId": self.control_id}
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_dict(cls, data: dict[str, object]) -> "ButtonUp":
|
|
102
|
+
"""Create from EventStore data."""
|
|
103
|
+
return cls(
|
|
104
|
+
control_id=str(data["ControlId"]), id=UUID(str(data["Id"])) if "Id" in data else uuid4()
|
|
105
|
+
)
|
|
@@ -6,9 +6,13 @@ Matches C# GstCaps and GstMetadata functionality.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
-
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
13
|
+
import numpy as np
|
|
14
|
+
import numpy.typing as npt
|
|
15
|
+
|
|
12
16
|
|
|
13
17
|
@dataclass
|
|
14
18
|
class GstCaps:
|
|
@@ -16,172 +20,312 @@ class GstCaps:
|
|
|
16
20
|
GStreamer capabilities representation.
|
|
17
21
|
|
|
18
22
|
Represents video format capabilities including format, dimensions, framerate, etc.
|
|
23
|
+
Matches the C# GstCaps implementation with proper parsing and numpy integration.
|
|
19
24
|
"""
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
width: int
|
|
27
|
+
height: int
|
|
28
|
+
format: str
|
|
29
|
+
depth_type: type[np.uint8] | type[np.uint16]
|
|
30
|
+
channels: int
|
|
31
|
+
bytes_per_pixel: int
|
|
32
|
+
framerate_num: int | None = None
|
|
33
|
+
framerate_den: int | None = None
|
|
34
|
+
interlace_mode: str | None = None
|
|
27
35
|
colorimetry: str | None = None
|
|
28
|
-
|
|
36
|
+
caps_string: str | None = None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def frame_size(self) -> int:
|
|
40
|
+
"""Calculate the expected frame size in bytes."""
|
|
41
|
+
return self.width * self.height * self.bytes_per_pixel
|
|
29
42
|
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
@property
|
|
44
|
+
def framerate(self) -> float | None:
|
|
45
|
+
"""Get framerate as double (FPS)."""
|
|
46
|
+
if (
|
|
47
|
+
self.framerate_num is not None
|
|
48
|
+
and self.framerate_den is not None
|
|
49
|
+
and self.framerate_den > 0
|
|
50
|
+
):
|
|
51
|
+
return self.framerate_num / self.framerate_den
|
|
52
|
+
return None
|
|
32
53
|
|
|
33
54
|
@classmethod
|
|
34
|
-
def
|
|
55
|
+
def parse(cls, caps_string: str) -> GstCaps:
|
|
35
56
|
"""
|
|
36
|
-
|
|
57
|
+
Parse GStreamer caps string.
|
|
58
|
+
Example: "video/x-raw, format=(string)RGB, width=(int)640, height=(int)480, framerate=(fraction)30/1"
|
|
37
59
|
|
|
38
60
|
Args:
|
|
39
|
-
|
|
61
|
+
caps_string: GStreamer caps string
|
|
40
62
|
|
|
41
63
|
Returns:
|
|
42
64
|
GstCaps instance
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: If caps string is invalid
|
|
43
68
|
"""
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
format=data.get("format"),
|
|
47
|
-
width=data.get("width"),
|
|
48
|
-
height=data.get("height"),
|
|
49
|
-
framerate=data.get("framerate"),
|
|
50
|
-
pixel_aspect_ratio=data.get("pixel-aspect-ratio"),
|
|
51
|
-
interlace_mode=data.get("interlace-mode"),
|
|
52
|
-
colorimetry=data.get("colorimetry"),
|
|
53
|
-
chroma_site=data.get("chroma-site"),
|
|
54
|
-
)
|
|
69
|
+
if not caps_string or not caps_string.strip():
|
|
70
|
+
raise ValueError("Empty caps string")
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
known_fields = {
|
|
58
|
-
"format",
|
|
59
|
-
"width",
|
|
60
|
-
"height",
|
|
61
|
-
"framerate",
|
|
62
|
-
"pixel-aspect-ratio",
|
|
63
|
-
"interlace-mode",
|
|
64
|
-
"colorimetry",
|
|
65
|
-
"chroma-site",
|
|
66
|
-
}
|
|
72
|
+
caps_string = caps_string.strip()
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
# Check if it's a video caps
|
|
75
|
+
if not caps_string.startswith("video/x-raw"):
|
|
76
|
+
raise ValueError(f"Not a video/x-raw caps string: {caps_string}")
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
try:
|
|
79
|
+
# Parse width
|
|
80
|
+
width_match = re.search(r"width=\(int\)(\d+)", caps_string)
|
|
81
|
+
if not width_match:
|
|
82
|
+
raise ValueError("Missing width in caps string")
|
|
83
|
+
width = int(width_match.group(1))
|
|
84
|
+
|
|
85
|
+
# Parse height
|
|
86
|
+
height_match = re.search(r"height=\(int\)(\d+)", caps_string)
|
|
87
|
+
if not height_match:
|
|
88
|
+
raise ValueError("Missing height in caps string")
|
|
89
|
+
height = int(height_match.group(1))
|
|
90
|
+
|
|
91
|
+
# Parse format
|
|
92
|
+
format_match = re.search(r"format=\(string\)(\w+)", caps_string)
|
|
93
|
+
format_str = format_match.group(1) if format_match else "RGB"
|
|
94
|
+
|
|
95
|
+
# Parse framerate (optional)
|
|
96
|
+
framerate_num = None
|
|
97
|
+
framerate_den = None
|
|
98
|
+
framerate_match = re.search(r"framerate=\(fraction\)(\d+)/(\d+)", caps_string)
|
|
99
|
+
if framerate_match:
|
|
100
|
+
framerate_num = int(framerate_match.group(1))
|
|
101
|
+
framerate_den = int(framerate_match.group(2))
|
|
102
|
+
|
|
103
|
+
# Parse interlace mode (optional)
|
|
104
|
+
interlace_mode = None
|
|
105
|
+
interlace_match = re.search(r"interlace-mode=\(string\)(\w+)", caps_string)
|
|
106
|
+
if interlace_match:
|
|
107
|
+
interlace_mode = interlace_match.group(1)
|
|
108
|
+
|
|
109
|
+
# Parse colorimetry (optional)
|
|
110
|
+
colorimetry = None
|
|
111
|
+
colorimetry_match = re.search(r"colorimetry=\(string\)([\w:]+)", caps_string)
|
|
112
|
+
if colorimetry_match:
|
|
113
|
+
colorimetry = colorimetry_match.group(1)
|
|
114
|
+
|
|
115
|
+
# Map format to numpy dtype and get channel info
|
|
116
|
+
depth_type, channels, bytes_per_pixel = cls._map_gstreamer_format_to_numpy(format_str)
|
|
117
|
+
|
|
118
|
+
return cls(
|
|
119
|
+
width=width,
|
|
120
|
+
height=height,
|
|
121
|
+
format=format_str,
|
|
122
|
+
depth_type=depth_type,
|
|
123
|
+
channels=channels,
|
|
124
|
+
bytes_per_pixel=bytes_per_pixel,
|
|
125
|
+
framerate_num=framerate_num,
|
|
126
|
+
framerate_den=framerate_den,
|
|
127
|
+
interlace_mode=interlace_mode,
|
|
128
|
+
colorimetry=colorimetry,
|
|
129
|
+
caps_string=caps_string,
|
|
130
|
+
)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
raise ValueError(f"Failed to parse caps string: {caps_string}") from e
|
|
73
133
|
|
|
74
134
|
@classmethod
|
|
75
|
-
def
|
|
135
|
+
def from_simple(cls, width: int, height: int, format: str = "RGB") -> GstCaps:
|
|
76
136
|
"""
|
|
77
|
-
|
|
137
|
+
Create GstCaps from simple parameters.
|
|
78
138
|
|
|
79
139
|
Args:
|
|
80
|
-
|
|
140
|
+
width: Frame width
|
|
141
|
+
height: Frame height
|
|
142
|
+
format: Pixel format (default: "RGB")
|
|
81
143
|
|
|
82
144
|
Returns:
|
|
83
145
|
GstCaps instance
|
|
84
146
|
"""
|
|
85
|
-
|
|
86
|
-
|
|
147
|
+
depth_type, channels, bytes_per_pixel = cls._map_gstreamer_format_to_numpy(format)
|
|
148
|
+
return cls(
|
|
149
|
+
width=width,
|
|
150
|
+
height=height,
|
|
151
|
+
format=format,
|
|
152
|
+
depth_type=depth_type,
|
|
153
|
+
channels=channels,
|
|
154
|
+
bytes_per_pixel=bytes_per_pixel,
|
|
155
|
+
)
|
|
87
156
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# Parse parameters
|
|
97
|
-
data: dict[str, Any] = {}
|
|
98
|
-
for param in params.split(","):
|
|
99
|
-
if "=" in param:
|
|
100
|
-
key, value = param.split("=", 1)
|
|
101
|
-
key = key.strip()
|
|
102
|
-
value_str = value.strip()
|
|
103
|
-
|
|
104
|
-
# Try to parse numeric values
|
|
105
|
-
if value_str.isdigit():
|
|
106
|
-
data[key] = int(value_str)
|
|
107
|
-
elif key in ["width", "height"] and value_str.startswith("(int)"):
|
|
108
|
-
data[key] = int(value_str[5:].strip())
|
|
109
|
-
else:
|
|
110
|
-
data[key] = value_str
|
|
111
|
-
|
|
112
|
-
return cls.from_dict(data)
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _map_gstreamer_format_to_numpy(
|
|
159
|
+
format: str,
|
|
160
|
+
) -> tuple[type[np.uint8] | type[np.uint16], int, int]:
|
|
161
|
+
"""
|
|
162
|
+
Map GStreamer format strings to numpy dtype.
|
|
163
|
+
Reference: https://gstreamer.freedesktop.org/documentation/video/video-format.html
|
|
113
164
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
result: dict[str, Any] = {}
|
|
117
|
-
|
|
118
|
-
if self.format is not None:
|
|
119
|
-
result["format"] = self.format
|
|
120
|
-
if self.width is not None:
|
|
121
|
-
result["width"] = self.width
|
|
122
|
-
if self.height is not None:
|
|
123
|
-
result["height"] = self.height
|
|
124
|
-
if self.framerate is not None:
|
|
125
|
-
result["framerate"] = self.framerate
|
|
126
|
-
if self.pixel_aspect_ratio is not None:
|
|
127
|
-
result["pixel-aspect-ratio"] = self.pixel_aspect_ratio
|
|
128
|
-
if self.interlace_mode is not None:
|
|
129
|
-
result["interlace-mode"] = self.interlace_mode
|
|
130
|
-
if self.colorimetry is not None:
|
|
131
|
-
result["colorimetry"] = self.colorimetry
|
|
132
|
-
if self.chroma_site is not None:
|
|
133
|
-
result["chroma-site"] = self.chroma_site
|
|
165
|
+
Args:
|
|
166
|
+
format: GStreamer format string
|
|
134
167
|
|
|
135
|
-
|
|
136
|
-
|
|
168
|
+
Returns:
|
|
169
|
+
Tuple of (numpy dtype, channels, bytes_per_pixel)
|
|
170
|
+
"""
|
|
171
|
+
format_upper = format.upper() if format else "RGB"
|
|
172
|
+
|
|
173
|
+
format_map = {
|
|
174
|
+
# RGB formats
|
|
175
|
+
"RGB": (np.uint8, 3, 3),
|
|
176
|
+
"BGR": (np.uint8, 3, 3),
|
|
177
|
+
"RGBA": (np.uint8, 4, 4),
|
|
178
|
+
"BGRA": (np.uint8, 4, 4),
|
|
179
|
+
"ARGB": (np.uint8, 4, 4),
|
|
180
|
+
"ABGR": (np.uint8, 4, 4),
|
|
181
|
+
"RGBX": (np.uint8, 4, 4), # RGB with padding
|
|
182
|
+
"BGRX": (np.uint8, 4, 4), # BGR with padding
|
|
183
|
+
"XRGB": (np.uint8, 4, 4), # RGB with padding
|
|
184
|
+
"XBGR": (np.uint8, 4, 4), # BGR with padding
|
|
185
|
+
# 16-bit RGB formats
|
|
186
|
+
"RGB16": (np.uint16, 3, 6),
|
|
187
|
+
"BGR16": (np.uint16, 3, 6),
|
|
188
|
+
# Grayscale formats
|
|
189
|
+
"GRAY8": (np.uint8, 1, 1),
|
|
190
|
+
"GRAY16_LE": (np.uint16, 1, 2),
|
|
191
|
+
"GRAY16_BE": (np.uint16, 1, 2),
|
|
192
|
+
# YUV planar formats (Y plane only for simplicity)
|
|
193
|
+
"I420": (np.uint8, 1, 1),
|
|
194
|
+
"YV12": (np.uint8, 1, 1),
|
|
195
|
+
"NV12": (np.uint8, 1, 1),
|
|
196
|
+
"NV21": (np.uint8, 1, 1),
|
|
197
|
+
# YUV packed formats
|
|
198
|
+
"YUY2": (np.uint8, 2, 2),
|
|
199
|
+
"UYVY": (np.uint8, 2, 2),
|
|
200
|
+
"YVYU": (np.uint8, 2, 2),
|
|
201
|
+
# Bayer formats (raw sensor data)
|
|
202
|
+
"BGGR": (np.uint8, 1, 1),
|
|
203
|
+
"RGGB": (np.uint8, 1, 1),
|
|
204
|
+
"GRBG": (np.uint8, 1, 1),
|
|
205
|
+
"GBRG": (np.uint8, 1, 1),
|
|
206
|
+
}
|
|
137
207
|
|
|
138
|
-
|
|
208
|
+
# Default to RGB if unknown
|
|
209
|
+
return format_map.get(format_upper, (np.uint8, 3, 3))
|
|
139
210
|
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
211
|
+
def create_array(
|
|
212
|
+
self, data: bytes | memoryview | npt.NDArray[np.uint8] | npt.NDArray[np.uint16]
|
|
213
|
+
) -> npt.NDArray[np.uint8] | npt.NDArray[np.uint16]:
|
|
214
|
+
"""
|
|
215
|
+
Create numpy array with proper format from data.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
data: Frame data as bytes, memoryview, or existing numpy array
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Numpy array with proper shape and dtype
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
ValueError: If data size doesn't match expected frame size
|
|
225
|
+
"""
|
|
226
|
+
# Convert memoryview to bytes if needed
|
|
227
|
+
if isinstance(data, memoryview):
|
|
228
|
+
data = bytes(data)
|
|
229
|
+
|
|
230
|
+
# If it's already a numpy array, check size and reshape if needed
|
|
231
|
+
if isinstance(data, np.ndarray):
|
|
232
|
+
if data.size * data.itemsize != self.frame_size:
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"Data size mismatch. Expected {self.frame_size} bytes for "
|
|
235
|
+
f"{self.width}x{self.height} {self.format}, got {data.size * data.itemsize}"
|
|
236
|
+
)
|
|
237
|
+
# Reshape if needed
|
|
238
|
+
if self.channels == 1:
|
|
239
|
+
return data.reshape((self.height, self.width))
|
|
240
|
+
else:
|
|
241
|
+
return data.reshape((self.height, self.width, self.channels))
|
|
242
|
+
|
|
243
|
+
# Check data size
|
|
244
|
+
if len(data) != self.frame_size:
|
|
245
|
+
raise ValueError(
|
|
246
|
+
f"Data size mismatch. Expected {self.frame_size} bytes for "
|
|
247
|
+
f"{self.width}x{self.height} {self.format}, got {len(data)}"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Create array from bytes
|
|
251
|
+
arr = np.frombuffer(data, dtype=self.depth_type)
|
|
252
|
+
|
|
253
|
+
# Reshape based on channels
|
|
254
|
+
if self.channels == 1:
|
|
255
|
+
return arr.reshape((self.height, self.width))
|
|
256
|
+
else:
|
|
257
|
+
# For multi-channel images, reshape to (height, width, channels)
|
|
258
|
+
total_pixels = self.width * self.height * self.channels
|
|
259
|
+
if self.depth_type == np.uint16:
|
|
260
|
+
# For 16-bit formats, we need to account for the item size
|
|
261
|
+
arr = arr[:total_pixels]
|
|
262
|
+
return arr.reshape((self.height, self.width, self.channels))
|
|
263
|
+
|
|
264
|
+
def create_array_from_pointer(
|
|
265
|
+
self, ptr: int, copy: bool = False
|
|
266
|
+
) -> npt.NDArray[np.uint8] | npt.NDArray[np.uint16]:
|
|
267
|
+
"""
|
|
268
|
+
Create numpy array from memory pointer (zero-copy by default).
|
|
156
269
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
270
|
+
Args:
|
|
271
|
+
ptr: Memory pointer as integer
|
|
272
|
+
copy: If True, make a copy of the data; if False, create a view
|
|
160
273
|
|
|
161
|
-
|
|
162
|
-
|
|
274
|
+
Returns:
|
|
275
|
+
Numpy array with proper shape and dtype
|
|
276
|
+
"""
|
|
277
|
+
# Calculate total elements based on depth type
|
|
278
|
+
if self.depth_type == np.uint16:
|
|
279
|
+
total_elements = self.width * self.height * self.channels
|
|
163
280
|
else:
|
|
164
|
-
|
|
281
|
+
total_elements = self.frame_size
|
|
165
282
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
"""Get framerate as a tuple of (numerator, denominator)."""
|
|
169
|
-
if not self.framerate or "/" not in self.framerate:
|
|
170
|
-
return None
|
|
283
|
+
# Create array from pointer using ctypes
|
|
284
|
+
import ctypes
|
|
171
285
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return None
|
|
286
|
+
# Create a buffer from the pointer
|
|
287
|
+
buffer_size = total_elements * self.depth_type.itemsize
|
|
288
|
+
c_buffer = (ctypes.c_byte * buffer_size).from_address(ptr)
|
|
289
|
+
arr = np.frombuffer(c_buffer, dtype=self.depth_type)
|
|
177
290
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return
|
|
291
|
+
# Reshape based on channels
|
|
292
|
+
if self.channels == 1:
|
|
293
|
+
shaped = arr.reshape((self.height, self.width))
|
|
294
|
+
else:
|
|
295
|
+
shaped = arr.reshape((self.height, self.width, self.channels))
|
|
296
|
+
|
|
297
|
+
return shaped.copy() if copy else shaped
|
|
298
|
+
|
|
299
|
+
def __str__(self) -> str:
|
|
300
|
+
"""String representation."""
|
|
301
|
+
# If we have the original caps string, return it for perfect round-tripping
|
|
302
|
+
if self.caps_string:
|
|
303
|
+
return self.caps_string
|
|
304
|
+
|
|
305
|
+
# Otherwise build a simple display string
|
|
306
|
+
fps = f" @ {self.framerate:.2f}fps" if self.framerate else ""
|
|
307
|
+
return f"{self.width}x{self.height} {self.format}{fps}"
|
|
308
|
+
|
|
309
|
+
def to_dict(self) -> dict[str, Any]:
|
|
310
|
+
"""Convert to dictionary representation."""
|
|
311
|
+
result = {
|
|
312
|
+
"width": self.width,
|
|
313
|
+
"height": self.height,
|
|
314
|
+
"format": self.format,
|
|
315
|
+
"channels": self.channels,
|
|
316
|
+
"bytes_per_pixel": self.bytes_per_pixel,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if self.framerate_num is not None:
|
|
320
|
+
result["framerate_num"] = self.framerate_num
|
|
321
|
+
if self.framerate_den is not None:
|
|
322
|
+
result["framerate_den"] = self.framerate_den
|
|
323
|
+
if self.interlace_mode:
|
|
324
|
+
result["interlace_mode"] = self.interlace_mode
|
|
325
|
+
if self.colorimetry:
|
|
326
|
+
result["colorimetry"] = self.colorimetry
|
|
327
|
+
|
|
328
|
+
return result
|
|
185
329
|
|
|
186
330
|
|
|
187
331
|
@dataclass
|
|
@@ -190,6 +334,7 @@ class GstMetadata:
|
|
|
190
334
|
GStreamer metadata structure.
|
|
191
335
|
|
|
192
336
|
Matches the JSON structure written by GStreamer plugins.
|
|
337
|
+
Compatible with C# GstMetadata record.
|
|
193
338
|
"""
|
|
194
339
|
|
|
195
340
|
type: str
|
|
@@ -207,24 +352,46 @@ class GstMetadata:
|
|
|
207
352
|
|
|
208
353
|
Returns:
|
|
209
354
|
GstMetadata instance
|
|
355
|
+
|
|
356
|
+
Raises:
|
|
357
|
+
ValueError: If JSON is invalid or missing required fields
|
|
210
358
|
"""
|
|
211
|
-
|
|
359
|
+
# Parse JSON if needed
|
|
360
|
+
if isinstance(json_data, (str, bytes)):
|
|
361
|
+
if isinstance(json_data, bytes):
|
|
362
|
+
json_data = json_data.decode("utf-8")
|
|
363
|
+
try:
|
|
364
|
+
data = json.loads(json_data)
|
|
365
|
+
except json.JSONDecodeError as e:
|
|
366
|
+
raise ValueError(f"Invalid JSON: {e}") from e
|
|
367
|
+
else:
|
|
368
|
+
data = json_data
|
|
369
|
+
|
|
370
|
+
# Validate required fields
|
|
371
|
+
if not isinstance(data, dict):
|
|
372
|
+
raise ValueError("JSON must be an object/dictionary")
|
|
373
|
+
|
|
374
|
+
# Get required fields
|
|
375
|
+
type_str = data.get("type", "")
|
|
376
|
+
version = data.get("version", "")
|
|
377
|
+
element_name = data.get("element_name", "")
|
|
212
378
|
|
|
213
|
-
# Parse caps
|
|
214
|
-
caps_data = data.get("caps"
|
|
379
|
+
# Parse caps - it's a STRING in the JSON!
|
|
380
|
+
caps_data = data.get("caps")
|
|
215
381
|
if isinstance(caps_data, str):
|
|
216
|
-
caps
|
|
382
|
+
# This is the normal case - caps is a string that needs parsing
|
|
383
|
+
caps = GstCaps.parse(caps_data)
|
|
217
384
|
elif isinstance(caps_data, dict):
|
|
218
|
-
|
|
385
|
+
# Fallback for dict format (shouldn't happen with real GStreamer)
|
|
386
|
+
# Create a simple caps from dict
|
|
387
|
+
width = caps_data.get("width", 640)
|
|
388
|
+
height = caps_data.get("height", 480)
|
|
389
|
+
format_str = caps_data.get("format", "RGB")
|
|
390
|
+
caps = GstCaps.from_simple(width, height, format_str)
|
|
219
391
|
else:
|
|
220
|
-
caps
|
|
392
|
+
raise ValueError(f"Invalid caps data type: {type(caps_data)}")
|
|
221
393
|
|
|
222
|
-
return cls(
|
|
223
|
-
type=data.get("type", ""),
|
|
224
|
-
version=data.get("version", ""),
|
|
225
|
-
caps=caps,
|
|
226
|
-
element_name=data.get("element_name", ""),
|
|
227
|
-
)
|
|
394
|
+
return cls(type=type_str, version=version, caps=caps, element_name=element_name)
|
|
228
395
|
|
|
229
396
|
def to_json(self) -> str:
|
|
230
397
|
"""Convert to JSON string."""
|
|
@@ -235,6 +402,10 @@ class GstMetadata:
|
|
|
235
402
|
return {
|
|
236
403
|
"type": self.type,
|
|
237
404
|
"version": self.version,
|
|
238
|
-
"caps": self.caps
|
|
405
|
+
"caps": str(self.caps), # Caps as string for C# compatibility
|
|
239
406
|
"element_name": self.element_name,
|
|
240
407
|
}
|
|
408
|
+
|
|
409
|
+
def __str__(self) -> str:
|
|
410
|
+
"""String representation."""
|
|
411
|
+
return f"GstMetadata(type={self.type}, element={self.element_name}, caps={self.caps})"
|