vsjetengine 1.0.0__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.
vsengine/policy.py ADDED
@@ -0,0 +1,395 @@
1
+ # vs-engine
2
+ # Copyright (C) 2022 cid-chan
3
+ # Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
4
+ # This project is licensed under the EUPL-1.2
5
+ # SPDX-License-Identifier: EUPL-1.2
6
+ """
7
+ This module implements VapourSynth's Environment Policy system,
8
+ allowing you to manage multiple VapourSynth environments within a single application.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import threading
14
+ from abc import ABC, abstractmethod
15
+ from collections.abc import Iterator
16
+ from contextlib import AbstractContextManager, contextmanager
17
+ from contextvars import ContextVar
18
+ from logging import getLogger
19
+ from types import MappingProxyType, TracebackType
20
+ from typing import TYPE_CHECKING, Self
21
+ from weakref import ReferenceType, ref
22
+
23
+ from vapoursynth import (
24
+ AudioNode,
25
+ Core,
26
+ Environment,
27
+ EnvironmentData,
28
+ EnvironmentPolicy,
29
+ EnvironmentPolicyAPI,
30
+ VideoOutputTuple,
31
+ core,
32
+ get_outputs,
33
+ register_policy,
34
+ )
35
+
36
+ from ._hospice import admit_environment
37
+
38
+ __all__ = ["ContextVarStore", "GlobalStore", "ManagedEnvironment", "Policy", "ThreadLocalStore"]
39
+
40
+
41
+ logger = getLogger(__name__)
42
+
43
+
44
+ class EnvironmentStore(ABC):
45
+ """
46
+ Environment Stores manage which environment is currently active.
47
+ """
48
+
49
+ @abstractmethod
50
+ def set_current_environment(self, environment: ReferenceType[EnvironmentData] | None) -> None:
51
+ """
52
+ Set the current environment in the store.
53
+ """
54
+
55
+ @abstractmethod
56
+ def get_current_environment(self) -> ReferenceType[EnvironmentData] | None:
57
+ """
58
+ Retrieve the current environment from the store (if any)
59
+ """
60
+
61
+
62
+ class GlobalStore(EnvironmentStore):
63
+ """
64
+ This is the simplest store: It just stores the environment in a variable.
65
+ """
66
+
67
+ _current: ReferenceType[EnvironmentData] | None
68
+ __slots__ = ("_current",)
69
+
70
+ def set_current_environment(self, environment: ReferenceType[EnvironmentData] | None) -> None:
71
+ self._current = environment
72
+
73
+ def get_current_environment(self) -> ReferenceType[EnvironmentData] | None:
74
+ return getattr(self, "_current", None)
75
+
76
+
77
+ class ThreadLocalStore(EnvironmentStore):
78
+ """
79
+ For simple threaded applications, use this store.
80
+
81
+ It will store the environment in a thread-local variable.
82
+ """
83
+
84
+ _current: threading.local
85
+
86
+ def __init__(self) -> None:
87
+ self._current = threading.local()
88
+
89
+ def set_current_environment(self, environment: ReferenceType[EnvironmentData] | None) -> None:
90
+ self._current.environment = environment
91
+
92
+ def get_current_environment(self) -> ReferenceType[EnvironmentData] | None:
93
+ return getattr(self._current, "environment", None)
94
+
95
+
96
+ class ContextVarStore(EnvironmentStore):
97
+ """
98
+ If you are using AsyncIO or similar frameworks, use this store.
99
+ """
100
+
101
+ _current: ContextVar[ReferenceType[EnvironmentData] | None]
102
+
103
+ def __init__(self, name: str = "vapoursynth") -> None:
104
+ self._current = ContextVar(name)
105
+
106
+ def set_current_environment(self, environment: ReferenceType[EnvironmentData] | None) -> None:
107
+ self._current.set(environment)
108
+
109
+ def get_current_environment(self) -> ReferenceType[EnvironmentData] | None:
110
+ return self._current.get(None)
111
+
112
+
113
+ class _ManagedPolicy(EnvironmentPolicy):
114
+ """
115
+ This class directly interfaces with VapourSynth.
116
+ """
117
+
118
+ __slots__ = ("_api", "_local", "_mutex", "_store")
119
+
120
+ def __init__(self, store: EnvironmentStore) -> None:
121
+ self._store = store
122
+ self._mutex = threading.Lock()
123
+ self._local = threading.local()
124
+
125
+ # For engine-calls that require vapoursynth but
126
+ # should not make their switch observable from the outside.
127
+
128
+ # Start the section.
129
+ def inline_section_start(self, environment: EnvironmentData) -> None:
130
+ self._local.environment = environment
131
+
132
+ # End the section.
133
+ def inline_section_end(self) -> None:
134
+ del self._local.environment
135
+
136
+ @property
137
+ def api(self) -> EnvironmentPolicyAPI:
138
+ if hasattr(self, "_api"):
139
+ return self._api
140
+
141
+ raise RuntimeError("Invalid state: No access to the current API")
142
+
143
+ def on_policy_registered(self, special_api: EnvironmentPolicyAPI) -> None:
144
+ self._api = special_api
145
+ logger.debug("Environment policy %r successfully registered with VapourSynth.", special_api)
146
+
147
+ def on_policy_cleared(self) -> None:
148
+ del self._api
149
+ logger.debug("Environment policy successfully cleared.")
150
+
151
+ def get_current_environment(self) -> EnvironmentData | None:
152
+ # For small segments, allow switching the environment inline.
153
+ # This is useful for vsengine-functions that require access to the
154
+ # vapoursynth api, but don't want to invoke the store for it.
155
+ if (env := getattr(self._local, "environment", None)) is not None and self.is_alive(env):
156
+ return env
157
+
158
+ # We wrap everything in a mutex to make sure
159
+ # no context-switch can reliably happen in this section.
160
+ with self._mutex:
161
+ current_environment = self._store.get_current_environment()
162
+ if current_environment is None:
163
+ return None
164
+
165
+ if current_environment() is None:
166
+ logger.warning("Environment reference from store resolved to dead object: %r", current_environment)
167
+ self._store.set_current_environment(None)
168
+ return None
169
+
170
+ received_environment = current_environment()
171
+
172
+ if TYPE_CHECKING:
173
+ assert received_environment
174
+
175
+ if not self.is_alive(received_environment):
176
+ logger.warning(
177
+ "Received environment object is not alive (Garbage collected?): %r",
178
+ received_environment,
179
+ )
180
+ # Remove the environment.
181
+ self._store.set_current_environment(None)
182
+ return None
183
+
184
+ return received_environment
185
+
186
+ def set_environment(self, environment: EnvironmentData | None) -> EnvironmentData | None:
187
+ with self._mutex:
188
+ previous_environment = self._store.get_current_environment()
189
+
190
+ if environment is not None and not self.is_alive(environment):
191
+ logger.warning("Attempted to set environment which is not alive: %r", environment)
192
+ self._store.set_current_environment(None)
193
+ else:
194
+ logger.debug("Environment successfully set to: %r", environment)
195
+ if environment is None:
196
+ self._store.set_current_environment(None)
197
+ else:
198
+ self._store.set_current_environment(ref(environment))
199
+
200
+ if previous_environment is not None:
201
+ return previous_environment()
202
+
203
+ return None
204
+
205
+
206
+ class ManagedEnvironment(AbstractContextManager["ManagedEnvironment"]):
207
+ """
208
+ Represents a VapourSynth environment that is managed by a policy.
209
+ """
210
+
211
+ __slots__ = ("_data", "_environment", "_policy")
212
+
213
+ def __init__(self, environment: Environment, data: EnvironmentData, policy: Policy) -> None:
214
+ self._environment = environment
215
+ self._data = data
216
+ self._policy = policy
217
+
218
+ def __enter__(self) -> Self:
219
+ return self
220
+
221
+ def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None:
222
+ self.dispose()
223
+
224
+ @property
225
+ def core(self) -> Core:
226
+ """
227
+ Returns the core representing this environment.
228
+ """
229
+ with self.inline_section():
230
+ return core.core
231
+
232
+ @property
233
+ def disposed(self) -> bool:
234
+ """
235
+ Checks if the environment is disposed
236
+ """
237
+ return not hasattr(self, "_data")
238
+
239
+ @property
240
+ def outputs(self) -> MappingProxyType[int, VideoOutputTuple | AudioNode]:
241
+ """
242
+ Returns the outputs within this environment.
243
+ """
244
+ with self.inline_section():
245
+ return get_outputs()
246
+
247
+ @property
248
+ def vs_environment(self) -> Environment:
249
+ """
250
+ Returns the vapoursynth.Environment-object representing this environment.
251
+ """
252
+ return self._environment
253
+
254
+ def switch(self) -> None:
255
+ """
256
+ Switches to the given environment without storing
257
+ which environment has been defined previously.
258
+ """
259
+ self._environment.use().__enter__()
260
+
261
+ def dispose(self) -> None:
262
+ """
263
+ Disposes of the environment.
264
+ """
265
+ if self.disposed:
266
+ return
267
+
268
+ logger.debug("Starting disposal of environment: %r", self._data)
269
+
270
+ admit_environment(self._data, self.core)
271
+ self._policy.api.destroy_environment(self._data)
272
+ del self._data
273
+
274
+ @contextmanager
275
+ def inline_section(self) -> Iterator[None]:
276
+ """
277
+ Private API!
278
+
279
+ Switches to the given environment within the block without
280
+ notifying the store.
281
+
282
+ If you follow the rules below, switching the environment
283
+ will be invisible to the caller.
284
+
285
+ Rules for safely calling this function:
286
+ - Do not suspend greenlets within the block!
287
+ - Do not yield or await within the block!
288
+ - Do not use __enter__ and __exit__ directly.
289
+ - This function is not reentrant.
290
+ """
291
+ self._policy.managed.inline_section_start(self._data)
292
+ try:
293
+ yield
294
+ finally:
295
+ self._policy.managed.inline_section_end()
296
+
297
+ @contextmanager
298
+ def use(self) -> Iterator[None]:
299
+ """
300
+ Switches to this environment within a block.
301
+ """
302
+ # prev_environment = self._policy.managed._store.get_current_environment()
303
+ with self._environment.use():
304
+ yield
305
+
306
+ # FIXME
307
+ # # Workaround: On 32bit systems, environment policies do not reset.
308
+ # self._policy.managed.set_environment(prev_environment)
309
+
310
+ def __del__(self) -> None:
311
+ if self.disposed:
312
+ return
313
+
314
+ import warnings
315
+
316
+ warnings.warn(f"Disposing {self!r} inside __del__. This might cause leaks.", ResourceWarning)
317
+ self.dispose()
318
+
319
+
320
+ class Policy(AbstractContextManager["Policy"]):
321
+ """
322
+ A managed policy is a very simple policy that just stores the environment
323
+ data within the given store.
324
+
325
+ For convenience (especially for testing), this is a context manager that
326
+ makes sure policies are being unregistered when leaving a block.
327
+ """
328
+
329
+ _managed: _ManagedPolicy
330
+
331
+ def __init__(self, store: EnvironmentStore, flags_creation: int = 0) -> None:
332
+ """
333
+ Initializes a new Policy
334
+
335
+ Args:
336
+ store: The store to use for managing environments.
337
+ flags_creation: The flags to use when creating environments.
338
+ See vapoursynth.CoreCreationFlags for more information.
339
+ """
340
+ self._managed = _ManagedPolicy(store)
341
+ self.flags_creation = flags_creation
342
+
343
+ def __enter__(self) -> Self:
344
+ self.register()
345
+ return self
346
+
347
+ def __exit__(self, _: type[BaseException] | None, __: BaseException | None, ___: TracebackType | None) -> None:
348
+ self.unregister()
349
+
350
+ @property
351
+ def api(self) -> EnvironmentPolicyAPI:
352
+ """
353
+ Returns the API instance for more complex interactions.
354
+
355
+ You will rarely need to use this directly.
356
+ """
357
+ return self._managed.api
358
+
359
+ @property
360
+ def managed(self) -> _ManagedPolicy:
361
+ """
362
+ Returns the actual policy within VapourSynth.
363
+
364
+ You will rarely need to use this directly.
365
+ """
366
+ return self._managed
367
+
368
+ def register(self) -> None:
369
+ """
370
+ Registers the policy with VapourSynth.
371
+ """
372
+ register_policy(self._managed)
373
+
374
+ def unregister(self) -> None:
375
+ """
376
+ Unregisters the policy from VapourSynth.
377
+ """
378
+ self._managed.api.unregister_policy()
379
+
380
+ def new_environment(self) -> ManagedEnvironment:
381
+ """
382
+ Creates a new VapourSynth core.
383
+
384
+ You need to call `dispose()` on this environment when you are done
385
+ using the new environment.
386
+
387
+ For convenience, a managed environment will also serve as a
388
+ context-manager that disposes the environment automatically.
389
+ """
390
+ data = self.api.create_environment(self.flags_creation)
391
+ env = self.api.wrap_environment(data)
392
+
393
+ menv = ManagedEnvironment(env, data, self)
394
+ logger.debug("Successfully created new environment %r", data)
395
+ return menv
vsengine/py.typed ADDED
File without changes
vsengine/video.py ADDED
@@ -0,0 +1,180 @@
1
+ # vs-engine
2
+ # Copyright (C) 2022 cid-chan
3
+ # Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
4
+ # This project is licensed under the EUPL-1.2
5
+ # SPDX-License-Identifier: EUPL-1.2
6
+ """
7
+ vsengine.render renders video frames for you.
8
+ """
9
+
10
+ from collections.abc import Iterator, Sequence
11
+ from concurrent.futures import Future
12
+
13
+ import vapoursynth as vs
14
+
15
+ from ._futures import UnifiedFuture, unified
16
+ from ._helpers import use_inline
17
+ from ._nodes import buffer_futures, close_when_needed
18
+ from .policy import ManagedEnvironment
19
+
20
+ __all__ = ["frame", "frames", "planes", "render"]
21
+
22
+
23
+ @unified(kind="future")
24
+ def frame(
25
+ node: vs.VideoNode, frameno: int, env: vs.Environment | ManagedEnvironment | None = None
26
+ ) -> Future[vs.VideoFrame]:
27
+ """
28
+ Request a specific frame from a node.
29
+
30
+ :param node: The node to request the frame from.
31
+ :param frameno: The frame number to request.
32
+ :param env: The environment to use for the request.
33
+ :return: A future that resolves to the frame.
34
+ """
35
+ with use_inline("frame", env):
36
+ return node.get_frame_async(frameno)
37
+
38
+
39
+ @unified(kind="future")
40
+ def planes(
41
+ node: vs.VideoNode,
42
+ frameno: int,
43
+ env: vs.Environment | ManagedEnvironment | None = None,
44
+ *,
45
+ planes: Sequence[int] | None = None,
46
+ ) -> Future[tuple[bytes, ...]]:
47
+ """
48
+ Request a specific frame from a node and return the planes as bytes.
49
+
50
+ :param node: The node to request the frame from.
51
+ :param frameno: The frame number to request.
52
+ :param env: The environment to use for the request.
53
+ :param planes: The planes to return. If None, all planes are returned.
54
+ :return: A future that resolves to a tuple of bytes.
55
+ """
56
+
57
+ def _extract(frame: vs.VideoFrame) -> tuple[bytes, ...]:
58
+ try:
59
+ # This might be a variable format clip.
60
+ # extract the plane as late as possible.
61
+ ps = range(len(frame)) if planes is None else planes
62
+ return tuple(bytes(frame[p]) for p in ps)
63
+ finally:
64
+ frame.close()
65
+
66
+ return frame(node, frameno, env).map(_extract)
67
+
68
+
69
+ @unified(kind="generator")
70
+ def frames(
71
+ node: vs.VideoNode,
72
+ env: vs.Environment | ManagedEnvironment | None = None,
73
+ *,
74
+ prefetch: int = 0,
75
+ backlog: int | None = None,
76
+ # Unlike the implementation provided by VapourSynth,
77
+ # we don't have to care about backwards compatibility and
78
+ # can just do the right thing from the beginning.
79
+ close: bool = True,
80
+ ) -> Iterator[Future[vs.VideoFrame]]:
81
+ """
82
+ Iterate over the frames of a node.
83
+
84
+ :param node: The node to iterate over.
85
+ :param env: The environment to use for the request.
86
+ :param prefetch: The number of frames to prefetch.
87
+ :param backlog: The maximum number of frames to keep in the backlog.
88
+ :param close: Whether to close the frames automatically.
89
+ :return: An iterator of futures that resolve to the frames.
90
+ """
91
+ with use_inline("frames", env):
92
+ length = len(node)
93
+
94
+ it = (frame(node, n, env) for n in range(length))
95
+
96
+ # If backlog is zero, skip.
97
+ if backlog is None or backlog > 0:
98
+ it = buffer_futures(it, prefetch=prefetch, backlog=backlog)
99
+
100
+ if close:
101
+ it = close_when_needed(it)
102
+
103
+ return it
104
+
105
+
106
+ @unified(kind="generator")
107
+ def render(
108
+ node: vs.VideoNode,
109
+ env: vs.Environment | ManagedEnvironment | None = None,
110
+ *,
111
+ prefetch: int = 0,
112
+ backlog: int | None = 0,
113
+ y4m: bool = False,
114
+ ) -> Iterator[Future[tuple[int, bytes]]]:
115
+ """
116
+ Render a node to a stream of bytes.
117
+
118
+ :param node: The node to render.
119
+ :param env: The environment to use for the request.
120
+ :param prefetch: The number of frames to prefetch.
121
+ :param backlog: The maximum number of frames to keep in the backlog.
122
+ :param y4m: Whether to output a Y4M header.
123
+ :return: An iterator of futures that resolve to a tuple of the frame number and the frame data.
124
+ """
125
+ frame_count = len(node)
126
+
127
+ if y4m:
128
+ match node.format.color_family:
129
+ case vs.GRAY:
130
+ y4mformat = "mono"
131
+ case vs.YUV:
132
+ match (node.format.subsampling_w, node.format.subsampling_h):
133
+ case (1, 1):
134
+ y4mformat = "420"
135
+ case (1, 0):
136
+ y4mformat = "422"
137
+ case (0, 0):
138
+ y4mformat = "444"
139
+ case (2, 2):
140
+ y4mformat = "410"
141
+ case (2, 0):
142
+ y4mformat = "411"
143
+ case (0, 1):
144
+ y4mformat = "440"
145
+ case _:
146
+ raise NotImplementedError
147
+ case _:
148
+ raise ValueError("Can only use GRAY and YUV for Y4M-Streams")
149
+
150
+ if node.format.bits_per_sample > 8:
151
+ y4mformat += f"p{node.format.bits_per_sample}"
152
+
153
+ y4mformat = "C" + y4mformat + " "
154
+
155
+ data = "YUV4MPEG2 {y4mformat}W{width} H{height} F{fps_num}:{fps_den} Ip A0:0 XLENGTH={length}\n".format( # noqa: UP032
156
+ y4mformat=y4mformat,
157
+ width=node.width,
158
+ height=node.height,
159
+ fps_num=node.fps_num,
160
+ fps_den=node.fps_den,
161
+ length=frame_count,
162
+ )
163
+ yield UnifiedFuture.resolve((0, data.encode("ascii")))
164
+
165
+ current_frame = 0
166
+
167
+ def render_single_frame(frame: vs.VideoFrame) -> tuple[int, bytes]:
168
+ buf = list[bytes]()
169
+
170
+ if y4m:
171
+ buf.append(b"FRAME\n")
172
+
173
+ for plane in iter(frame):
174
+ buf.append(bytes(plane))
175
+
176
+ return current_frame, b"".join(buf)
177
+
178
+ for frame, fut in enumerate(frames(node, env, prefetch=prefetch, backlog=backlog).futures, 1):
179
+ current_frame = frame
180
+ yield UnifiedFuture.from_future(fut).map(render_single_frame)